本文揭示了Spring AOP实现机制导致的一个小小的陷阱,分析了问题产生的原因,并给出了一些解决方案。

  前面的文章中我们介绍了Spring AOP的简单使用,并从源码的角度学习了其底层的实现原理,有了这些基础之后,本文来讨论一下Spring AOP失效的问题,这个问题可能我们在平时工作中或多或少也会碰到。这个话题应该从同一个对象内的嵌套方法调用拦截失效说起。

1. 问题的现象

  假设我们有如下对象类定义(同一对象内方法嵌套调用的目标对象示例):

public class NestableInvocationDemo {
    public void method1(){
        method2();
        System.out.println("method1 executed!");
    }

    public void method2(){
        System.out.println("method2 executed!");
    }
}

  这个类定义中需要我们关注的是它的某个方法会调用同一对象上定义的其他方法。这通常是比较常见的,在NestableInvocationDemo类中,method1()方法调用了同一个对象的method2()方法。

  现在,我们要使用Spring AOP拦截该类定义的method1()和method2()方法,比如一个简单的性能检测,我们定义一个Aspect:

@Aspect
public class PerformanceTraceAspect {

    @Pointcut("execution(public void *.method1())")
    public void method1(){}

    @Pointcut("execution(public void *.method2())")
    public void method2(){}

    @Pointcut("method1() || method2()")
    public void compositePointcut(){};

    @Around("compositePointcut()")
    public Object performanceTrace(ProceedingJoinPoint joinpoint) throws Throwable{
        StopWatch watch = new StopWatch();
        try{
            watch.start();
            return joinpoint.proceed();
        }finally{
            watch.stop();
            System.out.println("PT in method[" + joinpoint.getSignature().getName() + "]>>>>" + watch.toString());
        }
    }
}

配置文件如下: 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop = "http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
     http://www.springframework.org/schema/aop
     http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
     
    <aop:aspectj-autoproxy/>
    <bean id = "nestableInvocationDemo" class = "xxx.xxx.NestableInvocationDemo"></bean>
    <bean class = "xxx.xxx.PerformanceTraceAspect"></bean>
</beans>

执行如下代码:

public static void main(String[] args) { 

    ClassPathXmlApplicationContext factory = new ClassPathXmlApplicationContext("spring/demo/aop.xml");
    NestableInvocationDemo demo = factory.getBean("nestableInvocationDemo", NestableInvocationDemo.class);
    demo.method2();
    demo.method1();

}

输出如下结果:

method2 executed!
PT in method[method2]>>>>StopWatch '': running time (millis) = 11; [] took 11 = 100%
method2 executed!
method1 executed!
PT in method[method1]>>>>StopWatch '': running time (millis) = 0; [] took 0 = 0%

   发现问题没有?当我们从外部直接调用NestableInvocationDemo对象的method2()时,显示拦截成功了,但是当调用method1()时,却只有method1()方法的执行拦截成功,其内部的method2()方法执行却没有被拦截,因为输出日志只有PT in method[method1]的信息。这说明部分AOP失效了,这是什么原因呢,我们接着往下看。

 

2. 原因的分析

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

  不过,代理模式的实现机制在处理方法调用的时序方面,会给使用这种机制实现的AOP产品造成一个小小的“缺憾”。我们来看一下代理对象方法与目标对象方法的调用时序:

proxy.method2{
    记录方法调用开始时间;
    target.method2;
    记录方法调用结束时间;
    计算消耗的时间并记录到日志;
}

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

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

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

 

3. 解决方案

  知道原因,我们才可以“对症下药”了。

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

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

public class NestableInvocationDemo {
    public void method1(){
        ((NestableInvocationDemo)AopContext.currentProxy()).method2();
        System.out.println("method1 executed!");
    }

    public void method2(){
        System.out.println("method2 executed!");
    }
}

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

  在基于配置文件的配置中,可按如下方式配置:

<aop:aspectj-autoproxy expose-proxy = "true"/>

  在基于注解地配置中,可按如下方式配置:

@EnableAspectJAutoProxy(proxyTargteClass = true, exposeProxy = true)

  现在,我们可以得到想要的拦截结果:

method2 executed!
PT in method[method2]>>>>StopWatch '': running time (millis) = 12; [] took 12 = 100%
method2 executed!
PT in method[method2]>>>>StopWatch '': running time (millis) = 0; [] took 0 = 0%
method1 executed!
PT in method[method1]>>>>StopWatch '': running time (millis) = 0; [] took 0 = 0%

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

  注入方式可以有多种:

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

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

 

4. 总结 

  本文揭示了Spring AOP实现机制导致的一个小小的陷阱,分析了问题产生的原因,并给出了一些解决方案。

  应该说,Spring AOP作为一个轻量的AOP框架,在简单与强大之间取得了很好的平衡。合理地使用Spring AOP,将帮助我们更快更好地完成各种工作,也希望大家在Spring AOP地使用之路上愉快地前行。

版权声明:本文为volcano-liu原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/volcano-liu/p/13065736.html