Invoke Spring @Cacheable from Another Method of Same Bean – 从同一 Bean 的另一个方法中调用 Spring @Cacheable

最后修改: 2023年 9月 21日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.导言

Spring provides an annotation-based approach to enable caching on a Spring-managed bean. Based on the AOP technology, it’s easy to make a method cacheable by adding the annotation @Cacheable on it. However, the cache will be ignored when called from within the same class.

Spring 提供了一种基于注解的方法,用于在 Spring 管理的 Bean 上启用缓存。基于 AOP 技术,只需在方法上添加注解 @Cacheable 即可轻松实现方法的缓存。但是,在同一个类中调用时,缓存将被忽略。

In this tutorial, we’ll explain why it happens and how to solve it.

在本教程中,我们将解释出现这种情况的原因和解决方法。

2. Reproducing the Problem

2.重现问题

First, we create a Spring Boot application with cache enabled. In this article, we created a MathService with a @Cacheable annotated square method:

首先,我们创建一个启用了缓存的Spring Boot 应用程序。在本文中,我们创建了一个带有 @Cacheable 注解 square 方法的 MathService

@Service
@CacheConfig(cacheNames = "square")
public class MathService {
    private final AtomicInteger counter = new AtomicInteger();

    @CacheEvict(allEntries = true)
    public AtomicInteger resetCounter() {
        counter.set(0);
        return counter;
    }

    @Cacheable(key = "#n")
    public double square(double n) {
        counter.incrementAndGet();
        return n * n;
    }
}

Second, we create a method sumOfSquareOf2 in MathService that invokes the square method twice:

其次,我们在 MathService 中创建了一个方法 sumOfSquareOf2,该方法会调用 square 方法两次:

public double sumOfSquareOf2() {
    return this.square(2) + this.square(2);
}

Third, we create a test for the method sumOfSquareOf2 to check how many times the square method is invoked:

第三,我们为方法 sumOfSquareOf2 创建一个测试,以检查 square 方法被调用的次数:

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

    @Resource
    private MathService mathService;

    @Test
    void givenCacheableMethod_whenInvokingByInternalCall_thenCacheIsNotTriggered() {
        AtomicInteger counter = mathService.resetCounter();

        assertThat(mathService.sumOfSquareOf2()).isEqualTo(8);
        assertThat(counter.get()).isEqualTo(2);
    }

}

Since the invocation from the same class doesn’t trigger the cache, the number of the counter is equal to 2, which indicates the method square with argument 2 is called twice and the cache is ignored. It isn’t our expectation, so we need to determine the reason for this behavior.

由于来自同一类的调用不会触发缓存,因此计数器的数字等于 2,这表明参数为 2 的方法 square 被调用了两次,缓存被忽略。这与我们的预期不符,因此我们需要确定这种行为的原因。

3. Analyzing the Problem

3.分析问题

The caching behavior for the @Cacheable method is supported by Spring AOP. We’ll find some clues if we use an IDE to debug this code. The variable mathService in MathServiceIntegrationTest points to an instance of MathService$$EnhancerBySpringCGLIB$$5cdf8ec8, whereas this in MathService points to an instance of MathService.

Spring AOP 支持 @Cacheable 方法的缓存行为。如果我们使用集成开发环境来调试这段代码,就会发现一些线索。MathServiceIntegrationTest 中的变量 mathService 指向 MathService$$EnhancerBySpringCGLIB$$5cdf8ec8 的实例,而 MathService 中的 this 指向 MathService 的实例。

MathService$$EnhancerBySpringCGLIB$$5cdf8ec8 is a proxy class generated by Spring. It intercepts all requests on the @Cacheable method for MathService and responds with the cached value.

MathService$$EnhancerBySpringCGLIB$$5cdf8ec8 是 Spring 生成的代理类。它拦截对 MathService@Cacheable 方法的所有请求,并以缓存值作为响应。

On the other hand, MathService itself doesn’t have the ability to cache, so internal calls within the same class won’t get the cached value.

另一方面,MathService 本身不具备缓存功能,因此同一类中的内部调用不会获得缓存值

Now that we understand the mechanism, let’s look for the solutions to this problem. Apparently, the simplest way is to move the @Cacheable method to another bean. But, if we have to keep the methods in the same bean for some reason, we have three possible solutions:

既然我们已经了解了这一机制,那么就让我们来寻找解决这一问题的方法吧。Apparently, 最简单的方法是将 @Cacheable 方法移动到另一个 Bean 中。但是,如果我们出于某种原因必须将这些方法保留在同一个 Bean 中,我们有三种可能的解决方案:

  • Self-injection
  • Compile-time weaving
  • Load-time weaving

In our Intro to  AspectJ article introduces aspect-oriented programming (AOP) and different weaving approaches in detail. Weaving is a way to insert the code that will occur when we compile the source code into .class files. It includes compile-time weaving, post-compile weaving, and load-time weaving in AspectJ. Since post-compile weaving is used to weave for third-party libraries, which isn’t our case, we just focus on compile-time weaving and load-time weaving.

我们的AspectJ入门文章详细介绍了面向方面编程(AOP)和不同的编织方法。编织是将源代码编译到 .class 文件时出现的代码插入其中的一种方法。在 AspectJ 中,它包括编译时编织、编译后编织和加载时编织。由于编译后织入用于为第三方库织入代码,而我们的情况并非如此,因此我们只关注编译时织入和加载时织入。

4. Solution 1: Self-Injection

4.解决方案 1:自我注射

Self-injection is a commonly used solution for bypassing Spring AOP limitations. It allows us to get a reference to the Spring-enhanced bean and call the method through that bean. In our case, we can autowire the mathService bean to a member variable called self, and call the square method by self instead of using the this reference:

Self-injection 是绕过 Spring AOP 限制的常用解决方案。它允许我们获取 Spring 增强 Bean 的引用,并通过该 Bean 调用方法。在我们的示例中,我们可以自动引用 mathService Bean 到名为 self 的成员变量,并通过 self 而不是使用 this 引用来调用 square 方法:

@Service
@CacheConfig(cacheNames = "square")
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MathService {

    @Autowired
    private MathService self;

    // other code

    public double sumOfSquareOf3() {
        return self.square(3) + self.square(3);
    }
}

The @Scope annotation helps create and inject a stub proxy to self due to the circular reference. It will later be filled with the same MathService instance. The test shows that the square method is only executed once:

由于存在循环引用,@Scope 注解有助于创建和注入指向 self 的存根代理。稍后,它将被填充为相同的 MathService 实例。测试表明,square 方法只执行一次:

@Test
void givenCacheableMethod_whenInvokingByExternalCall_thenCacheIsTriggered() {
    AtomicInteger counter = mathService.resetCounter();

    assertThat(mathService.sumOfSquareOf3()).isEqualTo(18);
    assertThat(counter.get()).isEqualTo(1);
}

5. Solution 2: Compile-Time Weaving

5.解决方案 2:编译时编织

The weaving process in compile-time weaving, as the name suggests, happens at compile-time. It’s the simplest approach to weaving. When we have both the source code of the aspect and the code that we’re using aspects in, the AspectJ compiler will compile from the source and produce a woven class file as output.
In a Maven project, we can use Mojo’s AspectJ Maven Plugin to weave AspectJ aspects into our classes using the AspectJ compiler. For the @Cacheable annotation, the source code of the aspect is provided by the library spring-aspects, so we need to add it as the Maven dependency and the aspect library for the AspectJ Maven Plugin.

There are three steps to enable compile-time wavring. First, let’s enable caching with AspectJ mode by adding @EnableCaching annotation on any configuration class:

启用编译时缓存有三个步骤。首先,在任何配置类上添加 @EnableCaching 注解,从而启用 AspectJ 模式的缓存功能:

@EnableCaching(mode = AdviceMode.ASPECTJ)

Second, we need to add the spring-aspects dependency:

其次,我们需要添加 spring-aspects 依赖关系:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>

Third, let’s define the aspectj-maven-plugin for the compile goal:

第三,让我们为 compile 目标定义 aspectj-maven-plugin

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>${aspectj-plugin.version}</version>
    <configuration>
        <source>${java.version}</source>
        <target>${java.version}</target>
        <complianceLevel>${java.version}</complianceLevel>
        <Xlint>ignore</Xlint>
        <encoding>UTF-8</encoding>
        <aspectLibraries>
            <aspectLibrary>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aspects</artifactId>
            </aspectLibrary>
        </aspectLibraries>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

The AspectJ Maven plugin shown above will weave the aspects when we execute mvn clean compile. With compile-time weaving, we don’t need to change the code, and the square method would be executed only once:

上图所示的 AspectJ Maven 插件将在我们执行 mvn clean compile 时编织各个方面。有了编译时编织,我们就不需要修改代码,square 方法也只需执行一次:

@Test
void givenCacheableMethod_whenInvokingByInternalCall_thenCacheIsTriggered() {
    AtomicInteger counter = mathService.resetCounter();

    assertThat(mathService.sumOfSquareOf2()).isEqualTo(8);
    assertThat(counter.get()).isEqualTo(1);
}

6. Solution 3: Load-Time Weaving

6.解决方案 3:负载时间编织

Load-time weaving is simply binary weaving deferred until the point that a classloader loads a class file and defines the class to the JVM. AspectJ load-time weaving can be enabled using an AspectJ agent to get involved in the class-loading process and weave any types before they’re defined in the VM.

加载时织入(Load-time weaving)是一种简单的二进制织入,它推迟到类加载器加载类文件并将类定义到 JVM 时才进行织入。可以使用 AspectJ 代理启用 AspectJ 加载时编织功能,让它参与类加载过程,并在虚拟机定义任何类型之前对其进行编织。

There are also three steps to enable load-time weaving. First, enable caching with AspectJ mode and load-time weaver by adding two annotations on any configuration class:

启用加载时编织也有三个步骤。首先,通过在任何配置类上添加两个注解,使用 AspectJ 模式和加载时编织器启用缓存:

@EnableCaching(mode = AdviceMode.ASPECTJ)
@EnableLoadTimeWeaving

Second, let’s add the spring-aspects dependency:

其次,让我们添加 spring-aspects 依赖关系:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>

Finally, we specify the javaagent option to the JVM -javaagent:path/to/aspectjweaver.jar or use the Maven plugin to configure the javaagent:

最后,我们在 JVM -javaagent:path/to/aspectjweaver.jar 中指定 javaagent 选项,或使用 Maven 插件配置 javaagent

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>${maven-surefire-plugin.version}</version>
            <configuration>
                <argLine>
                    --add-opens java.base/java.lang=ALL-UNNAMED
                    --add-opens java.base/java.util=ALL-UNNAMED
                    -javaagent:"${settings.localRepository}"/org/aspectj/aspectjweaver/${aspectjweaver.version}/aspectjweaver-${aspectjweaver.version}.jar
                    -javaagent:"${settings.localRepository}"/org/springframework/spring-instrument/${spring.version}/spring-instrument-${spring.version}.jar
                </argLine>
                <useSystemClassLoader>true</useSystemClassLoader>
                <forkMode>always</forkMode>
                <includes>
                    <include>com.baeldung.selfinvocation.LoadTimeWeavingIntegrationTest</include>
                </includes>
            </configuration>
        </plugin>
    </plugins>
</build>

The test givenCacheableMethod_whenInvokingByInternalCall_thenCacheIsTriggered will also pass for load-time weaving.

测试 givenCacheableMethod_whenInvokingByInternalCall_thenCacheIsTriggered 也将通过加载时编织。

7. Conclusion

7.结论

In this article, we explained why cache doesn’t take effect when the @Cacheable method is invoked from the same bean. Then, we shared self-injection and two weaving solutions to solve this problem. As usual, the source code for this article is available over on GitHub.