实习过程中碰到的通过this调用同类方法导致的AOP失效问题

441 阅读4分钟

前言

最近在看公司代码的时候无意间看到一个我不常用的api,是通过AopContext.currentProxy()来获取当前对象的代理对象,再通过代理对象调用一个方法,我在想,为什么同一个类中的方法,不用this.来调用,而是通过这样的方式,肯定是有原因的,我就去搜了一下,说是直接用this.调用的话会使得被调用的方法的AOP增强失效!

接着我就去全面了解了一下,总结总结分享给大家。

通过this关键字调用导致AOP失效的原因

在 Spring 框架中,AOP(面向切面编程)和事务管理是通过动态代理实现的。当你在一个类的内部方法中直接使用 this 调用另一个方法时,Spring 无法通过代理来拦截和应用 AOP 增强(如事务管理、日志记录等)。这导致 AOP 或事务注解失效的原因主要有以下几点:

  1. 动态代理机制(Spring 使用两种主要的代理机制来实现 AOP 和事务管理):

JDK 动态代理:

  • 如果被代理的类实现了接口,Spring 会使用 java.lang.reflect.Proxy 类生成代理对象。
  • 代理对象实现了与目标类相同的接口,并在方法调用前后插入事务管理的逻辑。

CGLIB 代理:

  • 如果被代理的类没有实现接口,Spring 会使用 CGLIB 库生成目标类的子类。
  • 子类重写了目标类的方法,并在方法调用前后插入事务管理的逻辑。

无论哪种代理机制,Spring 都会在运行时生成一个代理对象,这个代理对象会拦截对目标方法的调用,并在调用前后应用 AOP 增强。

2. this 调用或直接调用绕过代理

当你在一个类的内部方法中使用 this 调用另一个方法时,实际上是直接调用了目标方法,而不是通过代理对象调用。因此,Spring 无法拦截这次调用,也就无法应用 AOP 增强。

@Transactional原理是通过动态代理实现的

@Transactional 注解实际上是一个 AOP 切面,Spring 会根据这个注解生成相应的切面逻辑。切面逻辑通常包括:

  • 前置通知:在方法调用前开启事务。
  • 后置通知:在方法调用后提交或回滚事务。

具体流程:

  • 配置解析: Spring 容器启动时,解析带有 @Transactional 注解的类和方法。 将这些类和方法的信息注册到 AOP 切面管理器中。
  • 代理对象生成: 根据配置生成相应的代理对象。 对于实现了接口的类,使用 JDK 动态代理。 对于没有实现接口的类,使用 CGLIB 动态代理。
  • 方法调用: 当外部调用被 @Transactional 注解的方法时,实际上是调用的代理对象的方法。 代理对象在方法调用前后插入事务管理的逻辑。

代码演示

1.使用AopContext:通过 AopContext 获取当前代理对象,然后调用内部方法。

@Service
public class ExamService {

    @Autowired
    private ExamMapper examMapper;

    @Transactional
    @Loggable
    public void createExam(Exam exam) {
        log.info("开始创建考试");
        examMapper.save(exam);
        log.info("考试创建成功");

        // 通过 AopContext 获取当前代理对象
        (ExamService) AopContext.currentProxy().processExam(exam);
    }

    @Transactional
    @Loggable
    public void processExam(Exam exam) {
        log.info("开始处理考试");
        // 模拟一些复杂的业务逻辑
        exam.setProcessed(true);
        examMapper.update(exam);
        log.info("考试处理完成");
    }
}

2.使用代理对象调用:通过获取代理对象来调用内部方法,确保 AOP 切面生效。

@Service
public class ExamService {

    @Autowired
    private ExamMapper examMapper;
    
    @Autowired
    private ExamService self;

    @Transactional
    @Loggable
    public void createExam(Exam exam) {
        log.info("开始创建考试");
        examMapper.save(exam);
        log.info("考试创建成功");

        // 通过代理对象调用内部方法
        self.processExam(exam);
    }

    @Transactional
    @Loggable
    public void processExam(Exam exam) {
        log.info("开始处理考试");
        // 模拟一些复杂的业务逻辑
        exam.setProcessed(true);
        examMapper.update(exam);
        log.info("考试处理完成");
    }
}

需要注意的是:用第二种方式需要注意循环依赖问题。

那什么是循环依赖呢?两个或多个Bean对象相互依赖,形成一个闭环,这种依赖关系会导致Spring容器在初始化的时候出现每个Bean都在等待另一个Bean初始化完成。(有点像死锁)

那么这种通过字段注入的单例Bean的循环依赖问题可以通过Spring的三级缓存来解决,这里又涉及到另一个知识点了,我就不再赘述了,想要了解的可以去网上搜搜看。