在同类中直接调用 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