-
同一对象内的嵌套方法调用AOP失效原因分析
同一对象内的嵌套方法调用AOP失效分析
举一个同一对象内的嵌套方法调用拦截失效的例子
首先定义一个目标对象:
/**
* @description: 目标对象与方法
* @create: 2020-12-20 17:10
*/
public class TargetClassDefinition {
public void method1(){
method2();
System.out.println("method1 执行了……");
}
public void method2(){
System.out.println("method2 执行了……");
}
}
在这个类定义中,method1()
方法会调用同一对象上的method2()
方法。
现在,我们使用Spring AOP拦截该类定义的method1()
和method2()
方法,比如一个简单的性能检测逻辑,定义如下Aspect:
/**
* @description: 性能检测Aspect定义
* @create: 2020-12-20 17:13
*/
@Aspect
public class AspectDefinition {
@Pointcut("execution(public void *.method1())")
public void method1(){}
@Pointcut("execution(public void *.method2())")
public void method2(){}
@Pointcut("method1() || method2()")
public void pointcutCombine(){}
@Around("pointcutCombine()")
public Object aroundAdviceDef(ProceedingJoinPoint pjp) throws Throwable{
StopWatch stopWatch = new StopWatch();
try{
stopWatch.start();
return pjp.proceed();
}finally {
stopWatch.stop();
System.out.println("PT in method [" + pjp.getSignature().getName() + "]>>>>>>"+stopWatch.toString());
}
}
}
由AspectDefinition
定义可知,我们的Around Advice会拦截pointcutCombine()
所指定的JoinPoint,即method1()
或method2()
的执行。
接下来将AspectDefinition
中定义的横切逻辑织入TargetClassDefinition
并运行,其代码如下:
/**
* @description: 启动方法
* @create: 2020-12-20 17:23
*/
public class StartUpDefinition {
public static void main(String[] args) {
AspectJProxyFactory weaver = new AspectJProxyFactory(new TargetClassDefinition());
weaver.setProxyTargetClass(true);
weaver.addAspect(AspectDefinition.class);
Object proxy = weaver.getProxy();
((TargetClassDefinition) proxy).method1();
System.out.println("-------------------");
((TargetClassDefinition) proxy).method2();
}
}
执行之后,得到如下结果:
method2 执行了……
method1 执行了……
PT in method [method1]>>>>>>StopWatch '': running time = 20855400 ns; [] took 20855400 ns = 100%
-------------------
method2 执行了……
PT in method [method2]>>>>>>StopWatch '': running time = 71200 ns; [] took 71200 ns = 100%
不难发现,从外部直接调用TargetClassDefinition
的method2()
方法的时候,因为该方法签名匹配AspectDefinition
中的Around Advice所对应的Pointcut定义,所以Around Advice逻辑得以执行,也就是说AspectDefinition
拦截method2()
成功了。但是,当调用method1()
时,只有method1()
方法执行拦截成功,而method1()
方法内部的method2()
方法没有执行却没有被拦截。
原因分析
这种结果的出现,归根结底是Spring AOP的实现机制造成的。众所周知Spring AOP使用代理模式实现AOP,具体的横切逻辑会被添加到动态生成的代理对象中,只要调用的是目标对象的代理对象上的方法,通常就可以保证目标对象上的方法执行可以被拦截。就像TargetClassDefinition
的method2()
方法执行一样。
不过,代理模式的实现机制在处理方法调用的时序方面,会给使用这种机制实现的AOP产品造成一个遗憾,一般的代理对象方法与目标对象方法的调用时序如下所示:
proxy.method2(){
记录方法调用开始时间;
target.method2();
记录方法调用结束时间;
计算消耗的时间并记录到日志;
}
在代理对象方法中,无论如何添加横切逻辑,不管添加多少横切逻辑,最终还是需要调用目标对象上的同一方法来执行最初所定义的方法逻辑。
如果目标对象中原始方法调用依赖于其他对象,我们可以为目标对象注入所需依赖对象的代理,并且可以保证想用的JoinPoint被拦截并织入横切逻辑。而一旦目标对象中的原始方法直接调用自身方法的时候,也就是说依赖于自身定义的其他方法时,就会出现如下图所示问题:
在代理对象的method1()
方法执行经历了层层拦截器后,最终会将调用转向目标对象上的method1()
,之后的调用流程全部都是在TargetClassDefinition
中,当method1()
调用method2()
时,它调用的是TargetObject
上的method2()
而不是ProxyObject
上的method2()
。而针对method2()
的横切逻辑,只织入到了ProxyObject
上的method2()
方法中。所以,在method1()
中调用的method2()
没有能够被拦截成功。
解决方案
当目标对象依赖于其他对象时,我们可以通过为目标对象注入依赖对象的代理对象,来解决相应的拦截问题。
当目标对象依赖于自身时,我们可以尝试将目标对象的代理对象公开给它,只要让目标对象调用自身代理对象上的相应方法,就可以解决内部调用的方法没有被拦截的问题。
Spring AOP提供了AopContext来公开当前目标对象的代理对象,我们只要在目标对象中使用AopContext.currentProxy()
就可以取得当前目标对象所对应的代理对象。重构目标对象,如下所示:
import org.springframework.aop.framework.AopContext;
/**
* @description: 目标对象与方法
* @create: 2020-12-20 17:10
*/
public class TargetClassDefinition {
public void method1(){
((TargetClassDefinition) AopContext.currentProxy()).method2();
// method2();
System.out.println("method1 执行了……");
}
public void method2(){
System.out.println("method2 执行了……");
}
}
要使AopContext.currentProxy()
生效,需要在生成目标对象的代理对象时,将ProxyConfig或者它相应的子类的exposeProxy属性设置为true,如下所示:
/**
* @description: 启动方法
* @create: 2020-12-20 17:23
*/
public class StartUpDefinition {
public static void main(String[] args) {
AspectJProxyFactory weaver = new AspectJProxyFactory(new TargetClassDefinition());
weaver.setProxyTargetClass(true);
weaver.setExposeProxy(true);
weaver.addAspect(AspectDefinition.class);
Object proxy = weaver.getProxy();
((TargetClassDefinition) proxy).method1();
System.out.println("-------------------");
((TargetClassDefinition) proxy).method2();
}
}
<!-- 在XML文件中的开启方式 -->
<aop:aspectj-autoproxy expose-proxy="true" />
再次执行代码,即可实现所需效果:
method2 执行了……
PT in method [method2]>>>>>>StopWatch '': running time = 180400 ns; [] took 180400 ns = 100%
method1 执行了……
PT in method [method1]>>>>>>StopWatch '': running time = 24027700 ns; [] took 24027700 ns = 100%
-------------------
method2 执行了……
PT in method [method2]>>>>>>StopWatch '': running time = 64200 ns; [] took 64200 ns = 100%
后记
虽然通过将目标对象的代理对象赋给目标对象实现了我们的目的,但解决的方式不够雅观,我们的目标对象都直接绑定到了Spring AOP的具体API上了。因此,在开发中应该尽量避免“自调用”的情况。