用注解解决业务逻辑和缓存逻辑的深度耦合
介绍
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");
}
GitHub地址
https://github.com/erlieStar/spring-boot-cache-demo
参考资料
https://www.ibm.com/developerworks/cn/opensource/os-cn-spring-cache/
文章转载自公众号:Java识堂