前言

之前面试的时候被问到AOP的执行顺序,当时满脑子都是Spring4和Spring5的执行顺序差别,对于嵌套的多个切面的执行顺序完全不知,一副不大聪明的样子😞。特此好好梳理一下这方面的知识,主要探讨以下几个问题:

  1. 单个切面中核心方法执行顺序?
  2. 多个切面中方法的执行顺序?
  3. 如何控制多个切面的执行顺序?
  4. AOP嵌套调用为什么会导致失效?

面试的问题:下图中的X、Y类的所有方法都配置了AOP的前置通知和后置通知,请问现在调用方法a,求通知的打印顺序。

class X{
  @Autowired
  Y y;
  
  public void a(){
    y.b();
  }
}

class Y{
  public void b(){
    this.c();
  }
  
  public void c(){
    // do something
  }
}

首先回顾一下AOP的通知类型:

  • @Around:环绕通知: 环绕目标方法执行

  • @Before:前置通知: 目标方法之前执行
  • @AfterReturning:返回后通知: 执行方法结束前执行(异常不执行)
  • @After:后置通知: 目标方法之后执行(始终执行)
  • @AfterThrowing:异常通知: 出现异常时候执行

多个AOP的执行顺序

单个切面中核心方法执行顺序

通过以下代码来验证单个切面中核心方法执行顺序(查阅了许多资料,各个版本之间变化好多啊😭,在此记录的是Spring Boot 2.3.4.RELEASE):

/**
 * 所有请求的切面
 */
@Aspect
@Component
public class RequestAspect {

  @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
  public void pointCut() {
  }

  /**
     * 当定义一个Around增强处理方法时,该方法的第一个形参必须是ProceedJoinPoint类型(至少含有一个形参),在增强处理方法体内,调用ProceedingJoinPoint参数的procedd()方法才会执行目标方法——这就是Around增强处理可以完全控制方法的执行时机、如何执行的关键;如果程序没有调用ProceedingJoinPoint参数的proceed()方法,则目标方法不会被执行。下面定义一个Around增强处理。
     */
  @Around("pointCut()")
  public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("R1-around-start");
    //此处必须将结果获取后返回才会正常对代码进行执行
    Object obj = joinPoint.proceed();
    System.out.println("R1-around-end");
    return Convert.toStr(obj) + "123";
  }

  @Before("pointCut()")
  public void before(JoinPoint joinPoint) {
    System.out.println("R1-before");
  }

  /**
     * After增强处理不管目标方法如何结束(包括成功完成和遇到异常中止两种情况),它都会被织入。
     */
  @After("pointCut()")
  public void after(JoinPoint joinPoint) {
    System.out.println("R1-after");
  }

  @AfterReturning("pointCut()")
  public void afterReturing(JoinPoint point) {
    System.out.println("R1-afterReturing");
    //return returnValue;
  }

  /**
     * 怎么获取是哪一类异常?
     */
  @AfterThrowing("pointCut()ut()")
  public void afterThrowing(JoinPoint joinPoint) {
    System.out.println("R1-afterThrowing");
  }
}

执行结果:

// 正常情况下
R1-around-start
R1-before
method invoke // 调用的被切的方法的输出结果。
R1-afterReturing
R1-after
R1-around-end
// 异常情况下
R1-around-start
R1-before
method invoke 
R1-afterThrowing
R1-after

ps:Spring boot 2.3.4.RELEASE 版本使用的AOP是spring-aop-5.2.9.RELEASE,AOP的通知顺序与Spring boot 2.1.1.RELEASE 不一样。主要注意几点:

  • 如果正常执行的话,那就是以AOP开始,以AOP收尾,@After在@AfterReturn之后,可以理解为@After在final中,@AfterReturn是返回值。
  • 如果执行过程出现异常的话,连接点后就变为@AfterThrowing,然后紧跟@After(可以理解为final块中一定执行。)

image-20210509174111790

如何控制多个AOP 切面的执行顺序

注解(推荐)

@Order(value)其中value的值越小,执行顺序越靠前。

@Order(100)
@Aspect
@Component
public class RequestAspect {}

实现接口

实现接口org.springframework.core.Ordered 种的getOrder()方法。也是value值越小,执行顺序越靠前。

@Aspect
@Component
public class RequestAspect implements Ordered {
  
  @Override
  public int getOrder() {
    return 0;
  }
}

同一个方法的多个切面中方法的执行顺序

还是用代码来验证,切面1:

@Order(1)
@Aspect
@Component
public class RequestAspect1 {

    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("R1-around-start");
        Object obj = joinPoint.proceed();
        System.out.println("R1-around-end");
        return Convert.toStr(obj) + "123";
    }

    @Before("pointCut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("R1-before");
    }


    @After("pointCut()")
    public void after(JoinPoint joinPoint) {
        System.out.println("R1-after");
    }

    @AfterReturning("pointCut()")
    public void afterReturing(JoinPoint point) {
        System.out.println("R1-afterReturing");
        //return returnValue;
    }

    @AfterThrowing("pointCut()")
    public void afterThrowing(JoinPoint joinPoint) {
        System.out.println("R1-afterThrowing");
    }
}

切面2:

@Order(100)
@Aspect
@Component
public class RequestAspect2  {

    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("R2-around-start");
        Object obj = joinPoint.proceed();
        System.out.println("R2-around-end");
        return Convert.toStr(obj) + "123";
    }

    @Before("pointCut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("R2-before");
    }

    @After("pointCut()")
    public void after(JoinPoint joinPoint) {
        System.out.println("R2-after");
    }

    @AfterReturning("pointCut()")
    public void afterReturing(JoinPoint point) {
        System.out.println("R2-afterReturing");
    }

    @AfterThrowing("pointCut()")
    public void afterThrowing(JoinPoint joinPoint) {
        System.out.println("R2-afterThrowing");
    }
}

执行结果:

R1-around-start
R1-before
R2-around-start
R2-before
method invoke
R2-afterReturing
R2-after
R2-around-end
R1-afterReturing
R1-after
R1-around-end

从上面的测试我们看到,确实是order越小越是最先执行,但更重要的是最先执行的最后结束。

这个不难理解,Spring AOP就是面向切面编程,什么是切面,画一个图来理解下:

image-20210509194544505

由此得出:spring aop就是一个同心圆,要执行的方法为圆心,最外层的order最小。从最外层按照AOP1、AOP2的顺序依次执行Around方法,Before方法。然后执行method方法,最后按照AOP2、AOP1的顺序依次执行AfterReturn、After、Around方法。也就是说对多个AOP来说,先before的,一定后after。R1执行AroundBefore的时候把R2当成一个整体进行,当R2执行完毕后,才会执行R1后续的。

AOP嵌套调用

AOP失效了?

Spring AOP同一个对象内部嵌套调用能生效吗?

如下,方法hello内部调用方法goodbye,同时,方法hello和goodbye都做了增强。

public class AOPInvalidDemo {
  public void hello() {
    System.out.println("Hello,IOC");
    goodbye();
  }
  public void goodbye() {
    System.out.println("Goodbye");
  }
}
@Pointcut("execution(public * io.github.silince.spring.core.aop.example.IOCServiceImpl.hello(..))")
public void testAOP1(){
}

@Around("testAOP1()")
public Object around(ProceedingJoinPoint p){
  System.out.println("around before testAOP1...");
  Object o = null;
  try {
    o = p.proceed();
  } catch (Throwable e) {
    e.printStackTrace();
  }
  System.out.println("around after testAOP1...");
  return o;
}
@Pointcut("execution(public * io.github.silince.spring.core.aop.example.IOCServiceImpl.goodbye(..))")
public void testAOP2(){
}

@Around("testAOP2()")
public void around(ProceedingJoinPoint p){
  System.out.println("around before testAOP2...");
  Object o = null;
  try {
    p.proceed();
  } catch (Throwable e) {
    e.printStackTrace();
  }
  System.out.println("around after testAOP2...");
  return o;
}

调用hello方法时,会发生什么呢?

只有hello方法被增强了,goodbye方法直接被调用,并没有被增强。输出如下:

around before testAOP1...
Hello,IOC
Goodbye
around after testAOP1...

为什么会失效?

这种结果的出现,归根结底是由Spring AOP的实现机制造成的。我们知道,Spring AOP采用代理模式实现AOP,具体的横切逻辑会被添加到动态生成的代理对象中,只要调用的是目标对象的代理对象上的方法,通常就可以保证目标对象上的方法可以被拦截。就像AOPInvalidDemo中的hello()方法执行一样,当我们调用代理对象上的hello()时,目标对象的hello()就会被成功拦截。

在代理对象方法中,不管如何添加横切逻辑,也不管添加多少横切逻辑,有一点是确定的。那就是,终归需要调用目标对象上的同一方法来执行最初所定义的方法逻辑。

如果目标对象中原始方法调用依赖于其他对象,那没问题,我们可以为目标对象注入所依赖对象的代理,并且可以保证相应Joinpoint被拦截并织入横切逻辑。而一旦目标对象中的原始方法调用直接调用自身方法的时候,也就是说,它依赖于自身所定义的其他方法的时候,问题就来了,看下面的图会更清楚:

img

🤔 在代理对象的method1方法执行经历了层层拦截器之后,最终会将调用转向目标对象上的method1,之后的调用流程全部是走在TargetObject之上,当method1调用method2时,它调用的是TargetObject上的method2,而不是ProxyObject上的method2。要知道,针对method2的横切逻辑,只织入到了ProxyObject上的method2方法中,所以,在method1中所调用的method2没有能够被成功拦截。

解决方案

当目标对象依赖于其他对象时,我们可以通过为目标注入依赖对象的代理对象,来解决相应的拦截问题。那么,当目标对象依赖于自身时,我们也可以尝试将目标对象的代理对象公开给它,只要让目标对象调用自身代理对象上的相应方法,就可以解决内部调用的方法没有被拦截的问题。

Spring AOP提供了AopContext 来公开当前目标对象的代理对象,我们只要在目标对象中使用AopContext.currentProxy()就可以取得当前目标对象所对应的代理对象。现在,我们重构目标对象,让它直接调用它的代理对象的相应方法,如下面代码所示:

public class AOPInvalidDemo {
  public void hello() {
    System.out.println("Hello,IOC");
    ((AOPInvalidDemo)AopContext.currentProxy()).goodbye();
  }
  public void goodbye() {
    System.out.println("Goodbye");
  }
}

要使AopContext.currentProxy()生效,我们在生成目标对象的代理对象时,需要设置expose-proxy为true,具体如下设置:

@EnableAspectJAutoProxy(proxyTargteClass = true, exposeProxy = true)

这种方式是可以解决问题,但是不是很优雅,因为我们的目标对象都直接绑定到了Spring AOP的具体API上了。所以,我们考虑能够通过其他方式来解决这个问题,既然我们知道能够通过AopContext.currentProxy()取得当前目标对象对应的代理对象,那完全可以在目标对象中声明对其代理对象的依赖,通过IOC容器来帮助我们注入这个代理对象。注入方式可以有多种:

  • 可以在目标对象中声明一个实例变量作为其代理对象的引用,然后由构造方法注入或者setter方法注入将AopContext.currentProxy()取得的Object注入给这个声明的实例变量;
  • 在目标对象中声明一个getter方法,如getThis(),然后通过Spring的IoC容器的方法注入或者方法替换,将这个方法的逻辑替换为return AopContext.currentProxy()。这样,在调用自身方法的时候,直接通过getThis().goodbye()就可以了;
  • 声明一个Wrapper类,并且让目标对象依赖于这个类。在Wrapper类中直接声明一个getProxy()或者类似的方法,将return AopContext.currentProxy()类似逻辑添加到这个方法中,目标对象只需要getWrapper().getProxy()就可以取得相应的代理对象。Wrapper类分离了目标对象与Spring API的直接耦合。至于让这个Wrapper以Util类出现,还是在目标对象中直接构造,或者依赖注入到目标对象,都可以;
  • 为类似的目标对象声明统一的接口定义,然后通过BeanPostProcessor处理这些接口实现类,将实现类的某个取得当前对象的代理对象的方法逻辑覆盖掉。这个与方法替换所使用的原理一样,只不过可以借助Spring的IoC容器进行批量处理而已。

实际上,这种情况的出现仅仅是因为Spring AOP采用的是代理机制实现。如果像AspectJ那样,直接将横切逻辑织入目标对象,那么代理对象和目标对象实际上就合为一体了,调用也不会出现这样的问题。

再次挑战这道面试题!

根据上一节的描述,有这么一个结论:Spring AOP同一个对象内部嵌套调用会导致内部方法无法被拦截,导致AOP失效。

现在首先调用的是a()方法,生成的是调用a()方法对象的代理对象,与容器中的y不是同一个对象。所以y.b()方法可以正常被拦截到,并生成y的代理对象。但是执行b()方法的还是被代理对象y本身,也就是this,因此调用的c()方法是不会被拦截到的。

呀嘞呀嘞daze,因此这道题的答案就是!

  • method a start –> method b start —> method b end –> method a end
// 面试的问题:下图中的X、Y类的所有方法都配置了AOP的前置通知和后置通知,请问现在调用方法a,求通知的打印顺序。
class X{
  @Autowired
  Y y;

  public void a(){
    y.b();
  }
}

class Y{
  public void b(){
    this.c();
  }

  public void c(){
    // do something
  }
}

参考:

你真的确定Spring AOP的执行顺序吗

Spring AOP 嵌套代理 - 用BeanPostProcessor实现

Spring AOP学习笔记05:AOP失效的罪因