1. 概述

使用 Spring AOP 时存在不少细节问题,其中最常见的就是同类内部方法调用导致 AOP 失效。本文将深入分析 Spring AOP 的工作原理,并提供几种实用的解决方案。

2. 代理对象与 Spring AOP

首先需要理解代理对象(Proxy)的核心概念。简单来说:

代理对象就像一个包装器,能在目标对象方法调用前后添加额外功能

在 Spring 框架中,当 Bean 需要增强功能时(如事务管理、缓存等),Spring 会创建代理对象。其他组件注入的实际上是这个代理对象而非原始对象,从而实现 AOP 功能。

3. AOP:内部调用与外部调用的差异

Spring 创建的代理对象负责实现 AOP 功能,但同类内部方法调用会绕过代理。我们通过一个缓存示例来说明:

@Component
@CacheConfig(cacheNames = "addOne")
public class AddComponent {

    private int counter = 0;

    @Cacheable
    public int addOne(int n) {
        counter++;
        return n + 1;
    }

    @CacheEvict
    public void resetCache() {
        counter = 0;
    }
}

当外部调用 addOne() 时,代理对象会拦截请求并应用缓存功能。测试验证:

@SpringBootTest(classes = Application.class)
class AddComponentUnitTest {

    @Resource
    private AddComponent addComponent;

    @Test
    void whenExternalCall_thenCacheHit() {
        addComponent.resetCache();

        addComponent.addOne(0);
        addComponent.addOne(0);

        assertThat(addComponent.getCounter()).isEqualTo(1);
    }
}

现在添加一个内部调用方法:

public int addOneAndDouble(int n) {
    return this.addOne(n) + this.addOne(n);
}

关键问题:内部调用直接访问原始对象,绕过了代理,导致缓存失效:

@Test
void whenInternalCall_thenCacheNotHit() {
    addComponent.resetCache();

    addComponent.addOneAndDouble(0);

    assertThat(addComponent.getCounter()).isEqualTo(2);
}

4. 解决方案

针对同类内部调用问题,有几种实用方案:

4.1 重构代码(推荐)

将内部调用方法提取到独立类中,通过依赖注入获取代理对象:

@Component
public class AddOneAndDoubleComponent {

    @Resource
    private AddComponent addComponent;

    public int addOneAndDouble(int n) {
        return addComponent.addOne(n) + addComponent.addOne(n);
    }
}

优点:代码更清晰,符合单一职责原则,测试验证缓存生效。

4.2 自注入代理对象

当无法重构时,可通过 @Lazy 注解解决循环依赖问题:

@Component
@CacheConfig(cacheNames = "selfInjectionAddOne")
public class SelfInjection {

    @Lazy
    @Resource
    private SelfInjection selfInjection;

    private int counter = 0;

    @Cacheable
    public int addOne(int n) {
        counter++;
        return n + 1;
    }

    public int addOneAndDouble(int n) {
        return selfInjection.addOne(n) + selfInjection.addOne(n);
    }

    @CacheEvict(allEntries = true)
    public void resetCache() {
        counter = 0;
    }
}

⚠️ 注意:需启用循环依赖(Spring Boot 默认已启用),测试验证:

@Test
void whenCallingFromExternalClass_thenAopProxyIsUsed() {
    selfInjection.resetCache();

    selfInjection.addOneAndDouble(0);

    assertThat(selfInjection.getCounter()).isEqualTo(1);
}

4.3 使用 AspectJ(终极方案)

Spring AOP 基于运行时代理(Runtime Weaving),而 AspectJ 支持编译期织入(Compile-time Weaving),能彻底解决内部调用问题。但需要额外配置和编译器支持。

5. 总结

处理 Spring AOP 同类方法调用问题:

  1. 优先重构代码:将方法拆分到不同类
  2. 次选自注入代理:使用 @Lazy 解决循环依赖
  3. 终极方案:改用 AspectJ 实现编译期织入

示例代码已上传至 GitHub,欢迎参考实践。


原始标题:Spring AOP for a Method Call Within the Same Class | Baeldung