基於註解的宣告式快取
對於快取宣告,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) {...}
預設 Key 生成
由於快取本質上是鍵值儲存,每個被快取方法的呼叫都需要轉換為適合快取訪問的鍵。快取抽象使用一個基於以下演算法的簡單 KeyGenerator
-
如果未提供引數,返回
SimpleKey.EMPTY
。 -
如果只提供一個引數,返回該例項。
-
如果提供多個引數,返回包含所有引數的
SimpleKey
。
只要引數具有自然鍵並實現了有效的 hashCode()
和 equals()
方法,這種方法適用於大多數用例。如果不是這種情況,你需要改變策略。
要提供不同的預設 key 生成器,你需要實現 org.springframework.cache.interceptor.KeyGenerator
介面。
預設 key 生成策略隨著 Spring 4.0 的釋出而改變。Spring 的早期版本使用一種 key 生成策略,對於多個 key 引數,它只考慮引數的 如果你想繼續使用以前的 key 策略,可以配置已棄用的 |
自定義 Key 生成宣告
由於快取是通用的,目標方法很可能具有各種簽名,這些簽名無法輕易地對映到快取結構上。當目標方法有多個引數,其中只有部分適合快取(而其餘的僅由方法邏輯使用)時,這一點往往變得很明顯。考慮以下示例
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
乍一看,雖然這兩個 boolean
引數影響查詢書籍的方式,但它們對快取沒有任何用處。此外,如果只有其中一個重要而另一個不重要怎麼辦?
對於這種情況,@Cacheable
註解允許你透過其 key
屬性指定如何生成 key。你可以使用 SpEL 來選取感興趣的引數(或其巢狀屬性),執行操作,甚至呼叫任意方法,而無需編寫任何程式碼或實現任何介面。與 預設生成器 相比,這是推薦的方法,因為隨著程式碼庫的增長,方法的簽名往往差異很大。雖然預設策略可能適用於某些方法,但很少適用於所有方法。
以下示例使用各種 SpEL 宣告(如果你不熟悉 SpEL,建議你閱讀 Spring Expression Language)
@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)
前面的程式碼片段展示了選擇某個引數、其屬性之一,甚至任意(靜態)方法是多麼容易。
如果負責生成 key 的演算法過於特定,或者需要共享,可以在操作上定義一個自定義的 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
,方式類似於替換 key 生成。每次快取操作都會請求解析,允許實現在執行時根據引數實際解析要使用的快取。以下示例展示瞭如何指定 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
的形式檢索(由 CompletableFuture
提供支援,用於快取的 List
值)
@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
值是 present(存在),它將被儲存在相關聯的快取中。如果 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
表示式都會針對一個專用的 上下文
進行求值。除了內建引數外,框架還提供了專用的快取相關元資料,例如引數名稱。下表描述了上下文中可用的項,以便你可以將它們用於 key 和條件計算
名稱 | 位置 | 描述 | 示例 |
---|---|---|---|
|
Root 物件 |
正在呼叫的方法名稱 |
|
|
Root 物件 |
正在呼叫的方法 |
|
|
Root 物件 |
正在呼叫的目標物件 |
|
|
Root 物件 |
正在呼叫的目標類 |
|
|
Root 物件 |
用於呼叫目標的引數(作為物件陣列) |
|
|
Root 物件 |
執行當前方法的快取集合 |
|
引數名 |
評估上下文 |
特定方法引數的名稱。如果名稱不可用(例如,因為程式碼編譯時沒有使用 |
|
|
評估上下文 |
方法呼叫的結果(要快取的值)。僅在 |
|
@CachePut
註解
當需要在不干擾方法執行的情況下更新快取時,可以使用 @CachePut
註解。也就是說,方法總是被呼叫,其結果會被放入快取中(根據 @CachePut
的選項)。它支援與 @Cacheable
相同的選項,應該用於快取填充而不是方法流程最佳化。下面的示例使用了 @CachePut
註解
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
通常強烈不鼓勵在同一個方法上同時使用 @CachePut 和 @Cacheable 註解,因為它們的行為不同。後者透過使用快取來跳過方法呼叫,而前者強制執行呼叫以執行快取更新。這會導致意料之外的行為,並且除了特殊的邊緣情況(例如註解具有相互排除的條件)外,應避免此類宣告。另請注意,此類條件不應依賴結果物件(即 #result 變數),因為這些變數是預先驗證以確認排除的。 |
從 6.1 版本開始,@CachePut
會考慮 CompletableFuture
和響應式返回型別,只要生成物件可用,就會執行 put 操作。
@CacheEvict
註解
快取抽象不僅允許填充快取儲存,還允許剔除。此過程對於從快取中刪除陳舊或未使用的資料很有用。與 @Cacheable
不同,@CacheEvict
標記執行快取剔除的方法(即,充當從快取中刪除資料觸發器的方法)。類似於其兄弟註解,@CacheEvict
需要指定受操作影響的一個或多個快取,允許指定自定義快取和 key 解析或條件,並具有一個額外引數(allEntries
),該引數指示是需要執行整個快取範圍的剔除,而不僅僅是條目剔除(基於 key)。以下示例清空 books
快取中的所有條目
@CacheEvict(cacheNames="books", allEntries=true) (1)
public void loadBooks(InputStream batch)
1 | 使用 allEntries 屬性清空快取中的所有條目。 |
當需要清空整個快取區域時,此選項非常方便。如前例所示,所有條目在一個操作中被移除,而不是逐個剔除條目(這會花費很長時間,因為它效率低下)。請注意,在此場景中,框架會忽略指定的任何 key,因為它不適用(清空的是整個快取,而不僅僅是一個條目)。
您還可以使用 beforeInvocation
屬性指示剔除應該在方法呼叫之後(預設)還是之前發生。前者提供與其餘註解相同的語義:一旦方法成功完成,就會在快取上執行一個操作(在本例中為剔除)。如果方法沒有執行(因為它可能已被快取)或丟擲異常,則不會發生剔除。後者(beforeInvocation=true
)會使剔除總是在方法呼叫之前發生。這在剔除不需要與方法結果關聯的情況下非常有用。
請注意,void
方法可以與 @CacheEvict
一起使用 - 由於這些方法充當觸發器,返回值會被忽略(因為它們不與快取互動)。@Cacheable
的情況並非如此,它會向快取新增資料或更新快取中的資料,因此需要一個結果。
從 6.1 版本開始,@CacheEvict
會考慮 CompletableFuture
和響應式返回型別,只要處理完成,就會執行呼叫後剔除操作。
@Caching
註解
有時,需要指定同一型別的多個註解(例如 @CacheEvict
或 @CachePut
)— 例如,因為不同快取之間的條件或 key 表示式不同。@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
類中,或者在 XML 中使用 cache:annotation-driven
元素
-
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' 屬性的替代方案來指定即可。 |
|
不適用(參見 |
|
要使用的自定義 key 生成器的名稱。 |
|
不適用(參見 |
|
要使用的自定義快取錯誤處理器的名稱。預設情況下,在快取相關操作期間丟擲的任何異常都會拋回給客戶端。 |
|
|
|
預設模式 ( |
|
|
|
僅適用於代理模式。控制為用 |
|
|
Ordered.LOWEST_PRECEDENCE |
定義應用於使用 |
<cache:annotation-driven/> 只在其定義所在的同一應用上下文中的 bean 上查詢 @Cacheable/@CachePut/@CacheEvict/@Caching 。這意味著,如果您將 <cache:annotation-driven/> 放在 DispatcherServlet 的 WebApplicationContext 中,它只檢查您的控制器中的 bean,而不是服務中的 bean。有關更多資訊,請參閱 the MVC section。 |
Spring 建議您僅使用 @Cache* 註解來註解具體類(以及具體類的方法),而不是註解介面。您當然可以在介面(或介面方法)上放置 @Cache* 註解,但這僅在使用代理模式(mode="proxy" )時有效。如果您使用基於織入的切面(mode="aspectj" ),織入基礎設施將無法識別介面級別宣告上的快取設定。 |
在代理模式(預設)下,僅攔截透過代理進入的外部方法呼叫。這意味著自我呼叫(實際上是目標物件內部的方法呼叫目標物件的另一個方法)在執行時不會導致實際快取,即使被呼叫的方法標有 @Cacheable 。在這種情況下,考慮使用 aspectj 模式。此外,代理必須完全初始化才能提供預期的行為,因此您不應在初始化程式碼(即 @PostConstruct )中依賴此功能。 |
使用自定義註解
快取抽象允許您使用自己的註解來標識哪些方法觸發快取填充或剔除。這作為一種模板機制非常方便,因為它消除了重複快取註解宣告的需要,特別是在指定了 key 或條件時,或者在程式碼庫中不允許使用外部匯入(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 註解,容器也會在執行時自動識別其宣告並理解其含義。請注意,如前面所述,註解驅動的行為需要被啟用。