一.切点表达式
在深入探讨String AOP(实际上,更常见的是Spring AOP,因为AOP通常不是直接应用于字符串处理的,而是用于解决应用程序中的横切关注点)时,切点表达式是一个核心概念。
概念:
切点表达式(Pointcut Expression) 是AOP中用于定义切入点的表达式,它指定了切面逻辑将在何处(即哪些方法或连接点)被织入。简单来说,切点表达式就是一组规则,用于告诉AOP框架在哪些具体的程序执行点上应用切面逻辑。
举例子:
假设我们有一个学校管理系统,系统中有多个模块,如学生管理、教师管理、课程管理等。这些模块在执行各自的业务逻辑时,都需要进行日志记录、权限验证等横切关注点。如果我们不使用AOP,那么每个模块都需要在自己的代码中显式地添加日志记录和权限验证的逻辑,这会导致代码重复且难以维护。
现在,我们引入AOP和切点表达式来解决这个问题。我们可以定义一个切面,该切面包含了日志记录和权限验证的逻辑。然后,我们使用切点表达式来指定这些逻辑将在哪些模块的方法上被执行。
切点表达式的作用:
-
解耦横切关注点:通过将横切关注点(如日志记录、权限验证等)与业务逻辑分离,切点表达式使得这些关注点可以在不修改业务代码的情况下被添加、修改或删除。
-
提高代码可维护性:由于横切关注点被集中管理,因此当这些关注点需要变化时,我们只需要修改切面逻辑和切点表达式,而不需要在每个业务模块中逐一修改。
-
增强代码的复用性:切点表达式允许我们将切面逻辑应用到多个不同的方法上,从而实现代码的复用。
-
简化配置:通过切点表达式,我们可以灵活地定义切面逻辑的作用范围,使得AOP的配置更加简单和直观。
切点表达式常⻅有两种表达⽅式
- execution(RR):根据⽅法的签名来匹配
- @annotation(RR) :根据注解匹配
1.execution表达式
execution() 是最常⽤的切点表达式, ⽤来匹配⽅法, 语法为:
execution(<访问修饰符> <返回类型> <包名.类名.⽅法(⽅法参数)> <异常>)
其中: 访问修饰符和异常可以省略
切点表达式⽀持通配符表达:
1.* :匹配任意字符,只匹配⼀个元素(返回类型, 包, 类名, ⽅法或者⽅法参数)
- 包名使⽤ * 表⽰任意包(⼀层包使⽤⼀个*)
- 类名使⽤ * 表⽰任意类
- 返回值使⽤ * 表⽰任意返回值类型
- ⽅法名使⽤ * 表⽰任意⽅法
- 参数使⽤ * 表⽰⼀个任意类型的参数
2.:匹配多个连续的任意符号, 可以通配任意层级的包, 或任意类型, 任意个数的参数
- 使⽤ .. 配置包名,标识此包以及此包下的所有⼦包
- 可以使⽤ .. 配置参数,任意个任意类型的参数
切点表达式示例:
TestController 下的 public修饰, 返回类型为String ⽅法名为t1, ⽆参⽅法
execution(public String com.example.demo.controller.TestController.t1())
1.省略访问修饰符
execution(String com.example.demo.controller.TestController.t1())
2.匹配所有返回类型
execution(* com.example.demo.controller.TestController.t1())
3.匹配TestController 下的所有⽆参⽅法
execution(* com.example.demo.controller.TestController.*())
4.匹配TestController 下的所有⽅法
execution(* com.example.demo.controller.TestController.*(..))
5.匹配controller包下所有的类的所有⽅法
execution(* com.example.demo.controller.*.*(..))
6.匹配所有包下⾯的TestController
execution(* com..TestController.*(..))
7.匹配com.example.demo包下, ⼦孙包下的所有类的所有⽅法
execution(* com.example.demo..*(..))
1.1@annotation
execution表达式更适⽤有规则的, 如果我们要匹配多个⽆规则的⽅法呢, ⽐如:TestController中的t1()
和UserController中的u1()这两个⽅法.
这个时候我们使⽤execution这种切点表达式来描述就不是很⽅便了.
我们可以借助⾃定义注解的⽅式以及另⼀种切点表达式 @annotation 来描述这⼀类的切点
实现步骤:
- 编写⾃定义注解
- 使⽤ @annotation 表达式来描述切点
- 在连接点的⽅法上添加⾃定义注解
准备测试代码:
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1() {
return "t1";
}@RequestMapping("/t2")
public boolean t2() {
return true;
}
}
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/u1")
public String u1(){
return "u1";
}
@RequestMapping("/u2")
public String u2(){
return "u2";
}
}
1.1.1⾃定义注解@MyAspect
创建⼀个注解类(和创建Class⽂件⼀样的流程, 选择Annotation就可以了)
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {}
代码简单说明, 了解即可. 不做过多解释
1.@Target 标识了 Annotation 所修饰的对象范围, 即该注解可以⽤在什么地⽅.
常⽤取值:
ElementType.TYPE: ⽤于描述类、接⼝(包括注解类型) 或enum声明
ElementType.METHOD: 描述⽅法
ElementType.PARAMETER: 描述参数
ElementType.TYPE_USE: 可以标注任意类型
2.@Retention 指Annotation被保留的时间⻓短, 标明注解的⽣命周期
@Retention 的取值有三种:
1. RetentionPolicy.SOURCE:表⽰注解仅存在于源代码中, 编译成字节码后会被丢弃. 这意味着 在运⾏时⽆法获取到该注解的信息, 只能在编译时使⽤. ⽐如 @SuppressWarnings , 以及lombok提供的注解 @Data , @Slf4j
2. RetentionPolicy.CLASS:编译时注解. 表⽰注解存在于源代码和字节码中, 但在运⾏时会被丢弃. 这意味着在编译时和字节码中可以通过反射获取到该注解的信息, 但在实际运⾏时⽆法获取. 通常⽤于⼀些框架和⼯具的注解.
3. RetentionPolicy.RUNTIME:运⾏时注解. 表⽰注解存在于源代码, 字节码和运⾏时中. 这意味着在编译时, 字节码中和实际运⾏时都可以通过反射获取到该注解的信息. 通常⽤于⼀些需要在运⾏时处理的注解, 如Spring的 @Controller @ResponseBody
1.1.2 切⾯类
使⽤ @annotation 切点表达式定义切点, 只对 @MyAspect ⽣效
切⾯类代码如下:
@Slf4j
@Component
@Aspect
public class MyAspectDemo {
//前置通知
@Before("@annotation(com.example.demo.aspect.MyAspect)")
public void before(){
log.info("MyAspect -> before ...");
}
//后置通知
@After("@annotation(com.example.demo.aspect.MyAspect)")
public void after(){
log.info("MyAspect -> after ...");
}
1.1.1.3 添加⾃定义注解(重点)
1.声明自定义注解@MyAspect:
package com.example.demo.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})//注解只能作用在方法上
@Retention(RetentionPolicy.RUNTIME)//生命周期是在运行时
public @interface MyAspect {
}
2.创建MyAspectDemo类实现注解声明方法
package com.example.demo.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect//表示这是一个切面类
@Component
@Slf4j
public class MyAspectDemo {
@Around("@annotation(com.example.demo.config.MyAspect)")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("do around before....");
Object o = null;
o = joinPoint.proceed();
log.info("do around after....");
return o;
}
}
3.作用在TestControlle的t2方法,UserController的u1方法
package com.example.demo.controller;
import com.example.demo.config.MyAspect;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1(){
log.info("执行t1方法...");
return "t1";
}
@MyAspect
@RequestMapping("/t2")
public String t2(){
log.info("执行t2方法...");
return "t2";
}
}
package com.example.demo.controller;
import com.example.demo.config.MyAspect;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("Test")
@Slf4j
public class UserController {
@MyAspect
public String u1(){
log.info("执行u1方法...");
return "u1";
}
public String u2(){
log.info("执行u2方法...");
return "u2";
}
}
4.启动类中启动代码
代码:
package com.example.demo;
import com.example.demo.component.UserComponent;
import com.example.demo.config.BeanConfig;
import com.example.demo.config.UserConfig;
import com.example.demo.controller.TestController;
import com.example.demo.controller.UserController;
import com.example.demo.model.UserInfo;
import com.example.demo.reop.UserReop;
import com.example.demo.service.UserService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext; // 导入正确的ApplicationContext
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
// 通过类型获取Bean
UserController bean = context.getBean(UserController.class);
TestController bean2 = context.getBean(TestController.class);
bean.u1();
bean.u2();
bean2.t1();
bean2.t2();
}
}
5.运行程序,查看日志
可以看到, 切⾯通知被执⾏了.