最近在搞产品相关的缓存整合,由于spring 的强大的生态,决定使用Spring cache来扩展缓存相关功能,但是由于产品相关需求,要求产品内部使用一种缓存机制,给客户提供spring cache原生的缓存扩展。比如产品本身默认使用 Caffeine ,而客户可以自己选择 使用redis缓存,ehcache缓存或者同样使用 Caffeine缓存。这就面临了一个问题,目前的版本Spring Cache本身是不支持多缓存机制配置的,默认Spring Cache只能有一个缓存管理也就是CacheManager。因此要实现这种需求,最少需要两个CacheManager去实现,一个是给产品内部使用的,一个是客户自定义使用Spring Cache配置的,因此对Spring Cache简单的源码实现看了一下,整理了实现方案希望对其他开发的小伙伴提供思路和解决方案。
关于Spring Cache的概念和拆箱使用的方式就不多做介绍了,网上一大堆,可以自行查看
Spring Cache自动装配机制
熟悉SpringBoot机制的小伙伴对SpringBoot的自动装配肯定是很熟悉了,如果不熟悉的话,那么可以看我之前的文章了解一下,这里就不多做解释了,我们先来看一下 org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
这个类, 我们可以从
spring-boot-autoconfigure-x.x.x.jar中的META-INF\spring.factories 看到自动装配了这个类。
这个类是Spring Cache抽象缓存实现的基础,我们简单的看一下,不必深入了解。
接下来我们重点看一下CacheConfigurationImportSelector
这个类,这个类主要就是筛选出实现抽象层的实际的CacheManager装配并进行自动装配,该自动装配的方式属于接口编程方式 可以看一下我这篇文章 Spring Framework 手动装配 笔记(一)。
/**
* {@link ImportSelector} to add {@link CacheType} configuration classes.
*/
static class CacheConfigurationImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
CacheType[] types = CacheType.values(); //枚举类,代表了使用的缓存类型,对应的是下面的装配类的筛选
String[] imports = new String[types.length];
for (int i = 0; i < types.length; i++) {
//装配类筛选,对应上面的CacheType[]
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
return imports;
}
}
我们再来看一下 CacheConfigurations#getConfigurationClass
的实现。由于代码不多我就全贴上来了。
final class CacheConfigurations {
private static final Map<CacheType, Class<?>> MAPPINGS;
//所有实现SpringCache抽象的装配类
static {
Map<CacheType, Class<?>> mappings = new EnumMap<>(CacheType.class);
mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);
mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
MAPPINGS = Collections.unmodifiableMap(mappings);
}
private CacheConfigurations() {
}
//返回 对应的装配类
static String getConfigurationClass(CacheType cacheType) {
Class<?> configurationClass = MAPPINGS.get(cacheType);
Assert.state(configurationClass != null, () -> "Unknown cache type " + cacheType);
return configurationClass.getName();
}
static CacheType getType(String configurationClassName) {
for (Map.Entry<CacheType, Class<?>> entry : MAPPINGS.entrySet()) {
if (entry.getValue().getName().equals(configurationClassName)) {
return entry.getKey();
}
}
throw new IllegalStateException("Unknown configuration class " + configurationClassName);
}
}
看到这我们其实大致就可以看明白Spring Cache支持的所有缓存方式。因此 如果你想使用任意一种缓存方式,先要看看 CacheConfigurations
是否支持。
CaffeineCacheConfiguration的装配以及实现
Spring Cache 从支持google的guava缓存组件升级为 Caffeine缓存组件,根据网上的资料,是性能更好的缓存组件,因此我们从这个缓存组件的装配入手,看看到底如何实现 Spring Cache 抽象层。
相关依赖
首先引入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
Caffeine的自动装配
我们打开 org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration
装配类,来看一下源码的实现。我们可以看到实现不到100行的代码,但是里面包含的东西,让我也学到了很多。接下来我们简单的看一下。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Caffeine.class, CaffeineCacheManager.class }) //装配条件,不多做解释
@ConditionalOnMissingBean(CacheManager.class) //装配条件,不多做解释
@Conditional({ CacheCondition.class }) //重点,同样的装配条件,但是为什么使用这种实现,后面会说到
class CaffeineCacheConfiguration {
//装配 抽象接口 CacheManager 的实现类 CaffeineCacheManager
@Bean
CaffeineCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers customizers,
ObjectProvider<Caffeine<Object, Object>> caffeine, ObjectProvider<CaffeineSpec> caffeineSpec,
ObjectProvider<CacheLoader<Object, Object>> cacheLoader) {
CaffeineCacheManager cacheManager = createCacheManager(cacheProperties, caffeine, caffeineSpec, cacheLoader);
List<String> cacheNames = cacheProperties.getCacheNames();
if (!CollectionUtils.isEmpty(cacheNames)) {
cacheManager.setCacheNames(cacheNames);
}
return customizers.customize(cacheManager);
}
private CaffeineCacheManager createCacheManager(CacheProperties cacheProperties,
ObjectProvider<Caffeine<Object, Object>> caffeine, ObjectProvider<CaffeineSpec> caffeineSpec,
ObjectProvider<CacheLoader<Object, Object>> cacheLoader) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
setCacheBuilder(cacheProperties, caffeineSpec.getIfAvailable(), caffeine.getIfAvailable(), cacheManager);
cacheLoader.ifAvailable(cacheManager::setCacheLoader);
return cacheManager;
}
//根据配置信息,配置缓存的类信息
private void setCacheBuilder(CacheProperties cacheProperties, CaffeineSpec caffeineSpec,
Caffeine<Object, Object> caffeine, CaffeineCacheManager cacheManager) {
String specification = cacheProperties.getCaffeine().getSpec();
if (StringUtils.hasText(specification)) {
cacheManager.setCacheSpecification(specification);
}
else if (caffeineSpec != null) {
cacheManager.setCaffeineSpec(caffeineSpec);
}
else if (caffeine != null) {
cacheManager.setCaffeine(caffeine);
}
}
}
可以看到上面的代码也就是 注入了一个CaffeineCacheManager的Bean,具体的创建方法大家可以自己跟代码看一下,这么看来确实没什么可分析的,但是为什么要贴这段代码呢,因为后面的支持多CacheManager实现,是参考这段代码写的。这段代码最重要的不是CacheManager的Bean注入,而是 @Conditional({ CacheCondition.class })
的条件装配。
我们来仔细看一下这个类并且学习多学习一个装配技巧。
/**
* General cache condition used with all cache configuration classes.
*
* @author Stephane Nicoll
* @author Phillip Webb
* @author Madhura Bhave
*/
class CacheCondition extends SpringBootCondition { //继承SpringBootCondition ,SpringBootCondition是所有装配条件实现的必要接口
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String sourceClass = "";
if (metadata instanceof ClassMetadata) {
//返回标注该注解的ClassName 在本段代码中,代表 CaffeineCacheConfiguration.class
sourceClass = ((ClassMetadata) metadata).getClassName();
}
ConditionMessage.Builder message = ConditionMessage.forCondition("Cache", sourceClass);
//获取配置信息资源
Environment environment = context.getEnvironment();
try {
//拿到想要设置的缓存类别 使用SpringBoot 配置文件配置 ,本文中配置类型为 spring.cache.type=Caffeine
BindResult<CacheType> specified = Binder.get(environment).bind("spring.cache.type", CacheType.class);
//如果类型不符 即 spring.cache.type=redis 则返回未匹配成功,CaffeineCacheConfiguration不会进行自动装配
if (!specified.isBound()) {
return ConditionOutcome.match(message.because("automatic cache type"));
}
//这里通过 CaffeineCacheConfiguration.class 去找对应的CacheType 可以看上面贴的源码
CacheType required = CacheConfigurations.getType(((AnnotationMetadata) metadata).getClassName());
//如果对应的枚举类和配置的类别相等,则进行装配
if (specified.get() == required) {
return ConditionOutcome.match(message.because(specified.get() + " cache type"));
}
}
catch (BindException ex) {
}
return ConditionOutcome.noMatch(message.because("unknown cache type"));
}
}
具体的代码解释注解中已经给出,从这就可以看到,为什么Spring Cache只可以配置一种缓存Type,并且可以灵活的针对配置的Type进行CacheManager的实现Bean的装配注入。
多CacheManager解决方案
本文并没有讲解具体的多CacheManager实现,而是先讲解了原生Spring Cache 实现,但是就是通过查看原生的实现,才有了之后的多CacheManager的解决方案,解决方案大致如下
- 自定义springboot外部化配置,如:
test.cache.type
来定义产品自身的缓存类型 - 模拟
CacheCondition.class
来实现条件装配,装配CompositeCacheManager
- 将
CompositeCacheManager
设为@Primary
这样无论客户还是产品本身都可以使用依赖查找的方式来找到CacheManager
的实现类。 - 按照Spring Cache 封箱好的方法调用即可。
小结
简单根据自己解决需求的思路去做了记录,希望可以帮助到大家,之后会有具体的整合实现文章。