当前位置: 首页>后端>正文

SpringBoot Cache整合多CacheManager(一)

最近在搞产品相关的缓存整合,由于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 看到自动装配了这个类。

SpringBoot Cache整合多CacheManager(一),第1张
autoCache路径.jpg

这个类是Spring Cache抽象缓存实现的基础,我们简单的看一下,不必深入了解。

SpringBoot Cache整合多CacheManager(一),第2张
装配.jpg

接下来我们重点看一下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 封箱好的方法调用即可。

小结

简单根据自己解决需求的思路去做了记录,希望可以帮助到大家,之后会有具体的整合实现文章。


https://www.xamrdz.com/backend/3e81920691.html

相关文章: