宣告式基於註解的快取
對於快取宣告,Spring 的快取抽象提供了一組 Java 註解
-
@Cacheable:觸發快取填充。 -
@CacheEvict:觸發快取清除。 -
@CachePut:更新快取而不干擾方法執行。 -
@Caching:將多個快取操作重新組合以應用於一個方法。 -
@CacheConfig:在類級別共享一些常見的快取相關設定。
@Cacheable 註解
顧名思義,您可以使用 @Cacheable 來標記可快取的方法——即其結果儲存在快取中的方法,以便在後續呼叫(使用相同引數)時,直接返回快取中的值,而無需實際呼叫方法。最簡單的形式,註解宣告需要與註解方法關聯的快取名稱,如下例所示
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
在前面的程式碼片段中,findBook 方法與名為 books 的快取相關聯。每次呼叫該方法時,都會檢查快取以檢視該呼叫是否已執行並且無需重複。雖然在大多數情況下,只宣告一個快取,但註解允許指定多個名稱,以便使用多個快取。在這種情況下,在呼叫方法之前會檢查每個快取——如果至少有一個快取命中,則返回關聯的值。
| 所有其他不包含該值的快取也會更新,即使快取方法實際上並未呼叫。 |
以下示例在 findBook 方法上使用 @Cacheable 並使用多個快取
@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}
預設鍵生成
由於快取本質上是鍵值儲存,因此每次呼叫快取方法都需要轉換為適合快取訪問的鍵。快取抽象使用一個簡單的 KeyGenerator,基於以下演算法
-
如果沒有給定引數,則返回
SimpleKey.EMPTY。 -
如果只給定一個引數,則返回該例項。
-
如果給定多個引數,則返回包含所有引數的
SimpleKey。
只要引數具有自然鍵並實現有效的 hashCode() 和 equals() 方法,此方法適用於大多數用例。如果不是這種情況,您需要更改策略。
要提供不同的預設鍵生成器,您需要實現 org.springframework.cache.interceptor.KeyGenerator 介面。
|
預設的鍵生成策略隨著 Spring 4.0 的釋出而改變。早期版本的 Spring 使用的鍵生成策略對於多個鍵引數只考慮引數的 如果您想繼續使用以前的鍵策略,可以配置已棄用的 |
自定義鍵生成宣告
由於快取是通用的,目標方法很可能具有各種簽名,這些簽名無法輕易對映到快取結構之上。當目標方法有多個引數,其中只有一些適合快取(而其餘引數僅由方法邏輯使用)時,這一點往往變得顯而易見。考慮以下示例
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
乍一看,雖然兩個 boolean 引數影響了查詢書籍的方式,但它們對快取毫無用處。此外,如果兩個引數中只有一個重要而另一個不重要呢?
對於這種情況,@Cacheable 註解允許您透過其 key 屬性指定鍵的生成方式。您可以使用 SpEL 選擇感興趣的引數(或其巢狀屬性),執行操作,甚至呼叫任意方法,而無需編寫任何程式碼或實現任何介面。這是推薦的方法,而不是 預設生成器,因為隨著程式碼庫的增長,方法的簽名往往差異很大。雖然預設策略可能適用於某些方法,但很少適用於所有方法。
以下示例使用各種 SpEL 宣告(如果您不熟悉 SpEL,請務必閱讀 Spring 表示式語言)
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
前面的程式碼片段展示了選擇某個引數、其屬性之一或甚至任意(靜態)方法是多麼容易。
如果負責生成鍵的演算法過於具體或需要共享,則可以在操作上定義自定義 keyGenerator。為此,請指定要使用的 KeyGenerator bean 實現的名稱,如下例所示
@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
key 和 keyGenerator 引數是互斥的,指定兩者的操作會導致異常。 |
預設快取解析
快取抽象使用一個簡單的 CacheResolver,它透過使用已配置的 CacheManager 檢索在操作級別定義的快取。
要提供不同的預設快取解析器,您需要實現 org.springframework.cache.interceptor.CacheResolver 介面。
自定義快取解析
預設快取解析非常適合使用單個 CacheManager 且沒有複雜快取解析要求的應用程式。
對於使用多個快取管理器的應用程式,您可以為每個操作設定要使用的 cacheManager,如下例所示
@Cacheable(cacheNames="books", cacheManager="anotherCacheManager") (1)
public Book findBook(ISBN isbn) {...}
| 1 | 指定 anotherCacheManager。 |
您還可以完全替換 CacheResolver,其方式類似於替換 鍵生成。每次快取操作都會請求解析,從而允許實現根據執行時引數實際解析要使用的快取。以下示例展示瞭如何指定 CacheResolver
@Cacheable(cacheResolver="runtimeCacheResolver") (1)
public Book findBook(ISBN isbn) {...}
| 1 | 指定 CacheResolver。 |
|
自 Spring 4.1 起,快取註解的 類似於 |
同步快取
在多執行緒環境中,某些操作可能會為相同的引數併發呼叫(通常在啟動時)。預設情況下,快取抽象不鎖定任何內容,並且相同的值可能會計算多次,從而失去了快取的目的。
對於這些特殊情況,您可以使用 sync 屬性指示底層快取提供程式在計算值時鎖定快取條目。因此,只有一個執行緒忙於計算值,而其他執行緒則被阻塞,直到快取中的條目更新。以下示例展示瞭如何使用 sync 屬性
@Cacheable(cacheNames="foos", sync=true) (1)
public Foo executeExpensiveOperation(String id) {...}
| 1 | 使用 sync 屬性。 |
這是一項可選功能,您最喜歡的快取庫可能不支援它。核心框架提供的所有 CacheManager 實現都支援它。有關更多詳細資訊,請參閱您的快取提供程式的文件。 |
使用 CompletableFuture 和響應式返回型別進行快取
從 6.1 開始,快取註解會考慮 CompletableFuture 和響應式返回型別,並相應地自動調整快取互動。
對於返回 CompletableFuture 的方法,該 future 生成的物件將在完成時快取,並且快取命中的查詢將透過 CompletableFuture 檢索
@Cacheable("books")
public CompletableFuture<Book> findBook(ISBN isbn) {...}
對於返回 Reactor Mono 的方法,該 Reactive Streams 釋出者發出的物件將在可用時快取,並且快取命中的查詢將作為 Mono 檢索(由 CompletableFuture 支援)
@Cacheable("books")
public Mono<Book> findBook(ISBN isbn) {...}
對於返回 Reactor Flux 的方法,該 Reactive Streams 釋出者發出的物件將被收集到 List 中並在該列表完成時快取,並且快取命中的查詢將作為 Flux 檢索(由 CompletableFuture 支援快取的 List 值)
@Cacheable("books")
public Flux<Book> findBooks(String author) {...}
這種 CompletableFuture 和響應式適配也適用於同步快取,在併發快取未命中的情況下只計算一次值
@Cacheable(cacheNames="foos", sync=true) (1)
public CompletableFuture<Foo> executeExpensiveOperation(String id) {...}
| 1 | 使用 sync 屬性。 |
為了使這種安排在執行時起作用,配置的快取需要能夠基於 CompletableFuture 進行檢索。Spring 提供的 ConcurrentMapCacheManager 會自動適應這種檢索方式,而 CaffeineCacheManager 在啟用其非同步快取模式時原生支援它:在您的 CaffeineCacheManager 例項上設定 setAsyncCacheMode(true)。 |
@Bean
CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCacheSpecification(...);
cacheManager.setAsyncCacheMode(true);
return cacheManager;
}
最後但同樣重要的是,請注意,註解驅動的快取不適用於涉及組合和背壓的複雜響應式互動。如果您選擇在特定的響應式方法上宣告 @Cacheable,請考慮相當粗粒度的快取互動的影響,它只是為 Mono 儲存發出的物件,甚至為 Flux 儲存預先收集的物件列表。
條件快取
有時,一個方法可能並非總是適合快取(例如,它可能取決於給定的引數)。快取註解透過 condition 引數支援此類用例,該引數接受一個 SpEL 表示式,該表示式求值為 true 或 false。如果為 true,則快取該方法。否則,它表現得好像該方法未快取(即,每次都呼叫該方法,無論快取中有哪些值或使用了哪些引數)。例如,以下方法僅在引數 name 的長度小於 32 時才快取
@Cacheable(cacheNames="book", condition="#name.length() < 32") (1)
public Book findBook(String name)
| 1 | 在 @Cacheable 上設定條件。 |
除了 condition 引數,您還可以使用 unless 引數來否決將值新增到快取中。與 condition 不同,unless 表示式在方法呼叫後進行評估。為了擴充套件前面的示例,也許我們只想快取平裝書,如下例所示
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") (1)
public Book findBook(String name)
| 1 | 使用 unless 屬性阻止精裝書。 |
快取抽象支援 java.util.Optional 返回型別。如果 Optional 值 存在,它將被儲存在關聯的快取中。如果 Optional 值不存在,null 將被儲存在關聯的快取中。#result 始終指業務實體,從不指受支援的包裝器,因此前面的示例可以改寫如下
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)
請注意,#result 仍然指 Book 而不是 Optional<Book>。由於它可能為 null,我們使用 SpEL 的 安全導航運算子。
可用的快取 SpEL 評估上下文
每個 SpEL 表示式都會針對一個專用的 context 進行評估。除了內建引數外,框架還提供專用的快取相關元資料,例如引數名稱。下表描述了上下文中可用的項,以便您可以使用它們進行鍵和條件計算
| 名稱 | 位置 | 描述 | 示例 |
|---|---|---|---|
|
根物件 |
正在呼叫的方法的名稱 |
|
|
根物件 |
正在呼叫的方法 |
|
|
根物件 |
正在呼叫的目標物件 |
|
|
根物件 |
正在呼叫的目標的類 |
|
|
根物件 |
用於呼叫目標的引數(作為物件陣列) |
|
|
根物件 |
當前方法執行所針對的快取集合 |
|
引數名稱 |
評估上下文 |
特定方法引數的名稱。如果名稱不可用(例如,因為程式碼在沒有 |
|
|
評估上下文 |
方法呼叫的結果(要快取的值)。僅在 |
|
@CachePut 註解
當需要更新快取而不干擾方法執行時,可以使用 @CachePut 註解。也就是說,該方法總是被呼叫,其結果被放入快取中(根據 @CachePut 選項)。它支援與 @Cacheable 相同的選項,並且應該用於快取填充而不是方法流最佳化。以下示例使用 @CachePut 註解
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
在同一個方法上同時使用 @CachePut 和 @Cacheable 註解通常強烈不鼓勵,因為它們具有不同的行為。後者透過使用快取導致方法呼叫被跳過,而前者強制呼叫以執行快取更新。這會導致意外行為,除了特定的極端情況(例如具有彼此排除的條件的註解)之外,應避免此類宣告。另請注意,此類條件不應依賴於結果物件(即 #result 變數),因為它們會預先驗證以確認排除。 |
從 6.1 開始,@CachePut 考慮了 CompletableFuture 和響應式返回型別,只要生成的物件可用,就執行放置操作。
@CacheEvict 註解
快取抽象不僅允許填充快取儲存,還允許清除。此過程對於從快取中刪除陳舊或未使用的資料很有用。與 @Cacheable 相對,@CacheEvict 標記執行快取清除的方法(即,作為從快取中刪除資料的觸發器的方法)。與它的兄弟註解類似,@CacheEvict 需要指定受操作影響的一個或多個快取,允許指定自定義快取和鍵解析或條件,並具有一個額外的引數(allEntries),指示是否需要執行全快取清除而不是僅清除條目(基於鍵)。以下示例從 books 快取中清除所有條目
@CacheEvict(cacheNames="books", allEntries=true) (1)
public void loadBooks(InputStream batch)
| 1 | 使用 allEntries 屬性清除快取中的所有條目。 |
當需要清除整個快取區域時,此選項非常方便。如前面的示例所示,不是清除每個條目(這將花費很長時間,因為它效率低下),而是在一個操作中刪除所有條目。請注意,在這種情況下,框架會忽略任何指定的鍵,因為它不適用(整個快取被清除,而不僅僅是一個條目)。
您還可以使用 beforeInvocation 屬性指示清除應該在方法呼叫之後(預設)還是之前發生。前者提供與其餘註解相同的語義:一旦方法成功完成,就會對快取執行操作(在本例中為清除)。如果方法沒有執行(因為它可能已快取)或丟擲異常,則不會發生清除。後者(beforeInvocation=true)導致清除總是在方法呼叫之前發生。這在清除不需要與方法結果關聯的情況下很有用。
請注意,void 方法可以與 @CacheEvict 一起使用 - 因為這些方法充當觸發器,返回值被忽略(因為它們不與快取互動)。@Cacheable 則不同,它向快取新增資料或更新快取中的資料,因此需要一個結果。
從 6.1 開始,@CacheEvict 會考慮 CompletableFuture 和響應式返回型別,在處理完成後執行呼叫後清除操作。
@Caching 註解
有時,需要指定多個相同型別的註解(例如 @CacheEvict 或 @CachePut)——例如,因為不同快取之間的條件或鍵表示式不同。@Caching 允許在同一個方法上使用多個巢狀的 @Cacheable、@CachePut 和 @CacheEvict 註解。以下示例使用兩個 @CacheEvict 註解
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)
@CacheConfig 註解
到目前為止,我們已經看到快取操作提供了許多自定義選項,並且可以為每個操作設定這些選項。但是,如果某些自定義選項適用於類的所有操作,則配置起來可能會很繁瑣。例如,為類的每個快取操作指定要使用的快取名稱可以替換為單個類級別定義。這就是 @CacheConfig 發揮作用的地方。以下示例使用 @CacheConfig 設定快取名稱
@CacheConfig("books") (1)
public class BookRepositoryImpl implements BookRepository {
@Cacheable
public Book findBook(ISBN isbn) {...}
}
| 1 | 使用 @CacheConfig 設定快取名稱。 |
@CacheConfig 是一個類級註解,允許共享快取名稱、自定義 KeyGenerator、自定義 CacheManager 和自定義 CacheResolver。將此註解放在類上不會開啟任何快取操作。
操作級自定義總是會覆蓋在 @CacheConfig 上設定的自定義。因此,這為每個快取操作提供了三個級別的自定義
-
全域性配置,例如透過
CachingConfigurer:參見下一節。 -
在類級別,使用
@CacheConfig。 -
在操作級別。
提供程式特定的設定通常在 CacheManager bean 上可用,例如在 CaffeineCacheManager 上。這些也實際上是全域性的。 |
啟用快取註解
需要注意的是,即使聲明瞭快取註解,它們也不會自動觸發其操作——像 Spring 中的許多事情一樣,該功能必須宣告性地啟用(這意味著如果您懷疑快取是罪魁禍首,您只需刪除一行配置而不是程式碼中的所有註解即可將其停用)。
要啟用快取註解,請將 @EnableCaching 註解新增到您的一個 @Configuration 類中,或將 cache:annotation-driven 元素與 XML 一起使用
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableCaching
class CacheConfiguration {
@Bean
CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCacheSpecification("...");
return cacheManager;
}
}
@Configuration
@EnableCaching
class CacheConfiguration {
@Bean
fun cacheManager(): CacheManager {
return CaffeineCacheManager().apply {
setCacheSpecification("...")
}
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd">
<cache:annotation-driven/>
<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager">
<property name="cacheSpecification" value="..."/>
</bean>
</beans>
cache:annotation-driven 元素和 @EnableCaching 註解都允許您指定各種選項,這些選項會影響透過 AOP 將快取行為新增到應用程式的方式。該配置與 @Transactional 的配置有意地相似。
處理快取註解的預設建議模式是 proxy,它只允許透過代理攔截呼叫。同一類中的本地呼叫無法以這種方式攔截。對於更高階的攔截模式,請考慮結合編譯時或載入時織入切換到 aspectj 模式。 |
有關實現 CachingConfigurer 所需的高階自定義(使用 Java 配置)的更多詳細資訊,請參閱 javadoc。 |
| XML 屬性 | 註解屬性 | 預設值 | 描述 |
|---|---|---|---|
|
N/A(參見 |
|
要使用的快取管理器的名稱。預設的 |
|
N/A(參見 |
使用已配置的 |
CacheResolver 的 bean 名稱,用於解析後端快取。此屬性不是必需的,只需作為“cache-manager”屬性的替代品指定即可。 |
|
N/A(參見 |
|
要使用的自定義鍵生成器的名稱。 |
|
N/A(參見 |
|
要使用的自定義快取錯誤處理器的名稱。預設情況下,在快取相關操作期間丟擲的任何異常都會拋回給客戶端。 |
|
|
|
預設模式( |
|
|
|
僅適用於代理模式。控制為使用 |
|
|
Ordered.LOWEST_PRECEDENCE |
定義應用於使用 |
<cache:annotation-driven/> 僅在其定義的同一應用程式上下文中查詢帶有 @Cacheable/@CachePut/@CacheEvict/@Caching 註解的 bean。這意味著,如果您在 DispatcherServlet 的 WebApplicationContext 中放置 <cache:annotation-driven/>,它只會檢查您的控制器中的 bean,而不會檢查您的服務。有關更多資訊,請參閱 MVC 部分。 |
Spring 建議您只使用 @Cache* 註解註釋具體類(以及具體類的方法),而不是註釋介面。您當然可以在介面(或介面方法)上放置 @Cache* 註解,但這僅在使用代理模式(mode="proxy")時才有效。如果您使用基於織入的切面(mode="aspectj"),則織入基礎設施將無法識別介面級宣告上的快取設定。 |
在代理模式(預設)下,只有透過代理傳入的外部方法呼叫才會被攔截。這意味著自呼叫(實際上是目標物件中呼叫目標物件另一個方法的方法)在執行時不會導致實際快取,即使被呼叫的方法被標記為 @Cacheable。在這種情況下,請考慮使用 aspectj 模式。此外,代理必須完全初始化才能提供預期的行為,因此您不應在初始化程式碼(即 @PostConstruct)中依賴此功能。 |
使用自定義註解
快取抽象允許您使用自己的註解來標識哪些方法觸發快取填充或清除。這作為一種模板機制非常方便,因為它消除了重複快取註解宣告的需要,這在指定鍵或條件時或在程式碼庫中不允許外部匯入(org.springframework)時尤其有用。與其餘的 stereotype 註解類似,您可以將 @Cacheable、@CachePut、@CacheEvict 和 @CacheConfig 用作 元註解(即可以註釋其他註解的註解)。在以下示例中,我們將一個常見的 @Cacheable 宣告替換為我們自己的自定義註解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(cacheNames="books", key="#isbn")
public @interface SlowService {
}
在前面的示例中,我們定義了自己的 SlowService 註解,該註解本身用 @Cacheable 註解。現在我們可以替換以下程式碼
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
以下示例顯示了我們可以替換前面程式碼的自定義註解
@SlowService
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
儘管 @SlowService 不是 Spring 註解,但容器會在執行時自動識別其宣告並理解其含義。請注意,如 前面 所述,註解驅動的行為需要啟用。