这是我参与更文挑战的第19天,活动详情查看:更文挑战
这个话题应该同一个对象内的嵌套方法调用拦截失效说起。
假设我们有如下所示的目标对象类定义。当然,这样的类定义可以映射到系统中的任何可能的业务对象。
public class NestableInvocationBO {
public void method1() {
method2();
System.out.println("method1 executed!");
}
public void method2() {
System.out.println("method2 executed.");
}
}
复制代码
该类定义中需要我们关注的是它的某个给方法会调用同一对象上定义的其他方法。这通常是比较常见的。在NestableInvocationBO类中,method1方法调用了同一对象的method2方法。
现在,我们要使用Spring AOP拦截该类定义的method1和method2方法,比如加入一个简单的性能检测逻辑。那么可以定义一个PerformanceTraceAspect,代码如下:
@Aspect
public class PerformanceTraceAspect {
private final Log logger = LogFactory = LogFactory.getLog(PerformanceTraceAspect.class);
@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();
if (logger.isInfoEnabled()) {
logger.info("PT in method[" +joinpoint.getSignature().getName()+ "]>>"+watch.toString());
}
}
}
}
复制代码
Around Advice定义会拦截compositePointcut()所指定的JoinPoint,即method1或者method2的执行。
如果将PerformanceTraceAspect中定义的横切逻辑织入NestableInvocationBO,然后运行如下代码并查看结果:
AspectJProxyFactory weaver = new AspectJProxyFactory(new NestableInvocationBO());
weaver.setProxyTargetClass(true);
weaver.addAspect(PerformanceTraceAspect.class);
Object proxy = weaver.getProxy();
(NestableInvocationBO) proxy.method2();
(NestableInvocationBO) proxy.method1();
复制代码
会得到如下的输出结果:
method2 executed!
701 [main] INFO ...PerformanceTraceAspect - PT in method[method2]>>0:00:00.000
method2 executed!
method1 executed!
701 [main] INFO ...PerformanceTraceAspect - PT in method[method1]>>0:00:00.000
复制代码
发现问题了没有?当我们从外部直接调用NestableInvocationBO对象的method2的时候,因为该方法签名匹配PerformanceTraceAspect中Around Advice所对应的Pointcut定义,所以,Around Advice逻辑得以执行,也就是说,PerformanceTraceAspect拦截method2的执行成功了。但是,当调用method1的时候,却只有method1方法的执行拦截成功,而method1方法内部的method2方法执行却没有被拦截,因为在输入日志中只有PT in method[method1]的信息。
原因分析
这种结果的出现,归根结底是由Spring AOP的实现机制造成的。我们知道,Spring AOP采用代理模式实现AOP,具体的横切逻辑会被添加到动态生成的代理对象中,只要调用的是目标对象的代理对象上的方法,通常就可以保证目标对象上的方法执行可以被拦截。就像NestableInvocationBO的method2方法执行一样,当我们调用代理对象上的method2的时候,目标对象的method2就会被成功拦截。
不过,代理模式的实现机制在处理方法调用的时许方面,会给使用这种机制实现的AOP产品造成一个小小的“缺憾”。我们来看一般的代理对象方法与目标对象方法的调用时序,如下所示:
proxy.method2 {
记录方法调用开始时间;
target.method2();
记录方法调用结束时间;
计算消耗的时间并记录到日志;
}
复制代码
在代理对象方法中,不管你如何添加横切逻辑,也不管你添加多少横切逻辑,有一点是确定的,那就是,你终归需要调用目标对象上的同一方法来执行最初所定义的方法逻辑。
如果目标对象中原始方法调用依赖于其他对象,那没问题,我们可以为目标对象注入所依赖对象的代理,并且可以保证相应Joinpoint被拦截注入横切逻辑。而一旦目标对象中的原始方法调用直接调用自身的方法的时候,也就是说,它所依赖于自身所定义的其他的方法的时候,那问题就来了,如下图所示:
在代理对象的method1方法执行经历了层层拦截器之后,最终会将调用转向目标对象上的method1,之后的调用流程全部都是走在TargetObject之上,当method1调用method2时,它调用的是TargetObject上的method2,而不是ProxyObject上的method2。要知道,针对method2的横切逻辑,只是织入到了ProxyObject上的method2方法中,所以,在method1中所调用的method2没有能够被成功拦截。
解决方法
知道了原因,我们就可以“对症下药”了。
当目标对象依赖于其他对象时,我们可以通过为目标对象注入依赖对象的代理,来解决相应的拦截问题。那么,当目标对象依赖于自身时,我们也可以尝试将目标对象的代理对象公开给它,只要让目标对象调用自身代理对象上的方法,就可以解决内部调用的方法没有被拦截的问题。
Spring AOP提供了AopContext来公开当前目标对象的代理对象,我们只要在目标对象中使用AopContext.currentProxy()就可以取得当前目标对象所对应的代理对象。现在,我们重构目标对象,让它直接调用它的代理对象的相应的方法:
public class NestableInvocationBO {
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();
复制代码
现在,我们得到了想要的拦截结果
method2 executed!
611 [main] INFO ...PerformanceTraceAspect - PT in method[method2]>>0:00:00.000
method2 executed!
611 [main] INFO ...PerformanceTraceAspect - PT in method[method2]>>0:00:00.000
method1 executed!
611 [main] INFO ...PerformanceTraceAspect - PT in method[method1]>>0:00:00.000
复制代码
问题是解决了,但解决的不是很雅观,因为我们的目标对象都直接绑定到了Spring AOP的具体API上了。所以,我们考虑重构目标对象定义。我们可以在目标对象中声明对其代理对象的依赖,而由IoC容器来帮助我们注入这个代理对象。
注意
实际上,这种情况的出现仅仅是因为Spring AOP实现机制导致的一个小小的陷进。如果像AspectJ那样,直接将横切逻辑织入目标对象,那么代理对象和目标对象实际上就合为一体了,调用也不会出现这样的问题。