AOP 学习基础,同时是在学习杨开振的《深入浅出 Spring Boot 2.x》过程中,针对其中 AOP 一章的相关代码片段给出的完整代码。
项目Gitee地址:https://gitee.com/loveinthesky/springboot
只看 demo02 中的内容即可。

2.1 demo01:一个约定编程示例

  1. interceptor / Myinterceptor: 自定义一个拦截器,拦截器中提供织入流程的方法。
  2. ProxyBean: 代理对象,在代理对象中的 invoke 方法定义约定的流程,流程中的方法来自拦截器。
  3. 测试用例 t1: 约定编程的测试用例。

2.2 AOP 编程的一个实例

所有的切面,都必须在 Springboot 的启动类中,利用 @Bean 注解进行注入。

2.2.1 demo02:AOP编程,非环绕通知
  1. aspect / MyAspect01: 利用 @Aspect 注解定义一个切面。切面中未加入around环绕通知。切面流程为:
  1. 先执行 Before
  2. 再执行 After
  3. 未出现异常执行 AfterReturning,出现异常执行 AfterThrowing
  1. pointCut: 利用 @Pointcut() 注解定义切点,切点的作用在于标明什么方法在执行时才会启用 AOP。execution()里的正则表达式注明了要拦截的方法。
  2. 基于 AOP 的编程,相对于上述的约定编程示例,可知,Springboot中的 AOP自动约定了流程,自动实现了代理。用户只需要创建一个切面,定义切点,即可实现面向 AOP 的编程。
  3. 在 Springboot 的启动类中,加入定义切面 MyAspect01 的代码。
  4. 测试用例: userController,调用API后(API集成在swagger中,浏览器输入:localhost:8888/swagger-ui.html),控制台打印引入 AOP 后的运行结果。

2.2.2 demo02:AOP 编程,引入环绕通知

环绕通知是所有通知中最强大的通知,强大也意味着难以控制。一般而言,使用环绕通知的场景是需要大幅度修改原有目标对象的服务逻辑时,否则尽量使用其他通知。同时,由于底层 Spring 版本之间的差异留下的问题,会使得在使用环绕通知时,Before() 的执行会受到影响,因此,在非必要时,不要使用环绕通知。

环绕通知是一个取代原有目标对象方法的通知,当然,他也提供回调原有目标对象方法的能力。

  1. MyAspect02: 利用 @Aspect 注解定义一个切面。切面中加入around环绕通知。其余与 MyAspect01并无差别。切面流程为:
  1. 先执行 Before
  2. 执行 Around
  3. 再执行 After
  4. 未出现异常执行 AfterReturning,出现异常执行 AfterThrowing
  1. 测试用例t2: 引入环绕通知后,进行的测试。注意,此时 MyAspect01 中的 @Aspect 注解需要取消。

注意:

在引入环绕通知中,设计中的输出结果是:

before.... around before.... id = 2222 username = zhangsan 使用环绕通知 around after.... after.... afterReturning....

而在实际测试中,出现的结果是如下。先打印出了 around before…

在测试中,引入环绕通知后,环绕通知将被率先执行,且:

  1. 环绕通知中未回调原有目标对象的方法,before() 将不会被执行。执行完 around() 后,后续正常。
  2. 环绕通知中回调原有目标对象的方法,先执行 around() 中回调之前的代码,在执行回调的过程中会按照切面的流程执行,先执行 Before,再实行回调的方法以及 around() 中回调之后的代码,后续正常。

around before.... before.... id = 2222 username = zhangsan 使用环绕通知 around after.... after.... afterReturning....


2.2.3 demo02:功能增强 + 通知获取参数

对应切面为 MyAspect03,该切面必须在 Springboot 的启动类中,以Bean注解的形式加入,否则将出错。

测试用例:t3

1. 功能增强
  1. 对原本的 UserService 功能进行增强,为此,在 service 中添加一个用户检测接口 UserValidator 及其实现类。在切面中加入该用户检测接口。
  2. @DeclareParents: 引入新的类来增强服务,有两个必须配置的属性:
  • value:指向你要增强功能的目标对象,这里是增强 UserServiceImpl 对象,则 value 的值为 com.yr.springboot.demo02.service.UserServiceImpl。
  • defaultImpl:引入增强功能的类,该类只能是非抽象类。“+”号表示这个类的子类。

注意 UserServiceImpl后面不添加 “+” 号,在Springboot2 之后,“+”加号表示这个类的子类,不包括其本身。

2. 通知获取参数
  1. 在非环绕接口中:
  • 在注解中添加:args(user),表明传入的参数名称为 user。
  • 在方法中利用 JoinPoint 传入对应的参数。方法传参中的 User user 不可省略,且与注解中的参数名称保持一致。
  1. 在环绕接口中:
  • 在注解中添加:args(user),表明传入的参数名称为 user。
  • 在方法中利用 ProceedingJoinPoint 传入对应的参数。方法传参中的 User user 不可省略,且与注解中的参数名称保持一致。

2.2.4 demo02:织入

织入是一个生成动态代理对象并且将切面和目标对象方法编织成为约定流程的过程。对于流程上的通知,前文已经有了比较完善的说明。而对于动态代理的实现,也有多种方法。

Spring采用了 JDK 和 CGLIB 两种方式实现动态代理。前文的 接口+实现类 的方式正是 JDK 的方式。而 CGLIB 的方式则不需要实现接口,直接在 service 类中添加 @Service 注解即可。

  1. aspect / MyAspect04: 对 CGLIB 方式进行测试。
  2. service / TestService: service 类添加 @Service 注解。
  3. 测试用例:t4

2.3 多个切面

Springboot 支持多个切面的同时运行。并提供确定切面运行顺序的两种方式:@Order注解 和 Ordered接口。

  1. manyAspect: 创建多个切面,每一个切面的切入点是一样的。
  2. 利用 @Order() 注解限定切面的执行顺序。切面执行的顺序即 @Order 注解中的数字(int型) 从小到大排列的顺序。
  3. service / TestService / test( ): 切入点。
  4. 测试用例:t5

测试结果如下。对于前置通知(before),都是从小到大运行的,对于后置通知(after),则是从大到小运行。其实质,是多个切面之间的层层嵌套,这是一个典型的责任链模式的顺序。

Aspect01 before...
Aspect02 before...
Aspect03 before...
多个切面测试...
Aspect03 after...
Aspect03 afterReturning...
Aspect02 after...
Aspect02 afterReturning...
Aspect01 after...
Aspect01 afterReturning...

利用接口 Ordered 也可以限定切面执行的顺序。

@Aspect
public class Aspect01 implements Ordered {
    @Override
    public int getOrder() {
        return 1;
    }
  ...
}