程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 注釋驅動的Spring cache緩存介紹

注釋驅動的Spring cache緩存介紹

編輯:關於JAVA

概述

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")// 使用了一個緩存名叫 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) 沒有緩存,每次都查詢數據庫。

如果有多個參數,如何進行 key 的組合

假設 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 確實 可以保證方法被執行,且結果一定會被緩存。

@Cacheable、@CachePut、@CacheEvict 注釋介紹

通過上面的 例子,我們可以看到 spring cache 主要使用兩個注釋標簽,即 @Cacheable、@CachePut 和 @CacheEvict,我們總結一下 其作用和配置方法。

表 1. @Cacheable 作用和配置方法

基本原理

和 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 起作用了。

注意和限制

基於 proxy 的 spring aop 帶來的內部調用問題

上面介紹過 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 的可靠性問題

我們看到,@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 方法問題

和內部調用問題類似,非 public 方法如果想實現基於注釋的緩存,必須采用基於 AspectJ 的 AOP 機制,這裡限 於篇幅不再細述。

其他技巧

Dummy CacheManager 的配置和作用

有的時候,我們在代碼遷移、調試或 者部署的時候,恰好沒有 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 實現自己的緩存方案。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved