首页 > 编程语言 > Spring AOP 对象内部方法间的嵌套调用方式
2022
08-11

Spring AOP 对象内部方法间的嵌套调用方式

Spring AOP 对象内部方法间的嵌套调用

前两天面试的时候,面试官问了一个问题,大概意思就是一个类有两个成员方法 A 和 B,两者都加了事务处理注解,定义了事务传播级别为 REQUIRE_NEW,问 A 方法内部直接调用 B 方法时能否触发事务处理机制。

答案有点复杂,Spring 的事务处理其实是通过AOP实现的,而实现AOP的方法有好几种,对于通过 Jdk 和 cglib 实现的 aop 处理,上述问题的答案为否,对于通过AspectJ实现的,上述问题答案为是。

本文就结合具体例子来看一下

我们先定义一个接口

public interface AopActionInf {
    void doSomething_01();
    void doSomething_02();
}

以及此接口的一个实现类

public class AopActionImpl implements AopActionInf{
    public void doSomething_01() {
        System.out.println("AopActionImpl.doSomething_01()");
        //内部调用方法 doSomething_02
        this.doSomething_02();
    }
    public void doSomething_02() {
        System.out.println("AopActionImpl.doSomething_02()");
    }
}

增加AOP处理

public class ActionAspectXML {
    public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println("进入环绕通知");
        Object object = pjp.proceed();//执行该方法
        System.out.println("退出方法");
        return object;
    }
}
<aop:aspectj-autoproxy/>
<bean id="actionImpl" class="com.maowei.learning.aop.AopActionImpl"/>
<bean id="actionAspectXML" class="com.maowei.learning.aop.ActionAspectXML"/>
<aop:config>
    <aop:aspect id = "aspectXML" ref="actionAspectXML">
        <aop:pointcut id="anyMethod" expression="execution(* com.maowei.learning.aop.AopActionImpl.*(..))"/>
        <aop:around method="aroundMethod" pointcut-ref="anyMethod"/>
    </aop:aspect>
</aop:config>

运行结果如下:

这里写图片描述

下图是断点分析在调用方法doSomething_02时的线程栈,很明显在调用doSomething_02时并没有对其进行AOP处理。

默认情况下,Spring AOP使用Jdk的动态代理机制实现,当然也可以通过如下配置更改为cglib实现,但是运行结果相同,此处不再赘述。

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

那有没有办法能够触发AOP处理呢?答案是有的,考虑到AOP是通过动态生成目标对象的代理对象而实现的,那么只要在调用方法时改为调用代理对象的目标方法即可。

我们将调用 doSomething_02 的那行代码改成如下,并修改相应配置信息:

public void doSomething_01() {
    System.out.println("AopActionImpl.doSomething_01()");
    ((AopActionInf) AopContext.currentProxy()).doSomething_02();
}
<aop:aspectj-autoproxy expose-proxy="true"/>

先来看一下运行结果,

这里写图片描述

从运行结果可以看出,嵌套调用方法已经能够实现AOP处理了,同样我们看一下线程调用栈信息,显然 doSomething_02 方法被增强处理了(红框中内容)。

同一对象内的嵌套方法调用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上了。因此,在开发中应该尽量避免“自调用”的情况。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持自学编程网。

编程技巧