知識庫 / Spring / Spring Boot RSS 訂閱

從同一 Bean 的另一個方法中調用 Spring @Cacheable

Spring Boot
HongKong
8
11:26 AM · Dec 06 ,2025

1. 引言

Spring 提供基於註解的方法來啓用對 Spring 管理的 Bean 的緩存。 藉助 AOP 技術,只需在方法上添加註解 @Cacheable,即可輕鬆使方法具有緩存能力。 但是,當方法在同一類中被調用時,緩存將被忽略。

在本教程中,我們將解釋為什麼會發生這種情況以及如何解決它。

2. 還原問題

首先,我們創建一個啓用了緩存的 Spring Boot 應用。 在本文中,我們創建了一個名為 MathService 的服務,其中包含一個帶有 @Cacheable 註解的 square 方法:

@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;
    }
}

第二,我們創建一個名為 sumOfSquareOf2 的方法,位於 MathService 中,該方法調用 square 方法兩次:

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

第三,我們創建一個測試,用於驗證方法 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);
    }

}

由於同一類中對該方法的調用不觸發緩存,計數器的值等於 2,表明 square 方法(帶參數 2)被調用了兩次,且緩存被忽略

3. 分析問題

對於使用 @Cacheable 方法的緩存行為,Spring AOP 提供了支持。我們可以使用 IDE 進行調試來尋找線索。變量 MathServiceIntegrationTest 中的 MathService$$EnhancerBySpringCGLIB$$5cdf8ec8 指向 MathService 的實例,而 thisMathService 中指向 MathService 的實例。

MathService$$EnhancerBySpringCGLIB$$5cdf8ec8 是 Spring 生成的代理類。它攔截了對 MathService@Cacheable 方法的所有請求,並返回緩存值。

另一方面,MathService 本身沒有緩存的能力,因此同一類中內部的調用不會獲取緩存值。

現在我們理解了機制,讓我們尋找解決此問題的方案。 顯然, 最簡單的方法是將 @Cacheable 方法移動到另一個 Bean 中。但是,如果出於某些原因,我們需要將方法保留在同一個 Bean 中,則有三種可能的解決方案:

  • 自注入
  • 編譯時織入
  • 加載時織入

在我們的《AspectJ 入門》文章中介紹了面向切面編程 (AOP) 和 AspectJ 中不同的織入方法。織入是一種將源代碼編譯成 .class 文件時插入代碼的方式。它包括編譯時織入、發佈後織入和加載時織入(在 AspectJ 中)。由於發佈後織入用於織入第三方庫,而我們的情況並非如此,因此我們僅關注編譯時織入和加載時織入。

4. 解決方案 1:自注入 (Self-Injection)

自注入是繞過 Spring AOP 限制的常用解決方案。它允許我們獲取 Spring 增強的 Bean 引用,並通過該 Bean 調用方法。在我們的案例中,我們可以將 mathService Bean 自動注入到一個名為 self 的成員變量中,並通過 self 調用 square 方法,而不是使用 this 引用。

@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);
    }
}

@Scope 註解有助於創建並注入一個樁代理,以解決循環引用問題。它稍後會被填充為相同的 MathService 實例。測試結果表明,square 方法僅執行一次:

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

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

5. 解決方案 2:編譯時編織

編譯時編織中的編織過程,正如其名稱所暗示的,發生在編譯時。 這是最簡單的編織方法。編織最簡單。 當我們既有方面代碼的源文件,又在使用方面代碼時,AspectJ 編譯器將從源文件編譯併產生編織後的類文件作為輸出。
在 Maven 項目中,我們可以使用 Mojo 的 AspectJ Maven 插件來使用 AspectJ 編譯器將 AspectJ 方面編織到我們的類中。 對於 @Cacheable 註解,方面代碼的源文件由 spring-aspects 庫提供,因此我們需要將其作為 Maven 依賴項和 AspectJ Maven 插件的方面庫添加。

啓用編譯時編織有三個步驟。 首先,讓我們通過在任何配置類上添加 @EnableCaching 註解來啓用 AspectJ 模式緩存:

@EnableCaching(mode = AdviceMode.ASPECTJ)
<div>
 <p>其次,我們需要添加 <em >spring-aspects</em> 依賴項:</p>
</div>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>
<p>第三,我們來定義 <em >aspectj-maven-plugin</em> 用於 <em >compile</em> 目標的配置:</p>
<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>

上文所示的 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. 方案 3:加載時織入 (Load-Time Weaving)

加載時織入 (Load-Time Weaving) 簡單來説,是在類加載器加載類文件並將其定義為 JVM 進程時才進行的二進制織入。 使用 AspectJ 代理可以啓用加載時織入,從而參與類加載過程,並在類被定義在 JVM 中之前對其進行織入。

啓用加載時織入需要三個步驟。 首先,啓用 AspectJ 模式和加載時織入器,通過在任何配置類上添加以下兩個註解:

@EnableCaching(mode = AdviceMode.ASPECTJ)
@EnableLoadTimeWeaving
<div>
 <p>第二,讓我們添加 <em >spring-aspects</em> 依賴項:</p>
</div>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>
<p>最後,我們指定 <em >javaagent</em> 選項給 JVM,即使用 <em >-javaagent:path/to/aspectjweaver.jar</em>,或者使用 Maven 插件來配置 <em >javaagent</em>。</p>
<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>

測試 givenCacheableMethod_whenInvokingByInternalCall_thenCacheIsTriggered 也將為按需加載時織入(load-time weaving)而通過。

7. 結論

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.