注:本文所用的springBoot版本号为1.5.6.6
在springBoot中同时配置了RedisCache和ehCache,当使用@Cacheable注解时,默认为redisCache,通过指定注解中"cacheManager"的属性值,达到任意切换cache的效果。
由于@Cacheable注解是可以通过参数指定CacheManager或CacheResolver的,因此推断springCache本身支持配置多个CacheManager。
首先尝试直接配置RedisCacheManager和EhCacheCacheManager,结果启动报错:
定位到该行报错代码,原来是在CacheAspectSupport.afterSingletonsInstantiated方法,通过beanFactory获取CacheManager.class的bean时,由于配置了多个bean,抛出了一个NoUniqueBeanDefinitionException异常。
理论上只要在其中一个CacheManager的bean上加上@primary注解就可以解决这个报错了。 但是这样解决看起来是不优雅的。
从代码中看,在CacheAspectSupport.afterSingletonsInstantiated方法中,会先通过getCacheResolver方法尝试获取类中的成员变量cacheResolver,如果没有获取到,才会通过getBean方法尝试获取默认的cacheManager并通过cacheManager创建一个cacheResolver。 也就是说,在此之前是有地方设置默认cacheManager的。
一路查看源代码,最终发现, springCache会在配置类ProxyCachingConfiguration通过@Bean实例化一个CacheInterceptor(CacheAspectSupport的子类),并给interceptor的cacheResolver、cacheManager等成员变量赋值:
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheInterceptor cacheInterceptor() {
CacheInterceptor interceptor = new CacheInterceptor();
interceptor.setCacheOperationSources(cacheOperationSource());
if (this.cacheResolver != null) {
interceptor.setCacheResolver(this.cacheResolver);
}
else if (this.cacheManager != null) {
interceptor.setCacheManager(this.cacheManager);
}
if (this.keyGenerator != null) {
interceptor.setKeyGenerator(this.keyGenerator);
}
if (this.errorHandler != null) {
interceptor.setErrorHandler(this.errorHandler);
}
return interceptor;
}
而用来赋值的cacheManager等,是在ProxyCachingConfiguration的父类,抽象配置类AbstractCachingConfiguration里面获取到默认值的:
@Autowired(required = false)
void setConfigurers(Collection<CachingConfigurer> configurers) {
if (CollectionUtils.isEmpty(configurers)) {
return;
}
if (configurers.size() > 1) {
throw new IllegalStateException(configurers.size() + " implementations of " +
"CachingConfigurer were found when only 1 was expected. " +
"Refactor the configuration such that CachingConfigurer is " +
"implemented only once or not at all.");
}
CachingConfigurer configurer = configurers.iterator().next();
useCachingConfigurer(configurer);
}
/**
* Extract the configuration from the nominated {@link CachingConfigurer}.
*/
protected void useCachingConfigurer(CachingConfigurer config) {
this.cacheManager = config.cacheManager();
this.cacheResolver = config.cacheResolver();
this.keyGenerator = config.keyGenerator();
this.errorHandler = config.errorHandler();
}
因此,只需实现CachingConfigurer配置接口编写配置类,并重写其cacheManager方法提供默认的cacheManager就可以指定默认的cacheManager了。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>net.sf.ehcachegroupId>
<artifactId>ehcacheartifactId>
<version>2.10.4version>
dependency>
spring:
redis:
# Connection URL, will override host, port and password (user will be ignored), e.g. redis://user:[email protected]:6379
url: redis://localhost:6379
pool:
maxActive: 8 # 连接池最大连接数(使用负值表示没有限制)
maxWait: 800 # 连接池最大阻塞等待时间(使用负值表示没有限制)
maxIdle: 8 # 连接池中的最大空闲连接
minIdle: 2 # 连接池中的最小空闲连接
timeout: 2000 # 连接或读取超时时长(毫秒)
cache:
type: ehcache
ehcache:
config: ehcache.xml
<ehcache>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="10"
timeToLiveSeconds="20"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"/>
<cache name="cp_salary:cache:10m"
maxElementsInMemory="100000"
eternal="false"
overflowToDisk="false"
timeToIdleSeconds="10"
timeToLiveSeconds="600"/>
<cache name="cp_salary:cache:20m"
maxElementsInMemory="100000"
eternal="false"
overflowToDisk="false"
timeToIdleSeconds="10"
timeToLiveSeconds="1200"/>
<cache name="cp_salary:cache:30m"
maxElementsInMemory="100000"
eternal="false"
overflowToDisk="false"
timeToIdleSeconds="10"
timeToLiveSeconds="1800"/>
ehcache>
package com.lianjia.sh.salary.payroll.config;
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import com.google.common.collect.ImmutableMap;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author Stephen.Shi
* @summary CacheManager配置
* @since 2018/9/9
*/
@Configuration
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)
public class CacheManagerConfig {
private final CacheProperties cacheProperties;
CacheManagerConfig(CacheProperties cacheProperties) {
this.cacheProperties = cacheProperties;
}
/**
* cacheManager名字
*/
public interface CacheManagerNames {
/**
* redis
*/
String REDIS_CACHE_MANAGER = "redisCacheManager";
/**
* ehCache
*/
String EHCACHE_CACHE_MAANGER = "ehCacheCacheManager";
}
/**
* 缓存名,名称暗示了缓存时长 注意: 如果添加了新的缓存名,需要同时在下面的RedisCacheCustomizer#RedisCacheCustomizer里配置名称对应的缓存时长
* ,时长为0代表永不过期;缓存名最好公司内部唯一,因为可能多个项目共用一个redis。
*
* @see RedisCacheCustomizer#customize(RedisCacheManager)
*/
public interface CacheNames {
/** 15分钟缓存组 */
String CACHE_15MINS = "cp_salary:cache:15m";
/** 30分钟缓存组 */
String CACHE_30MINS = "cp_salary:cache:30m";
/** 60分钟缓存组 */
String CACHE_60MINS = "cp_salary:cache:60m";
/** 180分钟缓存组 */
String CACHE_180MINS = "cp_salary:cache:180m";
}
/**
* ehcache缓存名
*/
public interface EhCacheNames {
String CACHE_10MINS = "cp_salary:cache:10m";
String CACHE_20MINS = "cp_salary:cache:20m";
String CACHE_30MINS = "cp_salary:cache:30m";
}
@Bean
public RedisTemplate redisTemplate(JedisConnectionFactory jedisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(jedisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericFastJsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 默认的redisCacheManager
* @param redisTemplate 通过参数注入,这里没有手动给它做配置。在引入了redis的jar包,并且往
* application.yml里添加了spring.redis的配置项,springboot的autoconfig会自动生成一个
* redisTemplate的bean
* @return
*/
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate<Object, Object> redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
cacheManager.setUsePrefix(true);
this.redisCacheManagerCustomizer().customize(cacheManager);
return cacheManager;
}
/** cache的一些自定义配置 */
@Bean
public RedisCacheCustomizer redisCacheManagerCustomizer() {
return new RedisCacheCustomizer();
}
private static class RedisCacheCustomizer
implements CacheManagerCustomizer<RedisCacheManager> {
/** CacheManager缓存自定义初始化比较早,尽量不要@autowired 其他spring 组件 */
@Override
public void customize(RedisCacheManager cacheManager) {
// 自定义缓存名对应的过期时间
Map<String, Long> expires = ImmutableMap.<String, Long>builder()
.put(CacheNames.CACHE_15MINS, TimeUnit.MINUTES.toSeconds(15))
.put(CacheNames.CACHE_30MINS, TimeUnit.MINUTES.toSeconds(30))
.put(CacheNames.CACHE_60MINS, TimeUnit.MINUTES.toSeconds(60))
.put(CacheNames.CACHE_180MINS, TimeUnit.MINUTES.toSeconds(180)).build();
// spring cache是根据cache name查找缓存过期时长的,如果找不到,则使用默认值
cacheManager.setDefaultExpiration(TimeUnit.MINUTES.toSeconds(30));
cacheManager.setExpires(expires);
}
}
/**
* 创建ehCacheCacheManager
*/
@Bean
public EhCacheCacheManager ehCacheCacheManager() {
Resource location = this.cacheProperties
.resolveConfigLocation(this.cacheProperties.getEhcache().getConfig());
return new EhCacheCacheManager(EhCacheManagerUtils.buildCacheManager(location));
}
}
package com.lianjia.sh.salary.payroll.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.SimpleCacheErrorHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
/**
* Spring cache的一些配置,建议组件相关配置都放在相应的configuration类中
*
* @author
*/
@Configuration
@ConditionalOnBean(RedisCacheManager.class)
public class CacheConfig extends CachingConfigurerSupport {
@Autowired
private RedisCacheManager redisCacheManager;
/**
* 重写这个方法,目的是用以提供默认的cacheManager
* @author Stephen.Shi
* @return
*/
@Override
public CacheManager cacheManager() {
return redisCacheManager;
}
/** 如果cache出错, 我们会记录在日志里,方便排查,比如反序列化异常 */
@Override
public CacheErrorHandler errorHandler() {
return new LoggingCacheErrorHandler();
}
/* non-public */ static class LoggingCacheErrorHandler extends SimpleCacheErrorHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
logger.error(String.format("cacheName:%s,cacheKey:%s",
cache == null ? "unknown" : cache.getName(), key), exception);
super.handleCacheGetError(exception, cache, key);
}
@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key,
Object value) {
logger.error(String.format("cacheName:%s,cacheKey:%s",
cache == null ? "unknown" : cache.getName(), key), exception);
super.handleCachePutError(exception, cache, key, value);
}
@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
logger.error(String.format("cacheName:%s,cacheKey:%s",
cache == null ? "unknown" : cache.getName(), key), exception);
super.handleCacheEvictError(exception, cache, key);
}
@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
logger.error(String.format("cacheName:%s", cache == null ? "unknown" : cache.getName()),
exception);
super.handleCacheClearError(exception, cache);
}
}
}
package com.lianjia.sh.salary.payroll.service;
import com.lianjia.sh.salary.payroll.config.CacheManagerConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
/**
* @author shitiecheng
* @version v1
* @since 2018/7/31
*/
@Service
public class CacheDemoService {
@Cacheable(key = "key", cacheManager = CacheManagerConfig.CacheManagerNames.EHCACHE_CACHE_MAANGER, cacheNames = CacheManagerConfig.EhCacheNames.CACHE_10MINS)
public String demo(String key) {
return "abc" + key;
}
@Cacheable(key = "key", cacheNames = CacheManagerConfig.CacheNames.CACHE_15MINS)
public String demo2(String key) {
return "abc" + key;
}
}
结果: