虽然以前实现缓存的方式,是定义了缓存操作接口,可以灵活实现不同的缓存,可毕竟精力有限,要完成不同的缓存实现也是件麻烦的事。更要命的是,业务代码中有大量缓存操作的代码,耦合度太高,看着很不优雅。
所以呢,抽空了解了一下其它实现方案。这不,spring3.1开始,支持基于注解的缓存,算是目前我比较可以接受的一种方案吧。学完之后还是做一下笔记吧。
spring cache是一套基于注解实现的缓存技术,其本身是并不是具体实现,不过默认实现了ConcurrentMap和EHCache实现的缓存。当然也是支持其它缓存的。
spring cache有哪些特性:
1.通过少量的配置 annotation 注解即可使得既有代码支持缓存 (非常节省开发时间)
2.支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存(位于spring-context包中,spring web项目都会引用这个包)
3.支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition (支持SpEL语法)
4.支持 AspectJ,并通过其实现任何方法的缓存支持 (默认基于AOP方案,采用AspectJ会更灵活,下文有介绍)
5.支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性(如果SpEL达不到你的预期,勇敢实现自己的KeyGenerator吧)
6.支持各种缓存实现,默认是基于ConcurrentMap实现的ConcurrentMapCache,同时支持ehcache实现。若要使用redis等缓存,引入redis的实现包即可。
有哪些遗憾呢?在我考虑的场景下,还有下面的一些遗憾:
1.不支持TTL,也就是不能设置expires time。这点挺遗憾的,spring-cache认为这是各个cache实现自己去完成的事情,有方案但是只能设置统一的过期时间,这明显不够灵活。比如用户的抽奖次数、有效期等业务,当天有效,或者3天、一周有效,我们倾向于设置缓存有效期解决这个问题,而spring-cache却无法完成。
2.无法根据查询结果中的内容生成缓存key,比如getUser(uid)方法,想通过查询出来的user.email生成缓存key就无法实现了。
3.调试起来麻烦
先贴一段以前的代码,看看以前是怎么做的:
/**
* 先取cache。如果没有,从DB取,再存cache
*/
@Override
public UserDetail getUserDetail(int userId) {
Result<String> info = cacheManager.get(UCConstants.nameSpace,
UCUtil.getKey(UCConstants.USER_DETAIL_UID_KEY, userId));
if (StringUtils.isNotEmpty(info.getEntity())) {
UserDetail userDetail = JSONUtils.fromJSON(info.getEntity(), UserDetail.class);
if (null != userDetail) {
return userDetail;
}
}
UserDetail userDetail = userDetailDAO.getUserDetail(userId);
if (userDetail != null) {
if (logger.isDebugEnabled()) {
logger.info("getUserDetail from db userDetail=" + userDetail.toString());
}
addToCache(userDetail);
return userDetail;
}
return null;
}
private void addToCache(UserDetail userDetail) {
cacheManager.put(UCConstants.nameSpace,
UCUtil.getKey(UCConstants.USER_DETAIL_UID_KEY, userDetail.getId()),
JSONUtils.toJSON(userDetail), UCConstants.USER_DETAIL_CACHE_TIME);
//节约空间,存id吧。多查一次吧
cacheManager.put(UCConstants.nameSpace,
UCUtil.getKey(UCConstants.USER_DETAIL_NAME_KEY, userDetail.getUserNickName()),
userDetail.getId() + "", UCConstants.USER_DETAIL_CACHE_TIME);
}
那采用注解驱动的spring-cache之后代码怎么写的:
@Cacheable(key="#uid", value = "userCache")
public UserDetail getUserDetail(int uid){
return userDetailMapper.getUser(uid);
}
当然,以前的代码写了两个缓存key,这点通过@Caching注解也是可以实现的,示例如下:
@Caching
(evict={@CacheEvict(value="userCache",key="#user.uid"),
@CacheEvict(value="userCache",key="#user.email")})
看了以上的代码,觉得用注解驱动的cache是不是很过瘾?代码简介,低耦合。简直是广大程序猿的福音。要像上面那样写代码,其实也挺容易的。下面就从头开始吧。
来,先把配置弄起。spring这点就很不爽,什么都要弄个配置。不过也正是基于配置+注解的方式,使得我们脱离了不断去new对象的苦海。
我的Cache是不打算使用ConcurrentMapCache的,所以我就直接拿EHCache来做示例好了。在使用EHCache之前,我们需要引入ehcache的包,以及配置ehcache.xml文件。
当然,在线上生产环境中,还是不建议只用ehcache这种方式。可以采用ehcache+redis,或者redis的方案都可以。
Maven的pom.xml文件配置:
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.9</version>
</dependency>
ehcache的配置,位于classpath下的ehcache.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false">
<diskStore path="D:/cache" /> <!-- 缓存存放目录(此目录为放入系统默认缓存目录),也可以是”D:/cache“ java.io.tmpdir -->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
maxElementsOnDisk="10000000"
diskPersistent="true"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>
<cache name="userCache"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
maxElementsOnDisk="10000000"
diskPersistent="true"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>
</ehcache>
这里说明一下,为什么两个cache呢,用一个不好吗?
我最早的时候是一个defaultCache,没有userCache。后来发现spring-cache配置的EhCacheCacheManager总是load失败,报错误:
loadCaches must not return an empty Collection
我很纳闷,这不是有一个defaultCache吗?于是我跟踪代码,在AbstractCacheManager源码的afterPropertiesSet方法中有如下代码:
public void afterPropertiesSet() {
Collection<? extends Cache> caches = loadCaches();
Assert.notEmpty(caches, "loadCaches must not return an empty Collection");
this.cacheMap.clear();
// preserve the initial order of the cache names
for (Cache cache : caches) {
this.cacheMap.put(cache.getName(), cache);
this.cacheNames.add(cache.getName());
}
}
这里取到的caches居然为empty!确实我也不知道为什么会这样。我当时尝试增加了一个名为default的cache配置,结果ehcache又报错,提示已经有名为default的cache了。于是将name改为userCache,问题解决。
从ehcache的报错来看,ehcache应该配置了一个名为default的cache,但不知道为什么spring-cache认不到。知道的同学可以告诉下我。
配置好ehcache后,就该配置我们的spring-cache了,为了方便管理,我倾向于配置独立的spring-cache.xml文件放在spring的配置目录下。内容如下:
<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 cache-manager="ehCacheManager"/>
<!-- 缓存 属性-->
<bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache.xml"/>
</bean>
<!-- generic cache manager -->
<bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="ehCacheManagerFactory"/>
</bean>
</beans>
当然,如果你想在没有缓存的环境中不做任何代码上的修改(比如环境迁移、临时测试等),即可简单的切换,那也是OK的。
又或者,你的环境中既有ehcache,又有redis,还有ConcurrentMapCache,那也是可以的。
上面说到的亮点,你可以用CompositeCacheManager完成。
需要重新配置以下的cacheManager:
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
<property name="cacheManagers">
<list>
<ref bean="ehCacheManager"/>
<ref bean="otherCachaManager"/>
</list>
</property>
<property name="fallbackToNoOpCache" value="true"/>
</bean>
fallbackToNoOpCache参数决定了在没有Cachhe的情况下会出现什么现象。如果为true,则会直接忽略掉缓存,可能进入db查询;如果为false(默认为false),则在无缓存时会抛出异常:
Cannot find cache named [userCache] for CacheableOperation
配置好了,那么该干正事了。spring-cache的使用非常简单,只会用简单的几个注解即可。那么,有哪些注解呢?看看下表:
Spring Cache配置 |
JSR-107规范 |
描述 |
@Cacheable |
@CacheResult |
缓存方法返回的结果,有三个参数,分别是value(缓存名称)、key(缓存key)、condition(缓存条件) |
@CachePut |
@CachePut |
缓存方法返回的结果,并且会在方法被调用的时候执行。参数同Cacheable |
@CacheEvict |
@CacheRemove |
清除缓存。有五个参数,除过上面的3个外,还有2个:allEntries(是否清除所有缓存)、beforeInvocation(是否在方法调用前就清除,默认为false,因此当方法抛出异常则缓存不会被清掉) |
@CacheEvict(allEntries=true) |
@CacheRemoveAll |
清除所有缓存 |
@CacheConfig |
@CacheDefaults |
在类级别上提供一些公共配置,比如value值,每个方法都一样,就只需要在class上配置一次就好了,这个属性很有用,可惜spring3.1不支持。 |
简单解释一下,上面的表示官方文档中的内容。左边是spring3.1关于Cache操作的注解;中间是JSR-107规范的注解,spring在4.1版本实现;右边是解释。我就懒得翻译了。
各个注解的作用与配置方法也有作者写的不错,我就直接拿来用了:
@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-cache的cache配置。cache元素配置除过cache-manager属性外,还有许多属性,简单罗列如下:
XML属性 |
注解属性 |
默认值 |
含义 |
cache-manager |
无 |
cacheManager |
默认的cacheManager名称。一个默认的CacheResolver在cacheManager的后台被初始化,要想更精细的管理缓存可以考虑设置cache-resolver属性 |
cache-resolver |
无 |
SimpleCacheResolver |
CacheResolver的bean名称,这个非必需属性,仅仅作为cache-manager属性的替代 |
key-generator |
无 |
SimpleKeyGenerator |
自定义的 key generator |
error-handler |
无 |
SimpleCacheErrorHandler |
自定义的Cache Error handler,默认情况下,异常会直接抛出给客户端 |
mode |
mode |
proxy |
spring cache默认使用了spring的AOP框架来通过proxy的方式处理注解,另一种可代替的方式就是aspectj。通过Spring AOP方式的cache注解,无法在内部调用的时候被proxy,因此也就在内部调用的时候缓存注解会失效,而aspectj的AOP则可以解决这个问题。 |
proxy-target-class |
proxyTargetClass |
false |
仅适用于proxy模式,控制类上的注解@Cacheable或@CacheEvict采用哪种缓存代理。如果proxy-target-class属性设置为true,那么将创建基于类的代理。 如果proxy-target-class为false,那么将创建基于标准JDK接口的代理。具体可以参考AOP的proxy-target-class属性 |
order |
order |
Ordered.LOWEST_PRECEDENCE |
确定bean注解中@Cacheable或@CacheEvict中cache advice的顺序,没有指定则意味着使用AOP决定的advice顺序。具体可以参考A |