Spring 3.1 引入了激動人心的基於註釋(annotation)的緩存(cache)技術,它本質上不是一個具體的緩存實現方案(例如 EHCache 或者 OSCache),而是一個對緩存使用的抽象,經過在既有代碼中添加少許它定義的各類 annotation,即可以達到緩存方法的返回對象的效果。java
Spring 的緩存技術還具有至關的靈活性,不只可以使用 SpEL(Spring Expression Language)來定義緩存的 key 和各類 condition,還提供開箱即用的緩存臨時存儲方案,也支持和主流的專業緩存例如 EHCache 集成。spring
其特色總結以下:數據庫
本文將針對上述特色對 Spring cache 進行詳細的介紹,主要經過一個簡單的例子和原理介紹展開,而後咱們將一塊兒看一個比較實際的緩存例子,最後會介紹 spring cache 的使用限制和注意事項。OK,Let ’ s begin!緩存
這裏先展現一個徹底自定義的緩存實現,即不用任何第三方的組件來實現某種對象的內存緩存。app
場景是:對一個帳號查詢方法作緩存,以帳號名稱爲 key,帳號對象爲 value,當以相同的帳號名稱查詢帳號的時候,直接從緩存中返回結果,不然更新緩存。帳號查詢服務還支持 reload 緩存(即清空緩存)。ide
首先定義一個實體類:帳號類,具有基本的 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// 從緩存加載
能夠看出咱們的緩存起效了,可是這種自定義的緩存方案有以下劣勢:
若是你的代碼中有上述代碼的影子,那麼你能夠考慮按照下面的介紹來優化一下你的代碼結構了,也能夠說是簡化,你會發現,你的代碼會變得優雅的多!
本 Hello World 相似於其餘任何的 Hello World 程序,從最簡單實用的角度展示 spring cache 的魅力,它基於剛纔自定義緩存方案的實體類 Account.java,從新定義了 AccountService.java 和測試類 Main.java(注意這個例子不用本身定義緩存管理器,由於 spring 已經提供了缺省實現)
爲了實用 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")// 使用了一個緩存名叫 accountCache public 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/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>
注意這個 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. 執行結果
first query...real query account.somebody// 第一次查詢 real querying db...somebody// 對數據庫進行了查詢 second query...// 第二次查詢,沒有打印數據庫查詢日誌,直接返回了緩存中的結果
能夠看出咱們設置的基於註釋的緩存起做用了,而在 AccountService.java 的代碼中,咱們沒有看到任何的緩存邏輯代碼,只有一行註釋:@Cacheable(value="accountCache"),就實現了基本的緩存方案,是否是很強大?
好,到目前爲止,咱們的 spring cache 緩存程序已經運行成功了,可是還不完美,由於還缺乏一個重要的緩存管理邏輯:清空緩存,當帳號數據發生變動,那麼必需要清空某個緩存,另外還須要按期的清空全部緩存,以保證緩存數據的可靠性。
爲了加入清空緩存的邏輯,咱們只要對 AccountService.java 進行修改,從業務邏輯的角度上看,它有兩個須要清空緩存的地方
清單 10. AccountService.java
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
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. 運行結果
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 方法修訂,支持條件)
@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. 測試方法
s.getAccountByName("somebody");// 長度大於 4,不會被緩存 s.getAccountByName("sbd");// 長度小於 4,會被緩存 s.getAccountByName("somebody");// 仍是查詢數據庫 s.getAccountByName("sbd");// 會從緩存返回
清單 15. 運行結果
real querying db...somebody real querying db...sbd real querying db...somebody
可見對長度大於 4 的帳號名 (somebody) 沒有緩存,每次都查詢數據庫。
假設 AccountService 如今有一個需求,要求根據帳號名、密碼和是否發送日誌查詢帳號信息,很明顯,這裏咱們須要根據帳號名、密碼對帳號對象進行緩存,而第三個參數「是否發送日誌」對緩存沒有任何影響。因此,咱們能夠利用 SpEL 表達式對緩存 key 進行設計
清單 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) { // 方法內部實現不考慮緩存邏輯,直接實現業務 return getFromDB(userName,password); }
注意上面的 key 屬性,其中引用了方法的兩個參數 userName 和 password,而 sendLog 屬性沒有考慮,由於其對緩存沒有影響。
清單 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);// 應該查詢數據庫 s.getAccount("somebody", "123456", true);// 應該走緩存 s.getAccount("somebody", "123456", false);// 應該走緩存 s.getAccount("somebody", "654321", true);// 應該查詢數據庫 s.getAccount("somebody", "654321", true);// 應該走緩存 }
上述測試,是採用了相同的帳號,不一樣的密碼組合進行查詢,那麼一共有兩種組合狀況,因此針對數據庫的查詢應該只有兩次。
清單 19. 運行結果
real querying db...userName=somebody password=123456 real querying db...userName=somebody password=654321
和咱們預期的一致。
根據前面的例子,咱們知道,若是使用了 @Cacheable 註釋,則當重複使用相同參數調用方法的時候,方法自己不會被調用執行,即方法自己被略過了,取而代之的是方法的結果直接從緩存中找到並返回了。
現實中並不老是如此,有些狀況下咱們但願方法必定會被調用,由於其除了返回一個結果,還作了其餘事情,例如記錄日誌,調用接口等,這個時候,咱們能夠用 @CachePut 註釋,這個註釋能夠確保方法被執行,同時方法的返回值也被記錄到緩存中。
清單 20. AccountService.java
@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
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. 運行結果
real querying db...someone real updating db...someone real updating db...someone 321
和分析的同樣,只查詢了一次數據庫,更新了兩次數據庫,最終的結果是最新的密碼。說明 @CachePut 確實能夠保證方法被執行,且結果必定會被緩存。
經過上面的例子,咱們能夠看到 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
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
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
<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
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. 運行結果
real querying db...someone passwd=null passwd=from mycache:accountCache
結果符合預期,即第一次查詢數據庫,且密碼爲空,第二次打印了一個特殊的密碼。說明咱們的 myCache 起做用了。
上面介紹過 spring cache 的原理,即它是基於動態生成的 proxy 代理機制來對方法的調用進行切面,這裏關鍵點是對象的引用問題,若是對象的方法是內部調用(即 this 引用)而不是外部引用,則會致使 proxy 失效,那麼咱們的切面就失效,也就是說上面定義的各類註釋包括 @Cacheable、@CachePut 和 @CacheEvict 都會失效,咱們來演示一下。
清單 28. AccountService.java
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
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. 運行結果
real querying db...someone real querying db...someone real querying db...someone
可見,結果是每次都查詢數據庫,緩存沒起做用。要避免這個問題,就是要避免對緩存方法的內部調用,或者避免使用基於 proxy 的 AOP 模式,可使用基於 aspectJ 的 AOP 模式來解決這個問題。
咱們看到,@CacheEvict 註釋有一個屬性 beforeInvocation,缺省爲 false,即缺省狀況下,都是在實際的方法執行完成後,纔對緩存進行清空操做。期間若是執行方法出現異常,則會致使緩存清空不被執行。咱們演示一下
清單 31. AccountService.java
@CacheEvict(value="accountCache",allEntries=true)// 清空 accountCache 緩存 public void reload() { throw new RuntimeException(); }
注意上面的代碼,咱們在 reload 的時候拋出了運行期異常,這會致使清空緩存失敗。
清單 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"); }
上面的測試代碼先查詢了兩次,而後 reload,而後再查詢一次,結果應該是隻有第一次查詢走了數據庫,其餘兩次查詢都從緩存,第三次也走緩存由於 reload 失敗了。
清單 33. 運行結果
real querying db...someone
和預期同樣。那麼咱們如何避免這個問題呢?咱們能夠用 @CacheEvict 註釋提供的 beforeInvocation 屬性,將其設置爲 true,這樣,在方法執行前咱們的緩存就被清空了。能夠確保緩存被清空。
清單 34. AccountService.java
@CacheEvict(value="accountCache",allEntries=true,beforeInvocation=true) // 清空 accountCache 緩存 public void reload() { throw new RuntimeException(); }
注意上面的代碼,咱們在 @CacheEvict 註釋中加了 beforeInvocation 屬性,確保緩存被清空。
執行相同的測試代碼
清單 35. 運行結果
real querying db...someone real querying db...someone
這樣,第一次和第三次都從數據庫取數據了,緩存清空有效。
和內部調用問題相似,非 public 方法若是想實現基於註釋的緩存,必須採用基於 AspectJ 的 AOP 機制,這裏限於篇幅再也不細述。
有的時候,咱們在代碼遷移、調試或者部署的時候,剛好沒有 cache 容器,好比 memcache 還不具有條件,h2db 尚未裝好等,若是這個時候你想調試代碼,豈不是要瘋掉?這裏有一個辦法,在不具有緩存條件的時候,在不改代碼的狀況下,禁用緩存。
方法就是修改 spring*.xml 配置文件,設置一個找不到緩存就不作任何操做的標誌位,以下
清單 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/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. 運行結果
real querying db...someone real querying db...someone real querying db...someone
能夠看出,緩存失效。每次都查詢數據庫。由於咱們沒有配置它須要的 accountCache 實例。
若是將上面 xml 配置文件的 fallbackToNoOpCache 設置爲 false,再次運行,則會獲得
清單 38. 運行結果
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 實現本身的緩存方案。