在同类中直接调用 Spring Aop 代理方法
1、概览
在使用 Spring AOP 时有许多错综复杂的问题。其中一个常见问题是如何处理同一类中的方法调用,因为这种直接调用会使 AOP 功能失效。
本文将带你了解 Spring AOP 的工作原理以及如何解决同类中 AOP 方法直接调用导致的 AOP 功能失效的问题。
2、代理 和 Spring AOP
代理对象可以看作是一个 “包装”,它可以在调用目标对象时添加功能。
在 Spring 中,当一个 Bean 需要额外功能时,就会创建一个代理,并将代理注入到其他 Bean 中。例如,如果某个方法具有事务(Transactional
)注解或任何缓存注解,那么该代理就会被用来在调用目标对象时添加所需的功能。
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;
}
}
当 Spring 创建 AddComponent
Bean 时,也会创建一个代理对象,以添加用于缓存的 AOP 功能。当另一个对象需要 AddComponent
Bean 时,Spring 会提供代理对象用于注入。
测试如下,从一个的组件中多次调用 addOne()
方法,使用相同的参数,并验证 counter
是否只递增一次:
@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);
}
}
现在,为 AddComponent
添加另一个内部调用 addOne()
的方法:
public int addOneAndDouble(int n) {
return this.addOne(n) + this.addOne(n);
}
当这个新方法调用 addOne()
时,调用不通过代理,counter
会递增两次:
@Test
void whenInternalCall_thenCacheNotHit() {
addComponent.resetCache();
addComponent.addOneAndDouble(0);
assertThat(addComponent.getCounter()).isEqualTo(2);
}
4、解决方法
虽然同一类中的直接方法调用会导致 AOP 功能失效,但有几种解决方法。
最好的方法之一就是 重构。在上面的 AddComponent
示例中,与其直接在类中添加 addOneAndDouble()
,不如创建一个包含该方法的新类。新类可以注入 AddComponent
,或者更准确地说,注入 AddComponent
的代理:
@Component
public class AddOneAndDoubleComponent {
@Resource
private AddComponent addComponent;
public int addOneAndDouble(int n) {
return addComponent.addOne(n) + addComponent.addOne(n);
}
}
和之前的测试一样,这只会使 counter
递增一次。
如果无法重构,可以尝试将代理直接注入类中。但是要小心,因为这会创建一个 直接的循环依赖关系,而 Spring 默认情况下不再允许这种关系。不过,Spring 中也有许多允许循环依赖的解决方案,例如,使用 @Lazy
来注解自依赖:
@Component
@CacheConfig(cacheNames = "selfInjectionAddOne")
public class SelfInjection {
// 使用 Lazy 注解,自注入
@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;
}
}
注入代理后,addOneAndDouble()
调用就会使用缓存功能,counter
只递增一次:
@Test
void whenCallingFromExternalClass_thenAopProxyIsUsed() {
selfInjection.resetCache();
selfInjection.addOneAndDouble(0);
assertThat(selfInjection.getCounter()).isEqualTo(1);
}
Spring AOP 创建代理的用法是动态织入的一个例子。另而 AspectJ 则利用了其他几种织入类型,因此不需要重构和自注入。
5、总结
本文介绍了实现在同一类中 Spring AOP 方法调用的几种方式,重构通常是首选的解决方案,但如果无法重构,则可以使用 AspectJ 或自注入来实现所需的功能。
Ref:https://www.baeldung.com/spring-aop-method-call-within-same-class