SpringBoot 中各种CacheConfiguration 加载顺序填坑探索

本文使用的spring-boot版本是2.0.2.RELEASE,本文力求白话,不会引入大段晦涩的框架代码造成阅读的极度不适,但是本文会引入一定量阅读舒适的代码,而且,读者需要有一定的框架基础。

1. 故事起源

最近在研究Spring中的缓存机制。就拿我自己的项目来讲,最早用了EhCache,后来全部迁移至Redis。我们知道在SpringBoot中开启Redis作为缓存是比较方便的,直接引入maven依赖即可:

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

就这么简单,不需要任何多余的代码行,SpringBoot就帮我们做完了一切的集成配置。我们来一段测试代码:

这里注意需要打开@EnableCaching功能,否则@Cacheable注解的方法不会被cglib代理。

CacheApplication.java

@EnableCaching
@SpringBootApplication
public class CacheApplication {

    @Autowired 
    private CacheService cacheService;

    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(CacheApplication.class, args);
        CacheApplication app = ctx.getBean(CacheApplication.class);
        System.out.println(app.cacheService.getStr(UUID.randomUUID().toString()));
    }
}

CacheService.java

@Service
public class CacheService {
    @Cacheable(cacheNames="XA", sync=true)
    public String getStr(String key) {
        return "value";
    }
}

跑一下,然后在redis中执行 keys *,结果为:

127.0.0.1:6379> keys *
1) "XA::f17ecb4d-b0e3-4bdc-9973-7592e9c7bacc"

OK,引入缓存成功!

2. AutoConfiguration初探

本来故事讲完了该结束了,不过作为一名有理想,有道德,有文化的攻城狮,怎么能不去了解博大精深的SpringBoot是如何帮我们引入这些自动配置的呢?

作为一个SpringBoot的资深新手,我们知道SpringBoot的傻瓜式懒人配置都存在于spring-boot-autoconfigure包中,那么我们就去这个包里寻找下线索。

autoconfig.png

不看不知道,一看居然有这么多种类型的缓存配置,Spring果然是包罗万象,主流的非主流的什么都有。
粗略的看了一下这些配置文件,大部分都有引入@Conditional的条件,比如EhCacheCacheConfiguration就需要有EhCache包提供的net.sf.ehcache.Cache定义才可以生效,而我们在上面的代码中没有引入EhCache包,自然不会生效。而RedisCacheConfiguration则需要一系列spring-boot-data-redis包提供的组件才可以生效,我们引入了,所以自然就会去使用Redis作为缓存

EhCacheCacheConfiguration.java

@Configuration
@ConditionalOnClass({ Cache.class, EhCacheCacheManager.class })
@ConditionalOnMissingBean(org.springframework.cache.CacheManager.class)
@Conditional({ CacheCondition.class,EhCacheCacheConfiguration.ConfigAvailableCondition.class })

RedisCacheConfiguration.java

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)

正是通过这些条件的限制,SpringBoot才会根据引入的pom包选择合适的缓存配置类。

3. 特殊的配置类

故事到这里结束了?没有,仔细看一看这些配置类,发现有几个特殊的小哥

NoOpCacheConfiguration
SimpleCacheConfiguration

你问我哪里特殊了?确实很特殊,因为他们两个的生效条件都是满足的,也就是说,这两个都能作为缓存配置引入,但是SpringBoot却非常“智能”的用了Redis作为缓存。

NoOpCacheConfiguration

@Configuration
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)

SimpleCacheConfiguration

@Configuration
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)

什么时候SpringBoot有AI功能了?囧,这不可能!

必须不可能啊,所以SpringBoot肯定做了一些事情,那么我们就再仔细探究下为什么只有RedisCacheConfigurationspring-boot选中了,其他两个却落榜了。

4. 再次探究选择机制

我们知道,SpringBoot在初始化的时候,会去spring-boot-autoconfigure包中寻找配置类。因为SpringBoot的设计理念就是即插即用,傻瓜式,让开发者告别繁琐的配置,所以这个包中内容是很丰富的,基本上主流的中间件都会在这个包中留下自己的配置定义。但是spring在初始化的时候并不是将所有的配置类都会加载进来,这时候那些没在spring-boot-autoconfigure包中的spring.factories中定义的,那些不满足@Conditional的的配置,就会被SpringBoot过滤掉。所以,如我们上面的测试代码,其实只引入了Redis部分的配置。
但是,这两个特殊的缓存配置类却是满足导入要求的,因此,我们在查看configClasses集合包含的类定义的时候,确实是有这两个配置类的定义的:

ConfigurationClassPostProcessor.processConfigBeanDefinitions 方法中的 configClasses

configuration.png

到这里暂时还没思绪,问题还是SpringBoot为何选择的是Redis缓存配置,而不是选择其他缓存配置。带着这个问题再次阅读一遍三个可选的缓存配置的导入条件。我们发现
RedisCacheConfiguration, NoOpCacheConfiguration, SimpleCacheConfiguration的条件都有一个是@ConditionalOnMissingBean(CacheManager.class)

并且我们看到三个配置类中都有对这个CacheManager的Bean输出。那么,这个问题的思路可能就变成这样了,肯定是三个配置类中的其中之一个抢跑了,输出了CacheManager的实例Bean定义,另外两个就导致判断@ConditionalOnMissingBean(CacheManager.class)条件失败而被废弃了。

5. 真相大白

到这里有了基本的思路,一定是SpringBoot根据某种条件对这三个配置类的加载顺序做了定义,现在我们需要做的是看这三个配置类的加载先后顺序SpringBoot究竟是怎么定义的。

在初始化的时候,SpringBoot首先会去寻找用户项目代码中是否包含@EnableAutoConfiguration注解,其实这个注解默认情况下已经被@SpringBootApplication所包含,找到之后会处理@EnableAutoConfiguration注解所导入的AutoConfigurationImportSelector类,这个类会根据spring-boot-autoconfigure包中预定义的spring.factories文件中去搜寻包中符合条件的配置类导入。这其中,包含了spring.factories列举的配置之一org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration

我们看到这个CacheAutoConfiguration配置类中也有一个@Import定义,我们打开它的定义查看一下,他包含一个相当长的注解列表:

@Configuration
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
        RedisAutoConfiguration.class })
@Import(CacheConfigurationImportSelector.class)

查看这个CacheConfigurationImportSelector类,发现如下代码

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++) {
            imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
        }
        return imports;
    }
}

这个CacheType就立刻映入眼帘了,查看一下发现是一个枚举,到这里就明白了,在这个枚举里,定义了配置类的加载顺序,我们看到了,REDIS是排在SIMPLENONE之前,这样子,优先被载入的肯定是Redis的配置类也就是RedisCacheConfiguration。那么载入RedisCacheConfiguration之后呢?RedisCacheConfiguration有一个CacheManager的Bean导出定义,那么,在RedisCacheConfiguration加载之后,另外两个缓存配置类的加载条件就不满足@ConditionalOnMissingBean(CacheManager.class)条件,自然被Spring所抛弃了。

6. 总结

到这里我们分析了SpringBoot对缓存加载选择的内部处理流程,SpringBoot由于支持多元化的缓存方案,因此在配置的时候,需要对这一块有所了解,防止掉坑。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352