基於註解的宣告式快取

對於快取宣告,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 引數,它只考慮引數的 hashCode() 而不考慮 equals()。這可能導致意外的 key 衝突(有關背景資訊,請參閱 spring-framework#14870)。新的 SimpleKeyGenerator 在這種場景下使用複合 key。

如果你想繼續使用以前的 key 策略,可以配置已棄用的 org.springframework.cache.interceptor.DefaultKeyGenerator 類,或建立自定義的基於 hash 的 KeyGenerator 實現。

自定義 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)
keykeyGenerator 引數是互斥的,同時指定兩者會導致異常。

預設快取解析

快取抽象使用一個簡單的 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 開始,快取註解的 value 屬性不再是強制性的,因為這些特定資訊可以由 CacheResolver 提供,而無需依賴註解的內容。

keykeyGenerator 類似,cacheManagercacheResolver 引數是互斥的,同時指定兩者會導致異常,因為自定義的 CacheManager 會被 CacheResolver 實現忽略。這可能不是你期望的結果。

同步快取

在多執行緒環境中,某些操作可能針對相同引數(通常在啟動時)被併發呼叫。預設情況下,快取抽象不會鎖定任何東西,並且相同的值可能會被計算多次,這違背了快取的目的。

對於這些特定情況,你可以使用 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 表示式,該表示式求值為 truefalse。如果為 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 和條件計算

表 1. SpEL 表示式中可用的快取元資料
名稱 位置 描述 示例

methodName

Root 物件

正在呼叫的方法名稱

#root.methodName

method

Root 物件

正在呼叫的方法

#root.method.name

target

Root 物件

正在呼叫的目標物件

#root.target

targetClass

Root 物件

正在呼叫的目標類

#root.targetClass

args

Root 物件

用於呼叫目標的引數(作為物件陣列)

#root.args[0]

caches

Root 物件

執行當前方法的快取集合

#root.caches[0].name

引數名

評估上下文

特定方法引數的名稱。如果名稱不可用(例如,因為程式碼編譯時沒有使用 -parameters 標誌),也可以使用 #a<#arg> 語法訪問單個引數,其中 <#arg> 代表引數索引(從 0 開始)。

#iban#a0(您也可以使用 #p0#p<#arg> 符號作為別名)。

結果

評估上下文

方法呼叫的結果(要快取的值)。僅在 unless 表示式、cache put 表示式(用於計算 key)或 cache evict 表示式(當 beforeInvocationfalse 時)中可用。對於支援的包裝器(例如 Optional),#result 指的是實際物件,而不是包裝器。

#result

@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
表 2. 快取註解設定
XML 屬性 註解屬性 預設值 描述

cache-manager

不適用(參見 CachingConfigurer javadoc)

cacheManager

要使用的快取管理器的名稱。預設的 CacheResolver 會在幕後使用此快取管理器進行初始化(如果未設定,則使用 cacheManager)。為了對快取解析進行更精細的管理,請考慮設定 'cache-resolver' 屬性。

cache-resolver

不適用(參見 CachingConfigurer javadoc)

一個使用配置的 cacheManagerSimpleCacheResolver

用於解析後備快取的 CacheResolver 的 bean 名稱。此屬性不是必需的,只需作為 'cache-manager' 屬性的替代方案來指定即可。

key-generator

不適用(參見 CachingConfigurer javadoc)

SimpleKeyGenerator

要使用的自定義 key 生成器的名稱。

error-handler

不適用(參見 CachingConfigurer javadoc)

SimpleCacheErrorHandler

要使用的自定義快取錯誤處理器的名稱。預設情況下,在快取相關操作期間丟擲的任何異常都會拋回給客戶端。

模式

模式

proxy

預設模式 (proxy) 使用 Spring 的 AOP 框架處理帶註解的 bean 以進行代理(遵循代理語義,如前所述,僅適用於透過代理進入的方法呼叫)。替代模式 (aspectj) 則使用 Spring 的 AspectJ 快取切面對受影響的類進行織入,修改目標類的位元組碼以適用於任何型別的方法呼叫。AspectJ 織入需要在類路徑中有 spring-aspects.jar,並且啟用載入時織入(或編譯時織入)。(有關如何設定載入時織入的詳細資訊,請參閱 Spring configuration)。

proxy-target-class

proxyTargetClass

false

僅適用於代理模式。控制為用 @Cacheable@CacheEvict 註解的類建立哪種型別的快取代理。如果 proxy-target-class 屬性設定為 true,則建立基於類的代理。如果 proxy-target-classfalse 或省略該屬性,則建立標準的 JDK 介面代理。(有關不同代理型別的詳細探討,請參閱 Proxying Mechanisms。)

順序

順序

Ordered.LOWEST_PRECEDENCE

定義應用於使用 @Cacheable@CacheEvict 註解的 bean 的快取通知的順序。(有關 AOP 通知排序相關規則的更多資訊,請參見 Advice Ordering。)未指定順序意味著 AOP 子系統決定通知的順序。

<cache:annotation-driven/> 只在其定義所在的同一應用上下文中的 bean 上查詢 @Cacheable/@CachePut/@CacheEvict/@Caching。這意味著,如果您將 <cache:annotation-driven/> 放在 DispatcherServletWebApplicationContext 中,它只檢查您的控制器中的 bean,而不是服務中的 bean。有關更多資訊,請參閱 the MVC section
方法可見性與快取註解

使用代理時,您應該僅將快取註解應用於具有公共可見性的方法。如果您確實使用這些註解註解了 protected、private 或包可見的方法,不會引發錯誤,但被註解的方法不會表現出配置的快取設定。如果您需要註解非公共方法,請考慮使用 AspectJ(參見本節的其餘部分),因為它會修改位元組碼本身。

Spring 建議您僅使用 @Cache* 註解來註解具體類(以及具體類的方法),而不是註解介面。您當然可以在介面(或介面方法)上放置 @Cache* 註解,但這僅在使用代理模式(mode="proxy")時有效。如果您使用基於織入的切面(mode="aspectj"),織入基礎設施將無法識別介面級別宣告上的快取設定。
在代理模式(預設)下,僅攔截透過代理進入的外部方法呼叫。這意味著自我呼叫(實際上是目標物件內部的方法呼叫目標物件的另一個方法)在執行時不會導致實際快取,即使被呼叫的方法標有 @Cacheable。在這種情況下,考慮使用 aspectj 模式。此外,代理必須完全初始化才能提供預期的行為,因此您不應在初始化程式碼(即 @PostConstruct)中依賴此功能。

使用自定義註解

自定義註解與 AspectJ

此功能僅適用於基於代理的方法,但透過使用 AspectJ,可以額外花一些力氣來啟用它。

spring-aspects 模組只為標準註解定義了一個切面。如果您定義了自己的註解,也需要為它們定義一個切面。請檢視 AnnotationCacheAspect 獲取示例。

快取抽象允許您使用自己的註解來標識哪些方法觸發快取填充或剔除。這作為一種模板機制非常方便,因為它消除了重複快取註解宣告的需要,特別是在指定了 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 註解,容器也會在執行時自動識別其宣告並理解其含義。請注意,如前面所述,註解驅動的行為需要被啟用。