掘金 后端 ( ) • 2024-04-22 11:10

背景

最近在项目开发中使用了 Spring AOP 做方法增强,结果发现在同一个类里,一个方法去调用另一个已经被AOP增强的方法,竟然未能触发预期的切面逻辑,类似这样:

@Service
public class PaymentService {

    public void processPayment() { 
        validatePayment();  //自调用方法,不触发切面
        // 其他处理逻辑...
    }

    public void validatePayment() {
        // 校验逻辑...
    }
}
@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.myapp.service.PaymentService.*(..))")
    public void logMethodStart(JoinPoint joinPoint) {
        // 日志逻辑...
    }
}

为什么同类方法自调用会让AOP切面失效?

Spring AOP 的工作方式基于代理模式。当 Spring 容器创建一个被 AOP 切面包装的 Bean 时,它实际上是在该 Bean 周围创建了一个代理(Proxy)对象。这个代理对象是实际暴露给其他 Bean 的对象。当通过代理调用目标 Bean 的方法时,代理将执行与这个方法调用相关联的所有切面逻辑以及实际的目标方法。

如果目标对象内部的一个方法直接调用同一个对象的另一个方法(即自调用),那么这个调用不会经过代理,而是直接在实际的对象上执行。因为代理是绕过不执行的,因此与第二个方法关联的任何切面逻辑也会被绕过。这就导致了如果你在同一个Bean内部进行方法调用,这些调用就不会触发Spring AOP代理的该方法上的任何增强(比如事务控制、日志记录、安全检查等)。

简而言之,AOP切面失效的核心问题是,自调用没有经过Spring创建的代理实例,切面增强通常绑定在代理实例上;没有经过代理实例就意味着绕过了AOP增强的执行。

解决方法

经过一番研究和探索后,发现这是 Spring AOP 的工作机制所导致的一种特殊行为。在 Spring AOP 的实现中,AOP 代理负责拦截对增强方法的调用。然而,当一个方法内部直接调用同类的另一个方法时,这个调用其实是绕过了 AOP 代理的,因为它是通过 this 关键字进行的直接内部调用。因此,尽管从表面上看,这个被调用的方法已经被AOP增强,但实际上,由于调用方式的问题,切面的逻辑并没有被执行。

解决这个问题也很简单,从 Spring 容器中获取当前类的代理对象,再通过代理对象调用即可。

1. 自我注入

在Spring中,可以通过将当前类注入到自身来解决这个问题。这听起来可能违反直觉,但确实有效。

@Service
public class PaymentService {

    @Autowired
    private PaymentService paymentService;

    public void processPayment() {
        paymentService.validatePayment(); // 使用自注入代理调用
        // 其他处理逻辑...
    }

    public void validatePayment() {
        // 校验逻辑...
    }
}

自我注入可能会引入循环依赖的问题。举个例子,如果一个Bean在构造函数、setter方法或者其他初始化方法里直接或间接地引用了自身,那么这可能导致循环依赖,从而影响应用的启动。Spring框架确实提供了一些机制来解决构造函数注入时的循环依赖,比如通过设置字段或setter方法进行注入,但是这并不意味着所有循环依赖的场景都能得到解决。如果你的应用中出现了循环依赖的错误,需要重新评估你的设计,看是否有更好的方式来组织你的代码。

2. 通过ApplicationContext获取代理对象

直接通过 Spring 的 ApplicationContext 来获取当前 Bean 的代理对象,然后通过这个代理对象调用方法,以确保切面的逻辑被执行。由于是在方法的执行过程中获取代理对象,这种方式通常不会造成启动时的循环依赖问题。

@Service
public class PaymentService {

    @Autowired
    private ApplicationContext context;

    public void processPayment() {
        PaymentService paymentService = context.getBean(PaymentService.class);
        paymentService.validatePayment();
        // 其他处理逻辑...
    }

    public void validatePayment() {
        // 校验逻辑...
    }
}

Spring 基于 AOP 实现的常用功能

1. @Transactional事务管理失效

由于 @Transactional 注解也是通过 Spring AOP 来实现事务管理的增强的,所以为了防止同类方法自调用导致事务管理失效,也需要采用上述的策略来确保同类中方法间的调用能够正确通过 Spring 的代理机制。

2. @Retryable 重试逻辑失效

通过 Spring Retry 库提供的 @Retryable 注解可以实现方法调用的自动重试逻辑,它也是基于 AOP 实现的,也需要注意方法子调用问题。