Spring AOP 代理类的自调用


#Spring#


当一个切面对一个业务类生效时,我们使用的业务类对象实际上是Spring帮我们生成的一个代理对象,而这个代理的粒度,是类级别的。

自调用,是指一个类的方法调用本类的其他方法。

1、简单的 AOP 使用示例

示例代码中有3个类:

├── Main.java
├── TestAspect.java
└── TestBean.java

TestBean.java

package demo01;

import org.springframework.stereotype.Component;

@Component
public class TestBean {

    public void hello() {
        System.out.println("hello");
    }
}

TestAspect.java

package demo01;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class TestAspect {

    // 定义切点(切入位置)
    @Pointcut("execution(* demo01.TestBean.*(..))")
    private void pointcut(){}


    @Before("pointcut()")
    public void before(JoinPoint joinPoint){
        System.out.println("我是前置通知");
    }

}

Main.java

package demo01;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class Main {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Main.class);
        TestBean testBean = ctx.getBean(TestBean.class);
        testBean.hello();
    }

}

执行结果如下:

我是前置通知
hello

2、代理的粒度是类级别的,所以自调用时不会走切面逻辑

我们对 TestBean 稍作改造,让hello函数:

@Component
public class TestBean {

    public void hello() {
        System.out.println("hello");
        hi();
    }

    public void hi() {
        System.out.println("hi");
    }
}

如果我们调用 hi 函数,会输出:

我是前置通知
hi

如果我们调用 hello 函数,会输出:

我是前置通知
hello
hi

注意,hi之前没有我是前置通知

Spring的事务管理中有个 @Transactional 注解可以方便的管理事务,也是基于 AOP 实现的。该注解在下面的情况下会失效:外部类调用本类的一个没有 @Transactional 注解的函数,该函数调用本类的一个有 @Transactional 注解的函数。失效原因就是因为代理是类级别的。

3、为什么要这样设计?

技术上能实现自调用的时也走切面逻辑,比如 cglib 的 MethodInterceptor (Java:使用 cglib 实现动态代理)。

有些场景自调用走代理更合适,而另外一些场景不走代理更合适。选择类级别的代理是权衡的结果。

我们有办法让自调用走代理吗?有。

4、让自调用走代理的方法1:自装配

将 TestBean 改造为:

@Component
public class TestBean {

    @Autowired
    private TestBean self;  // 这个装配的对象是代理对象

    public void hello() {
        System.out.println("hello");
        self.hi();
    }

    public void hi() {
        System.out.println("hi");
    }
}

如果一个 Bean 没有被代理,也可以这样使用。如果在你的测试中运行失败,请将 Spring 升级到最新版本。

5、让自调用走代理的方法2:启用 exposeProxy

使用 @EnableAspectJAutoProxy 注解时,指定 exposeProxy 为 true。

@EnableAspectJAutoProxy(exposeProxy=true)

业务逻辑中使用AopContext.currentProx获取当前的代理。

比如将 TestBean 改造为:

// letianbiji.com
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Component;

@Component
public class TestBean {

    public void hello() {
        System.out.println("hello");
        TestBean self = (TestBean) AopContext.currentProxy();  // 获取当前代理
        self.hi();
    }

    public void hi() {
        System.out.println("hi");
    }
}

( 本文完 )