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 同类方法调用问题:
- 优先重构代码:将方法拆分到不同类
- 次选自注入代理:使用
@Lazy
解决循环依赖 - 终极方案:改用 AspectJ 实现编译期织入
示例代码已上传至 GitHub,欢迎参考实践。