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 的實例,而 this 在 MathService 中指向 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:編譯時編織
啓用編譯時編織有三個步驟。 首先,讓我們通過在任何配置類上添加 @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)而通過。