從同一 Bean 的另一個方法呼叫 Spring @Cacheable
一、簡介
Spring 提供了一種基於註解的方法來在 Spring 管理的 bean 上啟用快取。基於AOP技術,透過在方法上添加@Cacheable
註解,可以輕鬆地使方法可緩存。但是,當從同一個類別中呼叫時,快取將被忽略。
在本教程中,我們將解釋為什麼會發生這種情況以及如何解決它。
2. 重現問題
首先,我們建立一個啟用快取的 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;
}
}
其次,我們在MathService
中建立一個方法sumOfSquareOf2
,它呼叫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,這表示參數為2的方法square
被呼叫了兩次,緩存被忽略。這不是我們的期望,因此我們需要確定此行為的原因。
3. 分析問題
Spring AOP 支援@Cacheable
方法的快取行為。如果我們使用IDE來調試這段程式碼,我們會發現一些線索。 MathServiceIntegrationTest
中的變數mathService
指向MathService$$EnhancerBySpringCGLIB$$5cdf8ec8
的實例,而MathService
中的this
指向MathService
的實例。
MathService$$EnhancerBySpringCGLIB$$5cdf8ec8
是Spring產生的代理類別。它攔截MathService
的@Cacheable
方法上的所有請求,並使用快取的值進行回應。
另一方面, MathService
本身沒有快取的能力,所以同一個類別內internal
呼叫不會得到快取的值。
現在我們了解了其中的機制,讓我們尋找解決這個問題的方法。顯然,最簡單的方法是將@Cacheable
方法移至另一個 bean。但是,如果由於某種原因我們必須將方法保留在同一個 bean 中,我們有三個可能的解決方案:
- 自註射
- 編譯時編織
- 加載時編織
在我們的 AspectJ 簡介文章中,詳細介紹了面向方面程式設計 (AOP) 和不同的編織方法。編織是一種插入程式碼的方法,當我們將原始程式碼編譯成.class
檔案時,就會發生這種情況。它包括AspectJ中的編譯時編織、編譯後編織和加載時編織。由於編譯後編織用於第三方庫的編織,這不是我們的情況,因此我們只關注編譯時編織和加載時編織。
4.方案一:自註入
自註入是繞過 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
註釋有助於建立存根代理並將其註入到self
中。稍後將用相同的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 插件的方面庫。
啟用編譯時 wavring 需要三個步驟。首先,讓我們透過在任何配置類別上新增@EnableCaching
註解來啟用 AspectJ 模式的快取:
@EnableCaching(mode = AdviceMode.ASPECTJ)
其次,我們需要加入spring-aspects
依賴項:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
第三,讓我們為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>
當我們執行mvn clean compile
時,上面顯示的 AspectJ Maven 插件將編織切面。使用編譯時編織,我們不需要更改程式碼, square
方法只會執行一次:
@Test
void givenCacheableMethod_whenInvokingByInternalCall_thenCacheIsTriggered() {
AtomicInteger counter = mathService.resetCounter();
assertThat(mathService.sumOfSquareOf2()).isEqualTo(8);
assertThat(counter.get()).isEqualTo(1);
}
6. 解決方案 3:加載時編織
載入時編織只是二進位編織,延遲到類別載入器載入類別檔案並將類別定義到 JVM 為止。可以使用 AspectJ 代理程式來啟用 AspectJ 載入時編織,以參與類別載入過程並在 VM 中定義任何類型之前編織它們。
啟用載入時編織還需要三個步驟。首先,透過在任何設定類別上新增兩個註解來啟用 AspectJ 模式和載入時編織器的快取:
@EnableCaching(mode = AdviceMode.ASPECTJ)
@EnableLoadTimeWeaving
其次,讓我們加入spring-aspects
依賴項:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
最後,我們為 JVM 指定javaagent
選項-javaagent:path/to/aspectjweaver.jar
或使用 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>
測試givenCacheableMethod_whenInvokingByInternalCall_thenCacheIsTriggered
也將透過載入時編織。
七、結論
在這篇文章中,我們解釋了為什麼緩存在 @Cacheable
方法是從同一個 bean 呼叫的。然後,我們分享了自註射和兩種編織方案來解決這個問題。和往常一樣,本文的原始碼已經可用 在 GitHub 上。在這篇文章中,我們解釋了為什麼緩存在 @Cacheable
方法是從同一個 bean 呼叫的。然後,我們分享了自註射和兩種編織方案來解決這個問題。和往常一樣,本文的原始碼已經可用 在 GitHub 上。