javascript
注释驱动的 Spring cache 缓存介绍--转载
概述
Spring 3.1 引入了激動(dòng)人心的基于注釋(annotation)的緩存(cache)技術(shù),它本質(zhì)上不是一個(gè)具體的緩存實(shí)現(xiàn)方案(例如 EHCache 或者 OSCache),而是一個(gè)對(duì)緩存使用的抽象,通過在既有代碼中添加少量它定義的各種 annotation,即能夠達(dá)到緩存方法的返回對(duì)象的效果。
Spring 的緩存技術(shù)還具備相當(dāng)?shù)撵`活性,不僅能夠使用 SpEL(Spring Expression Language)來定義緩存的 key 和各種 condition,還提供開箱即用的緩存臨時(shí)存儲(chǔ)方案,也支持和主流的專業(yè)緩存例如 EHCache 集成。
其特點(diǎn)總結(jié)如下:
- 通過少量的配置 annotation 注釋即可使得既有代碼支持緩存
- 支持開箱即用 Out-Of-The-Box,即不用安裝和部署額外第三方組件即可使用緩存
- 支持 Spring Express Language,能使用對(duì)象的任何屬性或者方法來定義緩存的 key 和 condition
- 支持 AspectJ,并通過其實(shí)現(xiàn)任何方法的緩存支持
- 支持自定義 key 和自定義緩存管理者,具有相當(dāng)?shù)撵`活性和擴(kuò)展性
本文將針對(duì)上述特點(diǎn)對(duì) Spring cache 進(jìn)行詳細(xì)的介紹,主要通過一個(gè)簡(jiǎn)單的例子和原理介紹展開,然后我們將一起看一個(gè)比較實(shí)際的緩存例子,最后會(huì)介紹 spring cache 的使用限制和注意事項(xiàng)。OK,Let ’ s begin!
回頁(yè)首
原來我們是怎么做的
這里先展示一個(gè)完全自定義的緩存實(shí)現(xiàn),即不用任何第三方的組件來實(shí)現(xiàn)某種對(duì)象的內(nèi)存緩存。
場(chǎng)景是:對(duì)一個(gè)賬號(hào)查詢方法做緩存,以賬號(hào)名稱為 key,賬號(hào)對(duì)象為 value,當(dāng)以相同的賬號(hào)名稱查詢賬號(hào)的時(shí)候,直接從緩存中返回結(jié)果,否則更新緩存。賬號(hào)查詢服務(wù)還支持 reload 緩存(即清空緩存)。
首先定義一個(gè)實(shí)體類:賬號(hào)類,具備基本的 id 和 name 屬性,且具備 getter 和 setter 方法
清單 1. Account.java
package cacheOfAnno; public class Account { private int id; private String name; public Account(String name) { this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }然后定義一個(gè)緩存管理器,這個(gè)管理器負(fù)責(zé)實(shí)現(xiàn)緩存邏輯,支持對(duì)象的增加、修改和刪除,支持值對(duì)象的泛型。如下:
清單 2. MyCacheManager.java
package oldcache; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class MyCacheManager<T> { private Map<String,T> cache = new ConcurrentHashMap<String,T>(); public T getValue(Object key) { return cache.get(key); } public void addOrUpdateCache(String key,T value) { cache.put(key, value); } public void evictCache(String key) {// 根據(jù) key 來刪除緩存中的一條記錄if(cache.containsKey(key)) { cache.remove(key); } } public void evictCache() {// 清空緩存中的所有記錄cache.clear(); } }好,現(xiàn)在我們有了實(shí)體類和一個(gè)緩存管理器,還需要一個(gè)提供賬號(hào)查詢的服務(wù)類,此服務(wù)類使用緩存管理器來支持賬號(hào)查詢緩存,如下:
清單 3. MyAccountService.java
package oldcache; import cacheOfAnno.Account; public class MyAccountService { private MyCacheManager<Account> cacheManager; public MyAccountService() { cacheManager = new MyCacheManager<Account>();// 構(gòu)造一個(gè)緩存管理器} public Account getAccountByName(String acctName) { Account result = cacheManager.getValue(acctName);// 首先查詢緩存if(result!=null) { System.out.println("get from cache..."+acctName); return result;// 如果在緩存中,則直接返回緩存的結(jié)果} result = getFromDB(acctName);// 否則到數(shù)據(jù)庫(kù)中查詢if(result!=null) {// 將數(shù)據(jù)庫(kù)查詢的結(jié)果更新到緩存中cacheManager.addOrUpdateCache(acctName, result); } return result; } public void reload() { cacheManager.evictCache(); } private Account getFromDB(String acctName) { System.out.println("real querying db..."+acctName); return new Account(acctName); } }現(xiàn)在我們開始寫一個(gè)測(cè)試類,用于測(cè)試剛才的緩存是否有效
清單 4. Main.java
package oldcache; public class Main { public static void main(String[] args) { MyAccountService s = new MyAccountService(); // 開始查詢賬號(hào)s.getAccountByName("somebody");// 第一次查詢,應(yīng)該是數(shù)據(jù)庫(kù)查詢s.getAccountByName("somebody");// 第二次查詢,應(yīng)該直接從緩存返回s.reload();// 重置緩存System.out.println("after reload..."); s.getAccountByName("somebody");// 應(yīng)該是數(shù)據(jù)庫(kù)查詢s.getAccountByName("somebody");// 第二次查詢,應(yīng)該直接從緩存返回} }按照分析,執(zhí)行結(jié)果應(yīng)該是:首先從數(shù)據(jù)庫(kù)查詢,然后直接返回緩存中的結(jié)果,重置緩存后,應(yīng)該先從數(shù)據(jù)庫(kù)查詢,然后返回緩存中的結(jié)果,實(shí)際的執(zhí)行結(jié)果如下:
清單 5. 運(yùn)行結(jié)果
real querying db...somebody// 第一次從數(shù)據(jù)庫(kù)加載get from cache...somebody// 第二次從緩存加載after reload...// 清空緩存real querying db...somebody// 又從數(shù)據(jù)庫(kù)加載get from cache...somebody// 從緩存加載可以看出我們的緩存起效了,但是這種自定義的緩存方案有如下劣勢(shì):
- 緩存代碼和業(yè)務(wù)代碼耦合度太高,如上面的例子,AccountService 中的 getAccountByName()方法中有了太多緩存的邏輯,不便于維護(hù)和變更
- 不靈活,這種緩存方案不支持按照某種條件的緩存,比如只有某種類型的賬號(hào)才需要緩存,這種需求會(huì)導(dǎo)致代碼的變更
- 緩存的存儲(chǔ)這塊寫的比較死,不能靈活的切換為使用第三方的緩存模塊
如果你的代碼中有上述代碼的影子,那么你可以考慮按照下面的介紹來優(yōu)化一下你的代碼結(jié)構(gòu)了,也可以說是簡(jiǎn)化,你會(huì)發(fā)現(xiàn),你的代碼會(huì)變得優(yōu)雅的多!
回頁(yè)首
Hello World,注釋驅(qū)動(dòng)的 Spring Cache
Hello World 的實(shí)現(xiàn)目標(biāo)
本 Hello World 類似于其他任何的 Hello World 程序,從最簡(jiǎn)單實(shí)用的角度展現(xiàn) spring cache 的魅力,它基于剛才自定義緩存方案的實(shí)體類 Account.java,重新定義了 AccountService.java 和測(cè)試類 Main.java(注意這個(gè)例子不用自己定義緩存管理器,因?yàn)?spring 已經(jīng)提供了缺省實(shí)現(xiàn))
需要的 jar 包
為了實(shí)用 spring cache 緩存方案,在工程的 classpath 必須具備下列 jar 包。
圖 1. 工程依賴的 jar 包圖
注意這里我引入的是最新的 spring 3.2.0.M1 版本 jar 包,其實(shí)只要是 spring 3.1 以上,都支持 spring cache。其中 spring-context-*.jar 包含了 cache 需要的類。
定義實(shí)體類、服務(wù)類和相關(guān)配置文件
實(shí)體類就是上面自定義緩存方案定義的 Account.java,這里重新定義了服務(wù)類,如下:
清單 6. AccountService.java
package cacheOfAnno; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; public class AccountService { @Cacheable(value="accountCache")// 使用了一個(gè)緩存名叫 accountCache public Account getAccountByName(String userName) { // 方法內(nèi)部實(shí)現(xiàn)不考慮緩存邏輯,直接實(shí)現(xiàn)業(yè)務(wù)System.out.println("real query account."+userName); return getFromDB(userName); } private Account getFromDB(String acctName) { System.out.println("real querying db..."+acctName); return new Account(acctName); } }注意,此類的 getAccountByName 方法上有一個(gè)注釋 annotation,即 @Cacheable(value=”accountCache”),這個(gè)注釋的意思是,當(dāng)調(diào)用這個(gè)方法的時(shí)候,會(huì)從一個(gè)名叫 accountCache 的緩存中查詢,如果沒有,則執(zhí)行實(shí)際的方法(即查詢數(shù)據(jù)庫(kù)),并將執(zhí)行的結(jié)果存入緩存中,否則返回緩存中的對(duì)象。這里的緩存中的 key 就是參數(shù) userName,value 就是 Account 對(duì)象。“accountCache”緩存是在 spring*.xml 中定義的名稱。
好,因?yàn)榧尤肓?spring,所以我們還需要一個(gè) spring 的配置文件來支持基于注釋的緩存
清單 7. Spring-cache-anno.xml
<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"xmlns:p="http://www.springframework.org/schema/p"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"> <cache:annotation-driven /><bean id="accountServiceBean" class="cacheOfAnno.AccountService"/> <!-- generic cache manager --> <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager"><property name="caches"> <set> <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"p:name="default" /> <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"p:name="accountCache" /> </set> </property> </bean> </beans>注意這個(gè) spring 配置文件有一個(gè)關(guān)鍵的支持緩存的配置項(xiàng):<cache:annotation-driven />,這個(gè)配置項(xiàng)缺省使用了一個(gè)名字叫 cacheManager 的緩存管理器,這個(gè)緩存管理器有一個(gè) spring 的缺省實(shí)現(xiàn),即 org.springframework.cache.support.SimpleCacheManager,這個(gè)緩存管理器實(shí)現(xiàn)了我們剛剛自定義的緩存管理器的邏輯,它需要配置一個(gè)屬性 caches,即此緩存管理器管理的緩存集合,除了缺省的名字叫 default 的緩存,我們還自定義了一個(gè)名字叫 accountCache 的緩存,使用了缺省的內(nèi)存存儲(chǔ)方案 ConcurrentMapCacheFactoryBean,它是基于 java.util.concurrent.ConcurrentHashMap 的一個(gè)內(nèi)存緩存實(shí)現(xiàn)方案。
OK,現(xiàn)在我們具備了測(cè)試條件,測(cè)試代碼如下:
清單 8. Main.java
package cacheOfAnno; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext( "spring-cache-anno.xml");// 加載 spring 配置文件AccountService s = (AccountService) context.getBean("accountServiceBean"); // 第一次查詢,應(yīng)該走數(shù)據(jù)庫(kù)System.out.print("first query..."); s.getAccountByName("somebody"); // 第二次查詢,應(yīng)該不查數(shù)據(jù)庫(kù),直接返回緩存的值System.out.print("second query..."); s.getAccountByName("somebody"); System.out.println(); } }上面的測(cè)試代碼主要進(jìn)行了兩次查詢,第一次應(yīng)該會(huì)查詢數(shù)據(jù)庫(kù),第二次應(yīng)該返回緩存,不再查數(shù)據(jù)庫(kù),我們執(zhí)行一下,看看結(jié)果
清單 9. 執(zhí)行結(jié)果
first query...real query account.somebody// 第一次查詢r(jià)eal querying db...somebody// 對(duì)數(shù)據(jù)庫(kù)進(jìn)行了查詢second query...// 第二次查詢,沒有打印數(shù)據(jù)庫(kù)查詢?nèi)罩?#xff0c;直接返回了緩存中的結(jié)果可以看出我們?cè)O(shè)置的基于注釋的緩存起作用了,而在 AccountService.java 的代碼中,我們沒有看到任何的緩存邏輯代碼,只有一行注釋:@Cacheable(value="accountCache"),就實(shí)現(xiàn)了基本的緩存方案,是不是很強(qiáng)大?
如何清空緩存
好,到目前為止,我們的 spring cache 緩存程序已經(jīng)運(yùn)行成功了,但是還不完美,因?yàn)檫€缺少一個(gè)重要的緩存管理邏輯:清空緩存,當(dāng)賬號(hào)數(shù)據(jù)發(fā)生變更,那么必須要清空某個(gè)緩存,另外還需要定期的清空所有緩存,以保證緩存數(shù)據(jù)的可靠性。
為了加入清空緩存的邏輯,我們只要對(duì) AccountService.java 進(jìn)行修改,從業(yè)務(wù)邏輯的角度上看,它有兩個(gè)需要清空緩存的地方
- 當(dāng)外部調(diào)用更新了賬號(hào),則我們需要更新此賬號(hào)對(duì)應(yīng)的緩存
- 當(dāng)外部調(diào)用說明重新加載,則我們需要清空所有緩存
清單 10. AccountService.java
點(diǎn)擊查看代碼清單
清單 11. Main.java
點(diǎn)擊查看代碼清單
清單 12. 運(yùn)行結(jié)果
first query...real querying db...somebody second query... start testing clear cache... real querying db...somebody1 real querying db...somebody2 real update db...somebody1 real querying db...somebody1 real querying db...somebody1 real querying db...somebody2結(jié)果和我們期望的一致,所以,我們可以看出,spring cache 清空緩存的方法很簡(jiǎn)單,就是通過 @CacheEvict 注釋來標(biāo)記要清空緩存的方法,當(dāng)這個(gè)方法被調(diào)用后,即會(huì)清空緩存。注意其中一個(gè) @CacheEvict(value=”accountCache”,key=”#account.getName()”),其中的 Key 是用來指定緩存的 key 的,這里因?yàn)槲覀儽4娴臅r(shí)候用的是 account 對(duì)象的 name 字段,所以這里還需要從參數(shù) account 對(duì)象中獲取 name 的值來作為 key,前面的 # 號(hào)代表這是一個(gè) SpEL 表達(dá)式,此表達(dá)式可以遍歷方法的參數(shù)對(duì)象,具體語(yǔ)法可以參考 Spring 的相關(guān)文檔手冊(cè)。
如何按照條件操作緩存
前面介紹的緩存方法,沒有任何條件,即所有對(duì) accountService 對(duì)象的 getAccountByName 方法的調(diào)用都會(huì)起動(dòng)緩存效果,不管參數(shù)是什么值,如果有一個(gè)需求,就是只有賬號(hào)名稱的長(zhǎng)度小于等于 4 的情況下,才做緩存,大于 4 的不使用緩存,那怎么實(shí)現(xiàn)呢?
Spring cache 提供了一個(gè)很好的方法,那就是基于 SpEL 表達(dá)式的 condition 定義,這個(gè) condition 是 @Cacheable 注釋的一個(gè)屬性,下面我來演示一下
清單 13. AccountService.java(getAccountByName 方法修訂,支持條件)
@Cacheable(value="accountCache",condition="#userName.length() <= 4")// 緩存名叫 accountCache public Account getAccountByName(String userName) { // 方法內(nèi)部實(shí)現(xiàn)不考慮緩存邏輯,直接實(shí)現(xiàn)業(yè)務(wù)return getFromDB(userName); }注意其中的 condition=”#userName.length() <=4”,這里使用了 SpEL 表達(dá)式訪問了參數(shù) userName 對(duì)象的 length() 方法,條件表達(dá)式返回一個(gè)布爾值,true/false,當(dāng)條件為 true,則進(jìn)行緩存操作,否則直接調(diào)用方法執(zhí)行的返回結(jié)果。
清單 14. 測(cè)試方法
s.getAccountByName("somebody");// 長(zhǎng)度大于 4,不會(huì)被緩存s.getAccountByName("sbd");// 長(zhǎng)度小于 4,會(huì)被緩存s.getAccountByName("somebody");// 還是查詢數(shù)據(jù)庫(kù)s.getAccountByName("sbd");// 會(huì)從緩存返回清單 15. 運(yùn)行結(jié)果
real querying db...somebody real querying db...sbd real querying db...somebody可見對(duì)長(zhǎng)度大于 4 的賬號(hào)名 (somebody) 沒有緩存,每次都查詢數(shù)據(jù)庫(kù)。
如果有多個(gè)參數(shù),如何進(jìn)行 key 的組合
假設(shè) AccountService 現(xiàn)在有一個(gè)需求,要求根據(jù)賬號(hào)名、密碼和是否發(fā)送日志查詢賬號(hào)信息,很明顯,這里我們需要根據(jù)賬號(hào)名、密碼對(duì)賬號(hào)對(duì)象進(jìn)行緩存,而第三個(gè)參數(shù)“是否發(fā)送日志”對(duì)緩存沒有任何影響。所以,我們可以利用 SpEL 表達(dá)式對(duì)緩存 key 進(jìn)行設(shè)計(jì)
清單 16. Account.java(增加 password 屬性)
private String password; public String getPassword() { return password; } public void setPassword(String password) { this.password = password; }清單 17. AccountService.java(增加 getAccount 方法,支持組合 key)
@Cacheable(value="accountCache",key="#userName.concat(#password)") public Account getAccount(String userName,String password,boolean sendLog) { // 方法內(nèi)部實(shí)現(xiàn)不考慮緩存邏輯,直接實(shí)現(xiàn)業(yè)務(wù)return getFromDB(userName,password); }注意上面的 key 屬性,其中引用了方法的兩個(gè)參數(shù) userName 和 password,而 sendLog 屬性沒有考慮,因?yàn)槠鋵?duì)緩存沒有影響。
清單 18. Main.java
public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext( "spring-cache-anno.xml");// 加載 spring 配置文件AccountService s = (AccountService) context.getBean("accountServiceBean"); s.getAccount("somebody", "123456", true);// 應(yīng)該查詢數(shù)據(jù)庫(kù)s.getAccount("somebody", "123456", true);// 應(yīng)該走緩存s.getAccount("somebody", "123456", false);// 應(yīng)該走緩存s.getAccount("somebody", "654321", true);// 應(yīng)該查詢數(shù)據(jù)庫(kù)s.getAccount("somebody", "654321", true);// 應(yīng)該走緩存}上述測(cè)試,是采用了相同的賬號(hào),不同的密碼組合進(jìn)行查詢,那么一共有兩種組合情況,所以針對(duì)數(shù)據(jù)庫(kù)的查詢應(yīng)該只有兩次。
清單 19. 運(yùn)行結(jié)果
real querying db...userName=somebody password=123456 real querying db...userName=somebody password=654321和我們預(yù)期的一致。
如何做到:既要保證方法被調(diào)用,又希望結(jié)果被緩存
根據(jù)前面的例子,我們知道,如果使用了 @Cacheable 注釋,則當(dāng)重復(fù)使用相同參數(shù)調(diào)用方法的時(shí)候,方法本身不會(huì)被調(diào)用執(zhí)行,即方法本身被略過了,取而代之的是方法的結(jié)果直接從緩存中找到并返回了。
現(xiàn)實(shí)中并不總是如此,有些情況下我們希望方法一定會(huì)被調(diào)用,因?yàn)槠涑朔祷匾粋€(gè)結(jié)果,還做了其他事情,例如記錄日志,調(diào)用接口等,這個(gè)時(shí)候,我們可以用 @CachePut 注釋,這個(gè)注釋可以確保方法被執(zhí)行,同時(shí)方法的返回值也被記錄到緩存中。
清單 20. AccountService.java
@Cacheable(value="accountCache")// 使用了一個(gè)緩存名叫 accountCache public Account getAccountByName(String userName) { // 方法內(nèi)部實(shí)現(xiàn)不考慮緩存邏輯,直接實(shí)現(xiàn)業(yè)務(wù)return getFromDB(userName); } @CachePut(value="accountCache",key="#account.getName()")// 更新 accountCache 緩存public Account updateAccount(Account account) { return updateDB(account); } private Account updateDB(Account account) { System.out.println("real updating db..."+account.getName()); return account; }清單 21. Main.java
public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext( "spring-cache-anno.xml");// 加載 spring 配置文件AccountService s = (AccountService) context.getBean("accountServiceBean"); Account account = s.getAccountByName("someone"); account.setPassword("123"); s.updateAccount(account); account.setPassword("321"); s.updateAccount(account); account = s.getAccountByName("someone"); System.out.println(account.getPassword()); }如上面的代碼所示,我們首先用 getAccountByName 方法查詢一個(gè)人 someone 的賬號(hào),這個(gè)時(shí)候會(huì)查詢數(shù)據(jù)庫(kù)一次,但是也記錄到緩存中了。然后我們修改了密碼,調(diào)用了 updateAccount 方法,這個(gè)時(shí)候會(huì)執(zhí)行數(shù)據(jù)庫(kù)的更新操作且記錄到緩存,我們?cè)俅涡薷拿艽a并調(diào)用 updateAccount 方法,然后通過 getAccountByName 方法查詢,這個(gè)時(shí)候,由于緩存中已經(jīng)有數(shù)據(jù),所以不會(huì)查詢數(shù)據(jù)庫(kù),而是直接返回最新的數(shù)據(jù),所以打印的密碼應(yīng)該是“321”
清單 22. 運(yùn)行結(jié)果
real querying db...someone real updating db...someone real updating db...someone 321和分析的一樣,只查詢了一次數(shù)據(jù)庫(kù),更新了兩次數(shù)據(jù)庫(kù),最終的結(jié)果是最新的密碼。說明 @CachePut 確實(shí)可以保證方法被執(zhí)行,且結(jié)果一定會(huì)被緩存。
@Cacheable、@CachePut、@CacheEvict 注釋介紹
通過上面的例子,我們可以看到 spring cache 主要使用兩個(gè)注釋標(biāo)簽,即 @Cacheable、@CachePut 和 @CacheEvict,我們總結(jié)一下其作用和配置方法。
表 1. @Cacheable 作用和配置方法
| @Cacheable 主要的參數(shù) | ||
| value | 緩存的名稱,在 spring 配置文件中定義,必須指定至少一個(gè) | 例如: @Cacheable(value=”mycache”) 或者? @Cacheable(value={”cache1”,”cache2”} |
| key | 緩存的 key,可以為空,如果指定要按照 SpEL 表達(dá)式編寫,如果不指定,則缺省按照方法的所有參數(shù)進(jìn)行組合 | 例如: @Cacheable(value=”testcache”,key=”#userName”) |
| condition | 緩存的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進(jìn)行緩存 | 例如: @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
表 2. @CachePut 作用和配置方法
| @CachePut 主要的參數(shù) | ||
| value | 緩存的名稱,在 spring 配置文件中定義,必須指定至少一個(gè) | 例如: @Cacheable(value=”mycache”) 或者? @Cacheable(value={”cache1”,”cache2”} |
| key | 緩存的 key,可以為空,如果指定要按照 SpEL 表達(dá)式編寫,如果不指定,則缺省按照方法的所有參數(shù)進(jìn)行組合 | 例如: @Cacheable(value=”testcache”,key=”#userName”) |
| condition | 緩存的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進(jìn)行緩存 | 例如: @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
表 3. @CacheEvict 作用和配置方法
| @CacheEvict 主要的參數(shù) | ||
| value | 緩存的名稱,在 spring 配置文件中定義,必須指定至少一個(gè) | 例如: @CachEvict(value=”mycache”) 或者? @CachEvict(value={”cache1”,”cache2”} |
| key | 緩存的 key,可以為空,如果指定要按照 SpEL 表達(dá)式編寫,如果不指定,則缺省按照方法的所有參數(shù)進(jìn)行組合 | 例如: @CachEvict(value=”testcache”,key=”#userName”) |
| condition | 緩存的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才清空緩存 | 例如: @CachEvict(value=”testcache”, condition=”#userName.length()>2”) |
| allEntries | 是否清空所有緩存內(nèi)容,缺省為 false,如果指定為 true,則方法調(diào)用后將立即清空所有緩存 | 例如: @CachEvict(value=”testcache”,allEntries=true) |
| beforeInvocation | 是否在方法執(zhí)行前就清空,缺省為 false,如果指定為 true,則在方法還沒有執(zhí)行的時(shí)候就清空緩存,缺省情況下,如果方法執(zhí)行拋出異常,則不會(huì)清空緩存 | 例如: @CachEvict(value=”testcache”,beforeInvocation=true) |
回頁(yè)首
基本原理
和 spring 的事務(wù)管理類似,spring cache 的關(guān)鍵原理就是 spring AOP,通過 spring AOP,其實(shí)現(xiàn)了在方法調(diào)用前、調(diào)用后獲取方法的入?yún)⒑头祷刂?#xff0c;進(jìn)而實(shí)現(xiàn)了緩存的邏輯。我們來看一下下面這個(gè)圖:
圖 2. 原始方法調(diào)用圖
上圖顯示,當(dāng)客戶端“Calling code”調(diào)用一個(gè)普通類 Plain Object 的 foo() 方法的時(shí)候,是直接作用在 pojo 類自身對(duì)象上的,客戶端擁有的是被調(diào)用者的直接的引用。
而 Spring cache 利用了 Spring AOP 的動(dòng)態(tài)代理技術(shù),即當(dāng)客戶端嘗試調(diào)用 pojo 的 foo()方法的時(shí)候,給他的不是 pojo 自身的引用,而是一個(gè)動(dòng)態(tài)生成的代理類
圖 3. 動(dòng)態(tài)代理調(diào)用圖
如上圖所示,這個(gè)時(shí)候,實(shí)際客戶端擁有的是一個(gè)代理的引用,那么在調(diào)用 foo() 方法的時(shí)候,會(huì)首先調(diào)用 proxy 的 foo() 方法,這個(gè)時(shí)候 proxy 可以整體控制實(shí)際的 pojo.foo() 方法的入?yún)⒑头祷刂?#xff0c;比如緩存結(jié)果,比如直接略過執(zhí)行實(shí)際的 foo() 方法等,都是可以輕松做到的。
回頁(yè)首
擴(kuò)展性
直到現(xiàn)在,我們已經(jīng)學(xué)會(huì)了如何使用開箱即用的 spring cache,這基本能夠滿足一般應(yīng)用對(duì)緩存的需求,但現(xiàn)實(shí)總是很復(fù)雜,當(dāng)你的用戶量上去或者性能跟不上,總需要進(jìn)行擴(kuò)展,這個(gè)時(shí)候你或許對(duì)其提供的內(nèi)存緩存不滿意了,因?yàn)槠洳恢С指呖捎眯?#xff0c;也不具備持久化數(shù)據(jù)能力,這個(gè)時(shí)候,你就需要自定義你的緩存方案了,還好,spring 也想到了這一點(diǎn)。
我們先不考慮如何持久化緩存,畢竟這種第三方的實(shí)現(xiàn)方案很多,我們要考慮的是,怎么利用 spring 提供的擴(kuò)展點(diǎn)實(shí)現(xiàn)我們自己的緩存,且在不改原來已有代碼的情況下進(jìn)行擴(kuò)展。
首先,我們需要提供一個(gè) CacheManager 接口的實(shí)現(xiàn),這個(gè)接口告訴 spring 有哪些 cache 實(shí)例,spring 會(huì)根據(jù) cache 的名字查找 cache 的實(shí)例。另外還需要自己實(shí)現(xiàn) Cache 接口,Cache 接口負(fù)責(zé)實(shí)際的緩存邏輯,例如增加鍵值對(duì)、存儲(chǔ)、查詢和清空等。利用 Cache 接口,我們可以對(duì)接任何第三方的緩存系統(tǒng),例如 EHCache、OSCache,甚至一些內(nèi)存數(shù)據(jù)庫(kù)例如 memcache 或者 h2db 等。下面我舉一個(gè)簡(jiǎn)單的例子說明如何做。
清單 23. MyCacheManager
package cacheOfAnno; import java.util.Collection; import org.springframework.cache.support.AbstractCacheManager; public class MyCacheManager extends AbstractCacheManager { private Collection<? extends MyCache> caches; /** * Specify the collection of Cache instances to use for this CacheManager. */ public void setCaches(Collection<? extends MyCache> caches) { this.caches = caches; } @Override protected Collection<? extends MyCache> loadCaches() { return this.caches; } }上面的自定義的 CacheManager 實(shí)際繼承了 spring 內(nèi)置的 AbstractCacheManager,實(shí)際上僅僅管理 MyCache 類的實(shí)例。
清單 24. MyCache
package cacheOfAnno; import java.util.HashMap; import java.util.Map; import org.springframework.cache.Cache; import org.springframework.cache.support.SimpleValueWrapper; public class MyCache implements Cache { private String name; private Map<String,Account> store = new HashMap<String,Account>();; public MyCache() { } public MyCache(String name) { this.name = name; } @Override public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public Object getNativeCache() { return store; } @Override public ValueWrapper get(Object key) { ValueWrapper result = null; Account thevalue = store.get(key); if(thevalue!=null) { thevalue.setPassword("from mycache:"+name); result = new SimpleValueWrapper(thevalue); } return result; } @Override public void put(Object key, Object value) { Account thevalue = (Account)value; store.put((String)key, thevalue); } @Override public void evict(Object key) { } @Override public void clear() { } }上面的自定義緩存只實(shí)現(xiàn)了很簡(jiǎn)單的邏輯,但這是我們自己做的,也很令人激動(dòng)是不是,主要看 get 和 put 方法,其中的 get 方法留了一個(gè)后門,即所有的從緩存查詢返回的對(duì)象都將其 password 字段設(shè)置為一個(gè)特殊的值,這樣我們等下就能演示“我們的緩存確實(shí)在起作用!”了。
這還不夠,spring 還不知道我們寫了這些東西,需要通過 spring*.xml 配置文件告訴它
清單 25. Spring-cache-anno.xml
<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"xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"> <cache:annotation-driven /> <bean id="accountServiceBean" class="cacheOfAnno.AccountService"/> <!-- generic cache manager --> <bean id="cacheManager" class="cacheOfAnno.MyCacheManager"><property name="caches"> <set> <bean class="cacheOfAnno.MyCache"p:name="accountCache" /> </set> </property> </bean> </beans>注意上面配置文件的黑體字,這些配置說明了我們的 cacheManager 和我們自己的 cache 實(shí)例。
好,什么都不說,測(cè)試!
清單 26. Main.java
public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext( "spring-cache-anno.xml");// 加載 spring 配置文件AccountService s = (AccountService) context.getBean("accountServiceBean"); Account account = s.getAccountByName("someone"); System.out.println("passwd="+account.getPassword()); account = s.getAccountByName("someone"); System.out.println("passwd="+account.getPassword()); }上面的測(cè)試代碼主要是先調(diào)用 getAccountByName 進(jìn)行一次查詢,這會(huì)調(diào)用數(shù)據(jù)庫(kù)查詢,然后緩存到 mycache 中,然后我打印密碼,應(yīng)該是空的;下面我再次查詢 someone 的賬號(hào),這個(gè)時(shí)候會(huì)從 mycache 中返回緩存的實(shí)例,記得上面的后門么?我們修改了密碼,所以這個(gè)時(shí)候打印的密碼應(yīng)該是一個(gè)特殊的值
清單 27. 運(yùn)行結(jié)果
real querying db...someone passwd=null passwd=from mycache:accountCache結(jié)果符合預(yù)期,即第一次查詢數(shù)據(jù)庫(kù),且密碼為空,第二次打印了一個(gè)特殊的密碼。說明我們的 myCache 起作用了。
回頁(yè)首
注意和限制
基于 proxy 的 spring aop 帶來的內(nèi)部調(diào)用問題
上面介紹過 spring cache 的原理,即它是基于動(dòng)態(tài)生成的 proxy 代理機(jī)制來對(duì)方法的調(diào)用進(jìn)行切面,這里關(guān)鍵點(diǎn)是對(duì)象的引用問題,如果對(duì)象的方法是內(nèi)部調(diào)用(即 this 引用)而不是外部引用,則會(huì)導(dǎo)致 proxy 失效,那么我們的切面就失效,也就是說上面定義的各種注釋包括 @Cacheable、@CachePut 和 @CacheEvict 都會(huì)失效,我們來演示一下。
清單 28. AccountService.java
public Account getAccountByName2(String userName) { return this.getAccountByName(userName); } @Cacheable(value="accountCache")// 使用了一個(gè)緩存名叫 accountCache public Account getAccountByName(String userName) { // 方法內(nèi)部實(shí)現(xiàn)不考慮緩存邏輯,直接實(shí)現(xiàn)業(yè)務(wù)return getFromDB(userName); }上面我們定義了一個(gè)新的方法 getAccountByName2,其自身調(diào)用了 getAccountByName 方法,這個(gè)時(shí)候,發(fā)生的是內(nèi)部調(diào)用(this),所以沒有走 proxy,導(dǎo)致 spring cache 失效
清單 29. Main.java
public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext( "spring-cache-anno.xml");// 加載 spring 配置文件AccountService s = (AccountService) context.getBean("accountServiceBean"); s.getAccountByName2("someone"); s.getAccountByName2("someone"); s.getAccountByName2("someone"); }清單 30. 運(yùn)行結(jié)果
real querying db...someone real querying db...someone real querying db...someone可見,結(jié)果是每次都查詢數(shù)據(jù)庫(kù),緩存沒起作用。要避免這個(gè)問題,就是要避免對(duì)緩存方法的內(nèi)部調(diào)用,或者避免使用基于 proxy 的 AOP 模式,可以使用基于 aspectJ 的 AOP 模式來解決這個(gè)問題。
@CacheEvict 的可靠性問題
我們看到,@CacheEvict 注釋有一個(gè)屬性 beforeInvocation,缺省為 false,即缺省情況下,都是在實(shí)際的方法執(zhí)行完成后,才對(duì)緩存進(jìn)行清空操作。期間如果執(zhí)行方法出現(xiàn)異常,則會(huì)導(dǎo)致緩存清空不被執(zhí)行。我們演示一下
清單 31. AccountService.java
@CacheEvict(value="accountCache",allEntries=true)// 清空 accountCache 緩存public void reload() { throw new RuntimeException(); }注意上面的代碼,我們?cè)?reload 的時(shí)候拋出了運(yùn)行期異常,這會(huì)導(dǎo)致清空緩存失敗。
清單 32. Main.java
public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext( "spring-cache-anno.xml");// 加載 spring 配置文件AccountService s = (AccountService) context.getBean("accountServiceBean"); s.getAccountByName("someone"); s.getAccountByName("someone"); try { s.reload(); } catch (Exception e) { } s.getAccountByName("someone"); }上面的測(cè)試代碼先查詢了兩次,然后 reload,然后再查詢一次,結(jié)果應(yīng)該是只有第一次查詢走了數(shù)據(jù)庫(kù),其他兩次查詢都從緩存,第三次也走緩存因?yàn)?reload 失敗了。
清單 33. 運(yùn)行結(jié)果
real querying db...someone和預(yù)期一樣。那么我們?nèi)绾伪苊膺@個(gè)問題呢?我們可以用 @CacheEvict 注釋提供的 beforeInvocation 屬性,將其設(shè)置為 true,這樣,在方法執(zhí)行前我們的緩存就被清空了。可以確保緩存被清空。
清單 34. AccountService.java
@CacheEvict(value="accountCache",allEntries=true,beforeInvocation=true)// 清空 accountCache 緩存public void reload() { throw new RuntimeException(); }注意上面的代碼,我們?cè)?@CacheEvict 注釋中加了 beforeInvocation 屬性,確保緩存被清空。
執(zhí)行相同的測(cè)試代碼
清單 35. 運(yùn)行結(jié)果
real querying db...someone real querying db...someone這樣,第一次和第三次都從數(shù)據(jù)庫(kù)取數(shù)據(jù)了,緩存清空有效。
非 public 方法問題
和內(nèi)部調(diào)用問題類似,非 public 方法如果想實(shí)現(xiàn)基于注釋的緩存,必須采用基于 AspectJ 的 AOP 機(jī)制,這里限于篇幅不再細(xì)述。
回頁(yè)首
其他技巧
Dummy CacheManager 的配置和作用
有的時(shí)候,我們?cè)诖a遷移、調(diào)試或者部署的時(shí)候,恰好沒有 cache 容器,比如 memcache 還不具備條件,h2db 還沒有裝好等,如果這個(gè)時(shí)候你想調(diào)試代碼,豈不是要瘋掉?這里有一個(gè)辦法,在不具備緩存條件的時(shí)候,在不改代碼的情況下,禁用緩存。
方法就是修改 spring*.xml 配置文件,設(shè)置一個(gè)找不到緩存就不做任何操作的標(biāo)志位,如下
清單 36. Spring-cache-anno.xml
<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"xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"> <cache:annotation-driven /> <bean id="accountServiceBean" class="cacheOfAnno.AccountService"/> <!-- generic cache manager --> <bean id="simpleCacheManager" class="org.springframework.cache.support.SimpleCacheManager"> <property name="caches"> <set> <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"p:name="default" /> </set> </property> </bean> <!-- dummy cacheManager --> <bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager"><property name="cacheManagers"> <list> <ref bean="simpleCacheManager" /> </list> </property> <property name="fallbackToNoOpCache" value="true" /> </bean> </beans>注意以前的 cacheManager 變?yōu)榱?simpleCacheManager,且沒有配置 accountCache 實(shí)例,后面的 cacheManager 的實(shí)例是一個(gè) CompositeCacheManager,他利用了前面的 simpleCacheManager 進(jìn)行查詢,如果查詢不到,則根據(jù)標(biāo)志位 fallbackToNoOpCache 來判斷是否不做任何緩存操作。
清單 37. 運(yùn)行結(jié)果
real querying db...someone real querying db...someone real querying db...someone可以看出,緩存失效。每次都查詢數(shù)據(jù)庫(kù)。因?yàn)槲覀儧]有配置它需要的 accountCache 實(shí)例。
如果將上面 xml 配置文件的 fallbackToNoOpCache 設(shè)置為 false,再次運(yùn)行,則會(huì)得到
清單 38. 運(yùn)行結(jié)果
Exception in thread "main" java.lang.IllegalArgumentException: Cannot find cache named [accountCache] for CacheableOperation [public cacheOfAnno.Account cacheOfAnno.AccountService.getAccountByName(java.lang.String)]caches=[accountCache] | condition='' | key=''可見,在找不到 accountCache,且沒有將 fallbackToNoOpCache 設(shè)置為 true 的情況下,系統(tǒng)會(huì)拋出異常。
回頁(yè)首
小結(jié)
總之,注釋驅(qū)動(dòng)的 spring cache 能夠極大的減少我們編寫常見緩存的代碼量,通過少量的注釋標(biāo)簽和配置文件,即可達(dá)到使代碼具備緩存的能力。且具備很好的靈活性和擴(kuò)展性。但是我們也應(yīng)該看到,spring cache 由于急于 spring AOP 技術(shù),尤其是動(dòng)態(tài)的 proxy 技術(shù),導(dǎo)致其不能很好的支持方法的內(nèi)部調(diào)用或者非 public 方法的緩存設(shè)置,當(dāng)然這都是可以解決的問題,通過學(xué)習(xí)這個(gè)技術(shù),我們能夠認(rèn)識(shí)到,AOP 技術(shù)的應(yīng)用還是很廣泛的,如果有興趣,我相信你也能基于 AOP 實(shí)現(xiàn)自己的緩存方案。
原文:http://www.ibm.com/developerworks/cn/opensource/os-cn-spring-cache/
轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/p/4231202.html
總結(jié)
以上是生活随笔為你收集整理的注释驱动的 Spring cache 缓存介绍--转载的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: AOP 的利器:ASM 3.0 介绍
- 下一篇: spring core源码解读之ASM4