概述
前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。
Spring 3.1 引入了激動人心的基于注釋(annotation)的緩存(cache)技術,它本質上不是一個具體的緩存實現方案(例如 EHCache 或者 OSCache),而是一個對緩存使用的抽象,通過在既有代碼中添加少量它定義的各種 annotation,即能夠達到緩存方法的返回對象的效果。
Spring 的緩存技術還具備相當的靈活性,不僅能夠使用 SpEL(Spring Expression Language)來定義緩存的 key 和各種 condition,還提供開箱即用的緩存臨時存儲方案,也支持和主流的專業緩存例如 EHCache 集成。
其特點總結如下:
- 通過少量的配置 annotation 注釋即可使得既有代碼支持緩存
- 支持開箱即用 Out-Of-The-Box,即不用安裝和部署額外第三方組件即可使用緩存
- 支持 Spring Express Language,能使用對象的任何屬性或者方法來定義緩存的 key 和 condition
- 支持 AspectJ,并通過其實現任何方法的緩存支持
- 支持自定義 key 和自定義緩存管理者,具有相當的靈活性和擴展性
本文將針對上述特點對 Spring cache 進行詳細的介紹,主要通過一個簡單的例子和原理介紹展開,然后我們將一起看一個比較實際的緩存例子,最后會介紹 spring cache 的使用限制和注意事項。OK,Let ’ s begin!
原來我們是怎么做的
這里先展示一個完全自定義的緩存實現,即不用任何第三方的組件來實現某種對象的內存緩存。
場景是:對一個賬號查詢方法做緩存,以賬號名稱為 key,賬號對象為 value,當以相同的賬號名稱查詢賬號的時候,直接從緩存中返回結果,否則更新緩存。賬號查詢服務還支持 reload 緩存(即清空緩存)。
首先定義一個實體類:賬號類,具備基本的 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;}
}
?
然后定義一個緩存管理器,這個管理器負責實現緩存邏輯,支持對象的增加、修改和刪除,支持值對象的泛型。如下:
清單 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) {// 根據 key 來刪除緩存中的一條記錄if(cache.containsKey(key)) {cache.remove(key);}}public void evictCache() {// 清空緩存中的所有記錄cache.clear();}
}
?
好,現在我們有了實體類和一個緩存管理器,還需要一個提供賬號查詢的服務類,此服務類使用緩存管理器來支持賬號查詢緩存,如下:
?
清單 3. MyAccountService.java
package oldcache;import cacheOfAnno.Account;public class MyAccountService {private MyCacheManager<Account> cacheManager;public MyAccountService() {cacheManager = new MyCacheManager<Account>();// 構造一個緩存管理器}public Account getAccountByName(String acctName) {Account result = cacheManager.getValue(acctName);// 首先查詢緩存if(result!=null) {System.out.println("get from cache..."+acctName);return result;// 如果在緩存中,則直接返回緩存的結果}result = getFromDB(acctName);// 否則到數據庫中查詢if(result!=null) {// 將數據庫查詢的結果更新到緩存中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);}
}
?
現在我們開始寫一個測試類,用于測試剛才的緩存是否有效
?
清單 4. Main.java
package oldcache;public class Main {public static void main(String[] args) {MyAccountService s = new MyAccountService();// 開始查詢賬號s.getAccountByName("somebody");// 第一次查詢,應該是數據庫查詢s.getAccountByName("somebody");// 第二次查詢,應該直接從緩存返回s.reload();// 重置緩存System.out.println("after reload...");s.getAccountByName("somebody");// 應該是數據庫查詢s.getAccountByName("somebody");// 第二次查詢,應該直接從緩存返回}}
?
按照分析,執行結果應該是:首先從數據庫查詢,然后直接返回緩存中的結果,重置緩存后,應該先從數據庫查詢,然后返回緩存中的結果,實際的執行結果如下:
?
清單 5. 運行結果
real querying db...somebody// 第一次從數據庫加載
get from cache...somebody// 第二次從緩存加載
after reload...// 清空緩存
real querying db...somebody// 又從數據庫加載
get from cache...somebody// 從緩存加載
?
可以看出我們的緩存起效了,但是這種自定義的緩存方案有如下劣勢:
?
- 緩存代碼和業務代碼耦合度太高,如上面的例子,AccountService 中的 getAccountByName()方法中有了太多緩存的邏輯,不便于維護和變更
- 不靈活,這種緩存方案不支持按照某種條件的緩存,比如只有某種類型的賬號才需要緩存,這種需求會導致代碼的變更
- 緩存的存儲這塊寫的比較死,不能靈活的切換為使用第三方的緩存模塊
如果你的代碼中有上述代碼的影子,那么你可以考慮按照下面的介紹來優化一下你的代碼結構了,也可以說是簡化,你會發現,你的代碼會變得優雅的多!
Hello World,注釋驅動的 Spring Cache
Hello World 的實現目標
本 Hello World 類似于其他任何的 Hello World 程序,從最簡單實用的角度展現 spring cache 的魅力,它基于剛才自定義緩存方案的實體類 Account.java,重新定義了 AccountService.java 和測試類 Main.java(注意這個例子不用自己定義緩存管理器,因為 spring 已經提供了缺省實現)
需要的 jar 包
為了實用 spring cache 緩存方案,在工程的 classpath 必須具備下列 jar 包。
圖 1. 工程依賴的 jar 包圖
注意這里我引入的是最新的 spring 3.2.0.M1 版本 jar 包,其實只要是 spring 3.1 以上,都支持 spring cache。其中 spring-context-*.jar 包含了 cache 需要的類。
定義實體類、服務類和相關配置文件
實體類就是上面自定義緩存方案定義的 Account.java,這里重新定義了服務類,如下:
清單 6. AccountService.java
package cacheOfAnno;import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;public class AccountService {@Cacheable(value="accountCache")// 使用了一個緩存名叫 accountCachepublic Account getAccountByName(String userName) {// 方法內部實現不考慮緩存邏輯,直接實現業務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 方法上有一個注釋 annotation,即 @Cacheable(value=”accountCache”),這個注釋的意思是,當調用這個方法的時候,會從一個名叫 accountCache 的緩存中查詢,如果沒有,則執行實際的方法(即查詢數據庫),并將執行的結果存入緩存中,否則返回緩存中的對象。這里的緩存中的 key 就是參數 userName,value 就是 Account 對象。“accountCache”緩存是在 spring*.xml 中定義的名稱。
?
好,因為加入了 spring,所以我們還需要一個 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/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/cachehttp://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><beanclass="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"p:name="default" /><beanclass="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"p:name="accountCache" /></set></property></bean>
</beans>
?
注意這個 spring 配置文件有一個關鍵的支持緩存的配置項:<cache:annotation-driven />,
這個配置項缺省使用了一個名字叫 cacheManager 的緩存管理器,這個緩存管理器有一個 spring 的缺省實現,即 org.springframework.cache.support.SimpleCacheManager,這個緩存管理器實現了我們剛剛自定義的緩存管理器的邏輯,它需要配置一個屬性 caches,即此緩存管理器管理的緩存集合,除了缺省的名字叫 default 的緩存,我們還自定義了一個名字叫 accountCache 的緩存,使用了缺省的內存存儲方案 ConcurrentMapCacheFactoryBean,它是基于 java.util.concurrent.ConcurrentHashMap 的一個內存緩存實現方案。
OK,現在我們具備了測試條件,測試代碼如下:
清單 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");// 第一次查詢,應該走數據庫System.out.print("first query...");s.getAccountByName("somebody");// 第二次查詢,應該不查數據庫,直接返回緩存的值System.out.print("second query...");s.getAccountByName("somebody");System.out.println();}
}
?
上面的測試代碼主要進行了兩次查詢,第一次應該會查詢數據庫,第二次應該返回緩存,不再查數據庫,我們執行一下,看看結果
?
清單 9. 執行結果
1 2 3 | first query...real query account.somebody// 第一次查詢 real querying db...somebody// 對數據庫進行了查詢 second query...// 第二次查詢,沒有打印數據庫查詢日志,直接返回了緩存中的結果 |
可以看出我們設置的基于注釋的緩存起作用了,而在 AccountService.java 的代碼中,我們沒有看到任何的緩存邏輯代碼,只有一行注釋:@Cacheable(value="accountCache"),就實現了基本的緩存方案,是不是很強大?
如何清空緩存
好,到目前為止,我們的 spring cache 緩存程序已經運行成功了,但是還不完美,因為還缺少一個重要的緩存管理邏輯:清空緩存,當賬號數據發生變更,那么必須要清空某個緩存,另外還需要定期的清空所有緩存,以保證緩存數據的可靠性。
為了加入清空緩存的邏輯,我們只要對 AccountService.java 進行修改,從業務邏輯的角度上看,它有兩個需要清空緩存的地方
- 當外部調用更新了賬號,則我們需要更新此賬號對應的緩存
- 當外部調用說明重新加載,則我們需要清空所有緩存
清單 10. AccountService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | package cacheOfAnno; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; public class AccountService { ?? @Cacheable(value="accountCache")// 使用了一個緩存名叫 accountCache ?? public Account getAccountByName(String userName) { ???? // 方法內部實現不考慮緩存邏輯,直接實現業務 ???? return getFromDB(userName); ?? } ?? @CacheEvict(value="accountCache",key="#account.getName()")// 清空 accountCache 緩存? public void updateAccount(Account account) { ???? updateDB(account); ?? } ? ??? @CacheEvict(value="accountCache",allEntries=true)// 清空 accountCache 緩存 ?? public void reload() { ?? } ? ??? private Account getFromDB(String acctName) { ???? System.out.println("real querying db..."+acctName); ???? return new Account(acctName); ?? } ? ??? private void updateDB(Account account) { ???? System.out.println("real update db..."+account.getName()); ?? } ? ?} |
清單 11. Main.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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"); ???? // 第一次查詢,應該走數據庫 ???? System.out.print("first query..."); ???? s.getAccountByName("somebody"); ???? // 第二次查詢,應該不查數據庫,直接返回緩存的值 ???? System.out.print("second query..."); ???? s.getAccountByName("somebody"); ???? System.out.println(); ??? ????? System.out.println("start testing clear cache...");??? // 更新某個記錄的緩存,首先構造兩個賬號記錄,然后記錄到緩存中 ???? Account account1 = s.getAccountByName("somebody1"); ???? Account account2 = s.getAccountByName("somebody2"); ???? // 開始更新其中一個??? account1.setId(1212); ???? s.updateAccount(account1); ???? s.getAccountByName("somebody1");// 因為被更新了,所以會查詢數據庫??? s.getAccountByName("somebody2");// 沒有更新過,應該走緩存??? s.getAccountByName("somebody1");// 再次查詢,應該走緩存??? // 更新所有緩存 ???? s.reload(); ???? s.getAccountByName("somebody1");// 應該會查詢數據庫??? s.getAccountByName("somebody2");// 應該會查詢數據庫??? s.getAccountByName("somebody1");// 應該走緩存??? s.getAccountByName("somebody2");// 應該走緩存 ?? } } |
清單 12. 運行結果
1 2 3 4 5 6 7 8 9 | 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 |
結果和我們期望的一致,所以,我們可以看出,spring cache 清空緩存的方法很簡單,就是通過 @CacheEvict 注釋來標記要清空緩存的方法,當這個方法被調用后,即會清空緩存。注意其中一個 @CacheEvict(value=”accountCache”,key=”#account.getName()”),其中的 Key 是用來指定緩存的 key 的,這里因為我們保存的時候用的是 account 對象的 name 字段,所以這里還需要從參數 account 對象中獲取 name 的值來作為 key,前面的 # 號代表這是一個 SpEL 表達式,此表達式可以遍歷方法的參數對象,具體語法可以參考 Spring 的相關文檔手冊。
如何按照條件操作緩存
前面介紹的緩存方法,沒有任何條件,即所有對 accountService 對象的 getAccountByName 方法的調用都會起動緩存效果,不管參數是什么值,如果有一個需求,就是只有賬號名稱的長度小于等于 4 的情況下,才做緩存,大于 4 的不使用緩存,那怎么實現呢?
Spring cache 提供了一個很好的方法,那就是基于 SpEL 表達式的 condition 定義,這個 condition 是 @Cacheable 注釋的一個屬性,下面我來演示一下
清單 13. AccountService.java(getAccountByName 方法修訂,支持條件)
1 2 3 4 5 | @Cacheable(value="accountCache",condition="#userName.length() <= 4")// 緩存名叫 accountCache public Account getAccountByName(String userName) { // 方法內部實現不考慮緩存邏輯,直接實現業務 return getFromDB(userName); } |
注意其中的 condition=”#userName.length() <=4”,這里使用了 SpEL 表達式訪問了參數 userName 對象的 length() 方法,條件表達式返回一個布爾值,true/false,當條件為 true,則進行緩存操作,否則直接調用方法執行的返回結果。
清單 14. 測試方法
1 2 3 4 | s.getAccountByName("somebody");// 長度大于 4,不會被緩存 s.getAccountByName("sbd");// 長度小于 4,會被緩存 s.getAccountByName("somebody");// 還是查詢數據庫 s.getAccountByName("sbd");// 會從緩存返回 |
清單 15. 運行結果
1 2 3 | real querying db...somebody real querying db...sbd real querying db...somebody |
可見對長度大于 4 的賬號名 (somebody) 沒有緩存,每次都查詢數據庫。
如果有多個參數,如何進行 key 的組合
假設 AccountService 現在有一個需求,要求根據賬號名、密碼和是否發送日志查詢賬號信息,很明顯,這里我們需要根據賬號名、密碼對賬號對象進行緩存,而第三個參數“是否發送日志”對緩存沒有任何影響。所以,我們可以利用 SpEL 表達式對緩存 key 進行設計
清單 16. Account.java(增加 password 屬性)
1 2 3 4 5 6 7 | private String password; public String getPassword() { ?? return password; } public void setPassword(String password) { ?? this.password = password; } |
清單 17. AccountService.java(增加 getAccount 方法,支持組合 key)
1 2 3 4 5 6 | @Cacheable(value="accountCache",key="#userName.concat(#password)") public Account getAccount(String userName,String password,boolean sendLog) { ?? // 方法內部實現不考慮緩存邏輯,直接實現業務 ?? return getFromDB(userName,password); ? ?} |
注意上面的 key 屬性,其中引用了方法的兩個參數 userName 和 password,而 sendLog 屬性沒有考慮,因為其對緩存沒有影響。
清單 18. Main.java
1 2 3 4 5 6 7 8 9 10 11 | 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);// 應該查詢數據庫 ?? s.getAccount("somebody", "123456", true);// 應該走緩存 ?? s.getAccount("somebody", "123456", false);// 應該走緩存 ?? s.getAccount("somebody", "654321", true);// 應該查詢數據庫 ?? s.getAccount("somebody", "654321", true);// 應該走緩存 } |
上述測試,是采用了相同的賬號,不同的密碼組合進行查詢,那么一共有兩種組合情況,所以針對數據庫的查詢應該只有兩次。
清單 19. 運行結果
1 2 | real querying db...userName=somebody password=123456 real querying db...userName=somebody password=654321 |
和我們預期的一致。
如何做到:既要保證方法被調用,又希望結果被緩存
根據前面的例子,我們知道,如果使用了 @Cacheable 注釋,則當重復使用相同參數調用方法的時候,方法本身不會被調用執行,即方法本身被略過了,取而代之的是方法的結果直接從緩存中找到并返回了。
現實中并不總是如此,有些情況下我們希望方法一定會被調用,因為其除了返回一個結果,還做了其他事情,例如記錄日志,調用接口等,這個時候,我們可以用 @CachePut 注釋,這個注釋可以確保方法被執行,同時方法的返回值也被記錄到緩存中。
清單 20. AccountService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Cacheable(value="accountCache")// 使用了一個緩存名叫 accountCache public Account getAccountByName(String userName) { ?? // 方法內部實現不考慮緩存邏輯,直接實現業務 ?? 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 方法查詢一個人 someone 的賬號,這個時候會查詢數據庫一次,但是也記錄到緩存中了。然后我們修改了密碼,調用了 updateAccount 方法,這個時候會執行數據庫的更新操作且記錄到緩存,我們再次修改密碼并調用 updateAccount 方法,然后通過 getAccountByName 方法查詢,這個時候,由于緩存中已經有數據,所以不會查詢數據庫,而是直接返回最新的數據,所以打印的密碼應該是“321”
清單 22. 運行結果
1 2 3 4 | real querying db...someone real updating db...someone real updating db...someone 321 |
和分析的一樣,只查詢了一次數據庫,更新了兩次數據庫,最終的結果是最新的密碼。說明 @CachePut 確實可以保證方法被執行,且結果一定會被緩存。
@Cacheable、@CachePut、@CacheEvict 注釋介紹
通過上面的例子,我們可以看到 spring cache 主要使用兩個注釋標簽,即 @Cacheable、@CachePut 和 @CacheEvict,我們總結一下其作用和配置方法。
表 1. @Cacheable 作用和配置方法
@Cacheable 的作用 | 主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存 | |
---|---|---|
@Cacheable 主要的參數 | ||
value | 緩存的名稱,在 spring 配置文件中定義,必須指定至少一個 | 例如: @Cacheable(value=”mycache”) 或者? @Cacheable(value={”cache1”,”cache2”} |
key | 緩存的 key,可以為空,如果指定要按照 SpEL 表達式編寫,如果不指定,則缺省按照方法的所有參數進行組合 | 例如: @Cacheable(value=”testcache”,key=”#userName”) |
condition | 緩存的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進行緩存 | 例如: @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
表 2. @CachePut 作用和配置方法
@CachePut 的作用 | 主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存,和 @Cacheable 不同的是,它每次都會觸發真實方法的調用 | |
---|---|---|
@CachePut 主要的參數 | ||
value | 緩存的名稱,在 spring 配置文件中定義,必須指定至少一個 | 例如: @Cacheable(value=”mycache”) 或者? @Cacheable(value={”cache1”,”cache2”} |
key | 緩存的 key,可以為空,如果指定要按照 SpEL 表達式編寫,如果不指定,則缺省按照方法的所有參數進行組合 | 例如: @Cacheable(value=”testcache”,key=”#userName”) |
condition | 緩存的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進行緩存 | 例如: @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
表 3. @CacheEvict 作用和配置方法
@CachEvict 的作用 | 主要針對方法配置,能夠根據一定的條件對緩存進行清空 | |
---|---|---|
@CacheEvict 主要的參數 | ||
value | 緩存的名稱,在 spring 配置文件中定義,必須指定至少一個 | 例如: @CachEvict(value=”mycache”) 或者? @CachEvict(value={”cache1”,”cache2”} |
key | 緩存的 key,可以為空,如果指定要按照 SpEL 表達式編寫,如果不指定,則缺省按照方法的所有參數進行組合 | 例如: @CachEvict(value=”testcache”,key=”#userName”) |
condition | 緩存的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才清空緩存 | 例如: @CachEvict(value=”testcache”, condition=”#userName.length()>2”) |
allEntries | 是否清空所有緩存內容,缺省為 false,如果指定為 true,則方法調用后將立即清空所有緩存 | 例如: @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation | 是否在方法執行前就清空,缺省為 false,如果指定為 true,則在方法還沒有執行的時候就清空緩存,缺省情況下,如果方法執行拋出異常,則不會清空緩存 | 例如: @CachEvict(value=”testcache”,beforeInvocation=true) |
基本原理
和 spring 的事務管理類似,spring cache 的關鍵原理就是 spring AOP,通過 spring AOP,其實現了在方法調用前、調用后獲取方法的入參和返回值,進而實現了緩存的邏輯。我們來看一下下面這個圖:
圖 2. 原始方法調用圖
上圖顯示,當客戶端“Calling code”調用一個普通類 Plain Object 的 foo() 方法的時候,是直接作用在 pojo 類自身對象上的,客戶端擁有的是被調用者的直接的引用。
而 Spring cache 利用了 Spring AOP 的動態代理技術,即當客戶端嘗試調用 pojo 的 foo()方法的時候,給他的不是 pojo 自身的引用,而是一個動態生成的代理類
圖 3. 動態代理調用圖
如上圖所示,這個時候,實際客戶端擁有的是一個代理的引用,那么在調用 foo() 方法的時候,會首先調用 proxy 的 foo() 方法,這個時候 proxy 可以整體控制實際的 pojo.foo() 方法的入參和返回值,比如緩存結果,比如直接略過執行實際的 foo() 方法等,都是可以輕松做到的。
擴展性
直到現在,我們已經學會了如何使用開箱即用的 spring cache,這基本能夠滿足一般應用對緩存的需求,但現實總是很復雜,當你的用戶量上去或者性能跟不上,總需要進行擴展,這個時候你或許對其提供的內存緩存不滿意了,因為其不支持高可用性,也不具備持久化數據能力,這個時候,你就需要自定義你的緩存方案了,還好,spring 也想到了這一點。
我們先不考慮如何持久化緩存,畢竟這種第三方的實現方案很多,我們要考慮的是,怎么利用 spring 提供的擴展點實現我們自己的緩存,且在不改原來已有代碼的情況下進行擴展。
首先,我們需要提供一個 CacheManager 接口的實現,這個接口告訴 spring 有哪些 cache 實例,spring 會根據 cache 的名字查找 cache 的實例。另外還需要自己實現 Cache 接口,Cache 接口負責實際的緩存邏輯,例如增加鍵值對、存儲、查詢和清空等。利用 Cache 接口,我們可以對接任何第三方的緩存系統,例如 EHCache、OSCache,甚至一些內存數據庫例如 memcache 或者 h2db 等。下面我舉一個簡單的例子說明如何做。
清單 23. MyCacheManager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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 實際繼承了 spring 內置的 AbstractCacheManager,實際上僅僅管理 MyCache 類的實例。
清單 24. MyCache
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | 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() { ?? } } |
上面的自定義緩存只實現了很簡單的邏輯,但這是我們自己做的,也很令人激動是不是,主要看 get 和 put 方法,其中的 get 方法留了一個后門,即所有的從緩存查詢返回的對象都將其 password 字段設置為一個特殊的值,這樣我們等下就能演示“我們的緩存確實在起作用!”了。
這還不夠,spring 還不知道我們寫了這些東西,需要通過 spring*.xml 配置文件告訴它
清單 25. Spring-cache-anno.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | < 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 實例。
好,什么都不說,測試!
清單 26. Main.java
1 2 3 4 5 6 7 8 9 10 11 | 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()); } |
上面的測試代碼主要是先調用 getAccountByName 進行一次查詢,這會調用數據庫查詢,然后緩存到 mycache 中,然后我打印密碼,應該是空的;下面我再次查詢 someone 的賬號,這個時候會從 mycache 中返回緩存的實例,記得上面的后門么?我們修改了密碼,所以這個時候打印的密碼應該是一個特殊的值
清單 27. 運行結果
1 2 3 | real querying db...someone passwd=null passwd=from mycache:accountCache |
結果符合預期,即第一次查詢數據庫,且密碼為空,第二次打印了一個特殊的密碼。說明我們的 myCache 起作用了。
注意和限制
基于 proxy 的 spring aop 帶來的內部調用問題
上面介紹過 spring cache 的原理,即它是基于動態生成的 proxy 代理機制來對方法的調用進行切面,這里關鍵點是對象的引用問題,如果對象的方法是內部調用(即 this 引用)而不是外部引用,則會導致 proxy 失效,那么我們的切面就失效,也就是說上面定義的各種注釋包括 @Cacheable、@CachePut 和 @CacheEvict 都會失效,我們來演示一下。
清單 28. AccountService.java
1 2 3 4 5 6 7 8 9 | public Account getAccountByName2(String userName) { ?? return this.getAccountByName(userName); } @Cacheable(value="accountCache")// 使用了一個緩存名叫 accountCache public Account getAccountByName(String userName) { ?? // 方法內部實現不考慮緩存邏輯,直接實現業務 ?? return getFromDB(userName); } |
上面我們定義了一個新的方法 getAccountByName2,其自身調用了 getAccountByName 方法,這個時候,發生的是內部調用(this),所以沒有走 proxy,導致 spring cache 失效
清單 29. Main.java
1 2 3 4 5 6 7 8 9 10 | 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. 運行結果
1 2 3 | real querying db...someone real querying db...someone real querying db...someone |
可見,結果是每次都查詢數據庫,緩存沒起作用。要避免這個問題,就是要避免對緩存方法的內部調用,或者避免使用基于 proxy 的 AOP 模式,可以使用基于 aspectJ 的 AOP 模式來解決這個問題。
@CacheEvict 的可靠性問題
我們看到,@CacheEvict 注釋有一個屬性 beforeInvocation,缺省為 false,即缺省情況下,都是在實際的方法執行完成后,才對緩存進行清空操作。期間如果執行方法出現異常,則會導致緩存清空不被執行。我們演示一下
清單 31. AccountService.java
1 2 3 4 | @CacheEvict(value="accountCache",allEntries=true)// 清空 accountCache 緩存 public void reload() { ?? throw new RuntimeException(); } |
注意上面的代碼,我們在 reload 的時候拋出了運行期異常,這會導致清空緩存失敗。
清單 32. Main.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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"); } |
上面的測試代碼先查詢了兩次,然后 reload,然后再查詢一次,結果應該是只有第一次查詢走了數據庫,其他兩次查詢都從緩存,第三次也走緩存因為 reload 失敗了。
清單 33. 運行結果
1 | real querying db...someone |
和預期一樣。那么我們如何避免這個問題呢?我們可以用 @CacheEvict 注釋提供的 beforeInvocation 屬性,將其設置為 true,這樣,在方法執行前我們的緩存就被清空了。可以確保緩存被清空。
清單 34. AccountService.java
1 2 3 4 5 | @CacheEvict(value="accountCache",allEntries=true,beforeInvocation=true) // 清空 accountCache 緩存 public void reload() { ?? throw new RuntimeException(); } |
注意上面的代碼,我們在 @CacheEvict 注釋中加了 beforeInvocation 屬性,確保緩存被清空。
執行相同的測試代碼
清單 35. 運行結果
1 2 | real querying db...someone real querying db...someone |
這樣,第一次和第三次都從數據庫取數據了,緩存清空有效。
非 public 方法問題
和內部調用問題類似,非 public 方法如果想實現基于注釋的緩存,必須采用基于 AspectJ 的 AOP 機制,這里限于篇幅不再細述。
其他技巧
Dummy CacheManager 的配置和作用
有的時候,我們在代碼遷移、調試或者部署的時候,恰好沒有 cache 容器,比如 memcache 還不具備條件,h2db 還沒有裝好等,如果這個時候你想調試代碼,豈不是要瘋掉?這里有一個辦法,在不具備緩存條件的時候,在不改代碼的情況下,禁用緩存。
方法就是修改 spring*.xml 配置文件,設置一個找不到緩存就不做任何操作的標志位,如下
清單 36. Spring-cache-anno.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | < 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 = "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 變為了 simpleCacheManager,且沒有配置 accountCache 實例,后面的 cacheManager 的實例是一個 CompositeCacheManager,他利用了前面的 simpleCacheManager 進行查詢,如果查詢不到,則根據標志位 fallbackToNoOpCache 來判斷是否不做任何緩存操作。
清單 37. 運行結果
1 2 3 | real querying db...someone real querying db...someone real querying db...someone |
可以看出,緩存失效。每次都查詢數據庫。因為我們沒有配置它需要的 accountCache 實例。
如果將上面 xml 配置文件的 fallbackToNoOpCache 設置為 false,再次運行,則會得到
清單 38. 運行結果
1 2 3 4 5 | 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 設置為 true 的情況下,系統會拋出異常。
小結
總之,注釋驅動的 spring cache 能夠極大的減少我們編寫常見緩存的代碼量,通過少量的注釋標簽和配置文件,即可達到使代碼具備緩存的能力。且具備很好的靈活性和擴展性。但是我們也應該看到,spring cache 由于急于 spring AOP 技術,尤其是動態的 proxy 技術,導致其不能很好的支持方法的內部調用或者非 public 方法的緩存設置,當然這都是可以解決的問題,通過學習這個技術,我們能夠認識到,AOP 技術的應用還是很廣泛的,如果有興趣,我相信你也能基于 AOP 實現自己的緩存方案。
?
轉自:https://www.ibm.com/developerworks/cn/opensource/os-cn-spring-cache/