用註解解決業務邏輯和快取邏輯的深度耦合
- 2019 年 10 月 5 日
- 筆記

介紹
spring3.1引入了基於註解的快取技術,即spring cache模組,它不是一個具體的快取實現方案,而是一個對快取使用的抽象。你可以類比為JDBC,定義了一系列快取操作的介面,由具體的快取來實現,如Ehcache,Redis等。
演示一下我們一般是怎麼操作快取的
實體類
@Data @NoArgsConstructor public class Account { private int id; private String name; private String password; public Account(String name) { this.name = name; } }
快取類,這個簡單用ConcurrentHashMap來演示一下
public class Cache<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); } // 根據 key 來刪除快取中的一條記錄 public void evictCache(String key) { if(cache.containsKey(key)) { cache.remove(key); } } // 清空快取中的所有記錄 public void evictCache() { cache.clear(); } }
服務類
public class AccountService { private Cache<Account> cache = new Cache<>(); public Account getAccountByName(String name) { Account result = cache.getValue(name); // 如果在快取中,則直接返回快取的結果 if (result != null) { System.out.println("get from cache " + name); return result; } result = getFromDB(name); // 將資料庫查詢的結果更新到快取中 if (result != null) { cache.addOrUpdateCache(name, result); } return result; } private Account getFromDB(String name) { System.out.println("get from db " + name); return new Account(name); } }
測試類
public class Main { public static void main(String[] args) { /** * get from db aaa * get from cache aaa */ AccountService s = new AccountService(); s.getAccountByName("aaa"); s.getAccountByName("aaa"); } }
結果和我們預想的一樣,第一次從資料庫中拿,第二次就從快取中拿
這樣寫有什麼問題呢?
1.快取程式碼和業務程式碼耦合度太高 2.目前快取存儲這塊寫的比較死,不能靈活的切換為第三方模組,當然你可以再抽象一層。
我們用Spring Cache改造一下上面的程式碼
在Spring Boot中使用Spring Chache
1.在pom文件中加入依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
2.在配置類中加@EnableCaching註解
@SpringBootApplication @EnableCaching public class CacheDemoApplication { public static void main(String[] args) { SpringApplication.run(CacheDemoApplication.class, args); } }
以@Enable開頭的這種類型的註解在Spring Boot和Spring Cloud項目中還是經常出現的,這種類型的註解是一個開關註解,對於調試還是非常方便的。比如調試中你不想用快取,你就可以把這個註解注釋掉。這樣快取相關的註解都不能使用了。
服務類改成如下形式
@Service public class AccountService { @Cacheable(value = "cache", key = "#name") public Account getAccountByName(String name) { return getFromDB(name); } @Cacheable(value = "cache1", key = "#name") public Account getAccountByNameCache1(String name) { return getFromDB(name); } @Cacheable(value = "cache2", key = "#name") public Account getAccountByNameCache2(String name) { return getFromDB(name); } private Account getFromDB(String name) { System.out.println("get from db " + name); return new Account(name); } }
現在你只看getAccountByName方法
@Test public void test0() { /** * get from db 1 * get from db 2 */ accountService.getAccountByName("1"); accountService.getAccountByName("1"); accountService.getAccountByName("2"); accountService.getAccountByName("2"); }
對於不同的key,第一次走資料庫,第二次走快取。完全符合我們的預期,而我們只用了一個註解。
spring cache的整體設計就類似下面這個map
Map<String, Map<String, String>> cacheManager;
最外層的map是快取的名字(因為有可能有多個快取),裡面的map是快取的key和快取的value。
最外層的map對應spring cache的CacheManager介面(管理多個快取),實現類有EhCacheCacheManager和ConcurrentMapCacheManager等
裡面的map對應spring cache的Cache介面(定義快取的具體操作,如put和get等),實現類有EhCacheCache和ConcurrentMapCache等
Spring cache默認使用的是ConcurrentMapCacheManager,即把快取數據放在ConcurrentHashMap中。所以如果你想使用第三方快取只要注入對應的CacheManager實現類和Cache實現類就行,或者你自己寫實現類
接著來說上面用到的註解
@Cacheable(value = "cache", key = "#name")
@Cacheable的value值為cache的名字,key為快取的key(使用SpEL表達式),快取的key有多種指定方式,我這裡只按照name進行了快取。實際的key有多個組合方式,還能設定key的快取條件。更多使用方式看最後的參考資料。
所以現在你明白@Cacheable的作用了把,如果能從快取中取到值,就從快取中取,否則就從資料庫中取,如果取到值,再把值放到快取中。背後的原理就是Spring Aop我就不深入解釋了。
當初我為了驗證我的想法,對getAccountByName寫了好幾個,就是@Cacheable中的value不同。
用來測試@Cacheable的value值是指定快取的名字
@Test public void test1() { /** * get from db 1 * get from db 1 */ accountService.getAccountByNameCache1("1"); accountService.getAccountByNameCache2("1"); }
列印所有快取的名字
@Autowired CacheManager cacheManager; @Test public void test3() { /** * get from db 1 * get from db 1 * cache2 * cache1 */ accountService.getAccountByNameCache1("1"); accountService.getAccountByNameCache2("1"); Collection<String> collection = cacheManager.getCacheNames(); collection.forEach(item ->{ System.out.println(item); }); }
我再演示一下其他註解的使用
@Service @CacheConfig(cacheNames = "cache") public class AccountService1 { @Cacheable(key = "#name") public Account getAccountByName(String name) { return getFromDB(name); } @CachePut(key = "#account.getName()") public Account updateAccount(Account account) { return account; } @CacheEvict(key = "#name") public void deleteAccount(String name) { } private Account getFromDB(String name) { System.out.println("get from db " + name); return new Account(name); } }
在前面的示例中,我們用到了@Cacheable註解,每次都得指明value屬性,即快取的名字。如果不想每次指定快取的名字,就可以用@CacheConfig註解在類上統一指定一個快取的名字。
@CachePut能夠根據方法的請求參數對其結果進行快取,和 @Cacheable 不同的是,它每次都會觸發真實方法的調用,所以這個註解經常用在更新操作上
@CacheEvict根據條件對快取清空,所以一般用在刪除方法上
@Test public void test6() { /** * get from db aaa * get from db aaa */ accountService1.getAccountByName("aaa"); accountService1.deleteAccount("aaa"); accountService1.getAccountByName("aaa"); }