宣告式註解基礎快取
對於快取宣告,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 和 Reactive 傳回類型進行快取
從 6.1 開始,快取註解會將 CompletableFuture
和 Reactive 傳回類型納入考量,並自動調整快取互動以符合需求。
對於傳回 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
(由快取 List
值的 CompletableFuture
支援)
@Cacheable("books")
public Flux<Book> findBooks(String author) {...}
這種 CompletableFuture
和 Reactive 適配也適用於同步快取,在發生並行快取未命中時僅計算一次值
@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;
}
最後但並非最不重要的一點是,請注意,註解驅動的快取不適用於涉及組合和背壓的複雜 Reactive 互動。 如果您選擇在特定的 Reactive 方法上宣告 @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 評估 Context
每個 SpEL
運算式都針對專用的 context
進行評估。 除了內建參數之外,框架還提供專用的快取相關中繼資料,例如引數名稱。 下表描述了 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
和反應式傳回類型納入考量,在處理完成後執行 invocation 後逐出操作。
@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 屬性 | 註解屬性 | 預設值 | 描述 |
---|---|---|---|
|
不適用(請參閱 |
|
要使用的快取管理器的名稱。預設的 |
|
不適用(請參閱 |
使用已配置的 |
要用於解析後端快取的 CacheResolver 的 bean 名稱。此屬性不是必要的,僅需要指定作為 'cache-manager' 屬性的替代方案。 |
|
不適用(請參閱 |
|
要使用的自訂鍵值產生器的名稱。 |
|
不適用(請參閱 |
|
要使用的自訂快取錯誤處理器的名稱。預設情況下,在快取相關操作期間拋出的任何例外都會拋回給用戶端。 |
|
|
|
預設模式( |
|
|
|
僅適用於代理模式。控制為帶有 |
|
|
Ordered.LOWEST_PRECEDENCE |
定義應用於帶有 |
<cache:annotation-driven/> 僅在定義它的同一個應用程式環境中的 bean 上尋找 @Cacheable/@CachePut/@CacheEvict/@Caching 。這表示,如果您在 DispatcherServlet 的 WebApplicationContext 中放置 <cache:annotation-driven/> ,它僅檢查控制器中的 bean,而不是服務中的 bean。請參閱 MVC 區段,以取得更多資訊。 |
Spring 建議您僅使用 @Cache* 註解來註解具體類別(以及具體類別的方法),而不是註解介面。您當然可以在介面(或介面方法)上放置 @Cache* 註解,但這僅在您使用代理模式(mode="proxy" )時才有效。如果您使用基於織入的 aspect(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 註解,容器也會在運行時自動拾取其宣告並理解其含義。請注意,如先前所述,需要啟用註解驅動的行為。