第12章 Spring AOP之扩展篇

本章内容

  • 有关公开当前调用的代理对象的探讨

有关公开当前调用的代理对象的探讨

注:有关公开当前调用的代理对象的探讨这句话很机械,可以这么理解:把当前目标对象所依赖的代理对象公开(暴露)给这个目标对象,这样目标对象就可以直接调用代理对象中的方法了。如果你能看到后面的解决方案 就明白了。

这个话题应该从同一个对象内的嵌套方法调用拦截失效说起。

问题的现象

假设我们有如下方代码所示的目标对象类定义。当然,这样的类定义可以映射到系统中的任何可能的业务对象。

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

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

现在,我们要使用SpringAOP拦截该类定义的method1和method2方法,比如加入一个简单的性能检测逻辑。那么可以定义一个PerformanceTraceAspect,如下方代码所示。

 
    @Aspect
    public class PerformanceTraceAspect {
        private final Log logger = LogFactory.getLog(PerformanceTraceAspect.class);
 
        @Pointcut("execution(public voiđ *.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();
                logger.info("PT in method[" +
                        joinpoint.getSignature().getName() +
                        "]>>>>>" + watch.toString());
            }
        }

我们的AroundAdvice定义会拦截compositePointcut()所指定的Joinpoint,即method1或者method2的执行。

如果将PerformnanceTraceAspect中定义的横切逻辑织入NestableInvocationBO,然后运行如下代码并查看结果:

 
    AspectJProxyFactory weaver = new AspectJProxyFactory(new NestableInvocationBO());
 
    weaver.setProxyTargetClass(true);
    weaver.addAspect(PerformanceTraceAspect.class);
 
    Object proxy = weaver.getProxy();
    ((NestableInvocationBO)proxy).method2();
    ((NestableInvocationBO)proxy).method1();
 

会得到如下的输出结果:

image-20220409202315130

发现问题没有?当我们从外部直接调用NestableInvocationBO对象的method2的时候,因为该方法签名匹配PerformanceTraceAspectAroundAdvice所对应的Pointcut定义,所以,AroundAdvice逻辑得以执行,也就是说,PerformanceTraceAspect拦截method2的执行成功了。

但是,当调用method1的时候,却只有method1方法的执行拦截成功,而method1方法内部的method2方法执行却没有被拦截,因为输出日志中只有PT in method[method1]的信息。

原因的分析

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

就像NestableInvocationBo的method2方法执行一样,当我们调用代理对象上的method2的时候,目标对象的method2就会被成功拦截。

不过,代理模式的实现机制在处理方法调用的时序方面,会给使用这种机制实现的AOP产品造成一个小小的“缺憾”。

我们来看 一般的代理对象方法与目标对象方法的调用时序 ,如下所示:

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

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

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

image.jpg

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

解决方案

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

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

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

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

要使AopContext.currentProxy()生效,我们在生成目标对象的代理对象时,需要将ProxyConfig或者它的相应子类的exposeProxy属性设置为true,如下所示:

 
    AspectJProxyFactory weaver = new AspectJProxyFactory(new NestableInvocationBO());
 
    weaver.setProxyTargetClass(true);
    // 重点是下面这行
    weaver.setExposeProxy(true);
    weaver.addAspect(PerformanceTraceAspect.class);
 
    Object proxy = weaver.getProxy();
    ((NestableInvocationBO)proxy).method2();
    ((NestableInvocationBO)proxy).method1();
 
 

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

image-20220409212311949

任何匹配Pointcut定义的method2都得到了成功拦截。

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

注入的方式可以不一而足。

  • 可以在目标对象中声明一个实例变量作为其代理对象的引用,然后由构造方法注入或者setter方法注入将AopContext.currentProxy()取得的Object注入给这个声明的实例变量。

  • 在目标对象中声明一个getter方法,如getThis(),然后通过Spring的IoC容器的方法注入或者方法替换,将这个方法的逻辑替换为returnAopContext.currentProxy()。这样,在调用自身方法的时候,直接通过getThis().method2();就可以了。

  • 声明一个Wrapper类,并且让目标对象依赖于这个类。在Wrapper类中直接声明一个getProxy()或者类似的方法,将return AopContext.currentProxy()类似逻辑添加到这个方法中,目标对象只要getWrapper().getProxy()就可以取得相应的代理对象。Wrapper类分离了目标对象与SpringAPI的直接耦合。至于让这个Wrapper以Utility类出现,还是在目标对象中直接构造,或者依赖注入到目标对象,这就由你来定了。

  • 为类似的目标对象声明统一的接口定义,然后通过BeanPostProcessor处理这些接口实现类,将实现类的某个取得当前对象的代理对象的方法逻辑覆盖掉。这与方法替换所使用的原理一样,只不过可以借助Spring的loC容器进行批量处理而已。

至于其他更好的方式,还是留待读者自己去考虑吧,我就不嘐嗦了。

实际上,这种情况的出现仅仅是因为SpringAOP采用的是代理机制实现。如果像AspectJ那样,直接将栈切逻辑织入目标对象,那么代理对象和目标对象实际上就合为一体了,调用也不会出现这样的问题.如果你觉得以上处理方式不好接受,那么改用AspectJ也无妨。不过,使用中同样会遇到这样或那样的问题,这简直是一定的。我的建议是用自己感觉合适的产品或者做事方式。

本章小结

在本部分内容结束之前,我们在本章中揭示了SpringAOP实现机制导致的一个小小的陷阱,并通过分析问题产生的原因给出了自己的解决方案。同时,也在某些方面给出了适当的扩展讨论。

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