AOP 与 Spring AOP
AOP:面向切面编程。是一种思想,对某一类事情的集中处理。
例如现在大多数平台的用户登录都是有权限效验的,而对于平台页面的操作,除过登录,注册或者一些简单的,大多都是需要验证用户有没有登录,没有登录的话就没有权限去做一些操作。在这个效验的过程中,没有使用 AOP 之前是需要对每个方法都实现效应操作的,这样以来就会增加代码的重复度,效率减少,而实现 AOP 之后,只需要在某一个类中单独配置切面,这样一来就不用在每个方法中实现效验代码了。
Spring AOP:是对该思想的具体实现。
也就是说当如上述那样功能统一,且使用的地方较多的时候,就可以考虑使用 AOP 进行统一处理了。
除了上述统一用户登录效验以外,AOP 还可以实现:
- 统一日志记录:利用 AOP 实现日志记录。通过 AOP 当一个方法执行的时候会自动打印出自定日志;
- 事务管理:通过 @Transaction 注解实现对异常事务进行回滚操作,免去了重复的事务管理逻辑。@Transaction 注解就是基于 AOP 实现;
- 性能统计:利用 AOP 在目标方法执行前后统计方法执行的时间;
- 权限控制:使用 AOP 在执行目标方法前判断用户是否具备所需要的权限,具备就执行目标方法,不具备就不执行(也就是上述举例);
- 缓存管理:使用 AOP 在目标方法执行前进行缓存读取和更新;等等、等等。
AOP 组成:
- 切面:定义事件(即当前 AOP 是干啥的),例如当前 AOP 是 用户登录效验 事件,对用户登录效验进行集中处理;
- 切点:定义具体规则。例如定义用户登录拦截规则,哪些接口/方法需要判断用户权限,哪些不需要;
- 通知:AOP 执行的具体方法;
- 连接点:有可能触发切点的所有点;例如所有的接口或者方法都有可能触发。
AOP 的实现原理:
- 常见的实现方式是通过动态代理、字节码操作进行实现
- Spring AOP 基于动态代理实现
Spring AOP 的实现步骤:
- 添加 Spring AOP 依赖;
- 定义切面;
- 定义切点;
- 实现通知。
通知的类型:
通知定义的是被拦截的⽅法具体要执⾏的业务(即当一个方法被拦截了,会在拦截时执行一些业务方法,而通知就是定义在这些业务方法上的),⽐如⽤户登录权限验证⽅法就是具体要执⾏的业务,此时通知就会定义到该方法上,当需要验证登录权限时,⽤户登录权限验证⽅法就会自动调用。Spring AOP 中,可以在⽅法上使⽤以下注解,设置⽅法为通知⽅法,在满⾜条件后会通知本⽅法进⾏调⽤:
- 前置通知:使⽤ @Before 注解,通知⽅法会在⽬标⽅法调⽤之前执⾏。
- 后置通知:使⽤ @After 注解,通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤。
- 返回之后通知:使⽤ @AfterReturning 注解,通知⽅法会在⽬标⽅法返回后调⽤。
- 抛异常后通知:使⽤ @AfterThrowing 注解,通知⽅法会在⽬标⽅法抛出异常后调⽤。
- 环绕通知:使⽤ @Around 注解,通知方法包裹了被通知的⽅法,在被通知的⽅法通知之前和调⽤之后执⾏⾃定义的⾏为。
Spring AOP 的使用
1. 对通知的几种类型进行应用
案例:使用 Spring AOP 实现对 UserController 类中的方法进行拦截处理
1. 添加依赖:
创建 Spring Boot 项目,添加 AOP 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 定义切面:
在类上面添加 @Aspect 注解
3. 定义切点:
先实现一个 UserController 类,用于后续拦截进行测试。若被拦截了就不会执行该类里面的方法
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/setUser")
public String setUser(){
System.out.println("[UserController] do setUser");
return "[UserController] setUser";
}
@RequestMapping("/getUser")
public String getUser(){
System.out.println("[UserController] do getUser");
return "[UserController] getUser";
}
}
定义切点:
@Aspect // 定义切面,表示此类为一个切面
@Component
public class UserAspect {
// 定义切点,使⽤ AspectJ 切点表达式
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointCut(){} // 切点是没有具体实现的
}
切点表达式:
切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:
execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
其中修饰符和异常一般都会省略
三种通配符:
1. * :匹配任意字符,只匹配⼀个元素(包,类,或⽅法,⽅法参数)
2. .. : 匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使⽤
3. + : 表示按照类型匹配指定类的所有类,必须跟在类名后⾯,如 com.demo.Animal+ ,表示继承该类的所有⼦类包括本身
4. 定义通知:
上面也提到了,通知有五中,而前4种(前置通知、后置通知、返回通知、异常通知)实现方法都是类似的,只需要在方法前面加各自的注解即可。而环绕通知会单独实现。
下面就先针对 UserController 这个类,实现前置通知和后置通知,返回通知和异常通知与前置、后置通知是类似的,只需要改变注解即可,在此就不另外实现。
定义前置通知:
@Aspect // 定义切面,表示此类为一个切面
@Component
public class UserAspect {
// 定义切点
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointCut(){} // 切点是没有具体实现的
// 通知,前置通知
@Before("pointCut()") // 双引号里面写的是 该通知针对哪个切点
public void doBefore(){
System.out.println("[UserAspect] 执行了前置通知");
}
}
启动项目,通过 URL 运行结果如下:
就会发现拦截器已经拦截成功了。当执行 UserController 里面的方法时,会先执行通知方法。
定义后置通知:
@Aspect // 定义切面,表示此类为一个切面
@Component
public class UserAspect {
// 定义切点
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointCut(){} // 切点是没有具体实现的
// 通知,前置通知
@Before("pointCut()") // 双引号里面写的是 该通知针对哪个切点
public void doBefore(){
System.out.println("[UserAspect] 执行了前置通知");
}
// 通知,后置通知
@After("pointCut()")
public void doAfter(){
System.out.println("[UserAspect] ---执行后置通知---");
}
}
启动项目,查看输出结果:
顾名思义,当使用后置通知的时候,通知是在 UserController 中的方法执行之后才进行通知的。
定义环绕通知:
@Aspect // 定义切面,表示此类为一个切面
@Component
public class UserAspect {
// 定义切点
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointCut(){} // 切点是没有具体实现的
// 通知,前置通知
@Before("pointCut()") // 双引号里面写的是 该通知针对哪个切点
public void doBefore(){
System.out.println("[UserAspect] 执行了前置通知");
}
// 通知,后置通知
@After("pointCut()")
public void doAfter(){
System.out.println("[UserAspect] ---执行后置通知---");
}
// 环绕通知
@Around("pointCut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { // ProceedingJoinPoint:获取连接点这个对象,然后通过连接点的对象中的 proceed 方法来对目标方法进行执行
System.out.println("环绕通知执行前");
Object result = joinPoint.proceed(); // joinPoint.proceed(): 执行目标方法
System.out.println("环绕通知执行后");
return result; // 将对象返回给框架
}
}
启动项目,输出结果如下:
可以发现,
- 环绕通知在执行前后都可以进行通知
- 环绕通知会在前置通知前、后置通知后进行通知
2. 对方法的性能进行检测
案例:使用 Spring AOP 实现对 UserController 类中的方法性能进行统计
还是按照上述先定义切面,定义切点、定义通知,实现代码如下:
@Slf4j // 此处使用 lombok @Slf4j 注解自定义打印日志
@Aspect // 定义切面,表示此类为一个切面
@Component
public class UserAspect {
// 定义切点,使⽤ AspectJ 切点表达式
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointCut(){} // 切点是没有具体实现的
// 环绕通知实现方法性能统计
@Around("pointCut()")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
/*
* 1. 记录开始时间
* 2. 执行目标方法
* 3. 记录结束时间
* 4. 计算消耗时间
* */
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
log.info(joinPoint.getSignature()+" : " + "[UserAspect] cost time: " +(System.currentTimeMillis() - start)+ "ms"); // 使用 Slf4j 日志输出 joinPoint.getSignature() 是用于获取哪个方法的执行的
return result;
}
}
启动项目,输入 URL ,输出结果如下:
这样以来就会将方法执行的时间统计出来,方便对代码的性能进行可视化。
Spring AOP 实现原理(基于动态代理)
Spring AOP 是通过动态代理的⽅式,在运⾏期将 AOP 代码织⼊到程序中的,它的实现⽅式有两种: JDK Proxy 和 CGLIB。
Spring AOP ⽀持 JDK Proxy 和 CGLIB ⽅式实现动态代理。默认情况下,实现了接口的类,使用 AOP 会基于 JDK Proxy ⽣成代理类,没有实现接⼝的类,会基于 CGLIB ⽣成代理类。
补充:
织入:织入是将切面和目标对象连接起来的过程,也就是将通知应用到切点匹配的连接点上。常见的织入时间有两种,分别是编译期织入和运行期织入。
JDK Proxy 和 CGLIB 的区别
- JDK Proxy 动态代理基于接口,要求目标对象实现接口;CGLIB 动态代理基于类,可以代理没有实现接口的目标对象;
- CGLIB 动态代理无法代理 final 类和 final 方法;JDK Proxy 动态代理可以代理任意类;
- JDK Proxy 动态代理性能相对较高,生成代理对象速度较快;CGLIB 动态代理性能相对较低,生成代理对象速度较慢。
Spring AOP 解决了什么问题(作用)
AOP 可以将横切关注点(如日志记录、事务管理、权限控制等)从核心业务逻辑中分离出来,实现关注点的分离。
- 横切关注点:多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等)
这样以来就避免在每个类或对象中对一些功能相同的代码进行重复实现,降低代码冗余,实现代码复用和解耦,提高代码的可维护性和可扩展性。