SpringBoot 中各種CacheConfiguration 加載順序填坑探索

本文使用的spring-boot版本是2.0.2.RELEASE,本文力求白話,不會(huì)引入大段晦澀的框架代碼造成閱讀的極度不適,但是本文會(huì)引入一定量閱讀舒適的代碼,而且,讀者需要有一定的框架基礎(chǔ)。

1. 故事起源

最近在研究Spring中的緩存機(jī)制。就拿我自己的項(xiàng)目來(lái)講,最早用了EhCache,后來(lái)全部遷移至Redis。我們知道在SpringBoot中開(kāi)啟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>

就這么簡(jiǎn)單,不需要任何多余的代碼行,SpringBoot就幫我們做完了一切的集成配置。我們來(lái)一段測(cè)試代碼:

這里注意需要打開(kāi)@EnableCaching功能,否則@Cacheable注解的方法不會(huì)被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中執(zhí)行 keys *,結(jié)果為:

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

OK,引入緩存成功!

2. AutoConfiguration初探

本來(lái)故事講完了該結(jié)束了,不過(guò)作為一名有理想,有道德,有文化的攻城獅,怎么能不去了解博大精深的SpringBoot是如何幫我們引入這些自動(dòng)配置的呢?

作為一個(gè)SpringBoot的資深新手,我們知道SpringBoot的傻瓜式懶人配置都存在于spring-boot-autoconfigure包中,那么我們就去這個(gè)包里尋找下線索。

autoconfig.png

不看不知道,一看居然有這么多種類型的緩存配置,Spring果然是包羅萬(wàn)象,主流的非主流的什么都有。
粗略的看了一下這些配置文件,大部分都有引入@Conditional的條件,比如EhCacheCacheConfiguration就需要有EhCache包提供的net.sf.ehcache.Cache定義才可以生效,而我們?cè)谏厦娴拇a中沒(méi)有引入EhCache包,自然不會(huì)生效。而RedisCacheConfiguration則需要一系列spring-boot-data-redis包提供的組件才可以生效,我們引入了,所以自然就會(huì)去使用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)

正是通過(guò)這些條件的限制,SpringBoot才會(huì)根據(jù)引入的pom包選擇合適的緩存配置類。

3. 特殊的配置類

故事到這里結(jié)束了?沒(méi)有,仔細(xì)看一看這些配置類,發(fā)現(xiàn)有幾個(gè)特殊的小哥

NoOpCacheConfiguration
SimpleCacheConfiguration

你問(wèn)我哪里特殊了?確實(shí)很特殊,因?yàn)樗麄儍蓚€(gè)的生效條件都是滿足的,也就是說(shuō),這兩個(gè)都能作為緩存配置引入,但是SpringBoot卻非?!爸悄堋钡挠昧薘edis作為緩存。

NoOpCacheConfiguration

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

SimpleCacheConfiguration

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

什么時(shí)候SpringBoot有AI功能了?囧,這不可能!

必須不可能啊,所以SpringBoot肯定做了一些事情,那么我們就再仔細(xì)探究下為什么只有RedisCacheConfigurationspring-boot選中了,其他兩個(gè)卻落榜了。

4. 再次探究選擇機(jī)制

我們知道,SpringBoot在初始化的時(shí)候,會(huì)去spring-boot-autoconfigure包中尋找配置類。因?yàn)镾pringBoot的設(shè)計(jì)理念就是即插即用,傻瓜式,讓開(kāi)發(fā)者告別繁瑣的配置,所以這個(gè)包中內(nèi)容是很豐富的,基本上主流的中間件都會(huì)在這個(gè)包中留下自己的配置定義。但是spring在初始化的時(shí)候并不是將所有的配置類都會(huì)加載進(jìn)來(lái),這時(shí)候那些沒(méi)在spring-boot-autoconfigure包中的spring.factories中定義的,那些不滿足@Conditional的的配置,就會(huì)被SpringBoot過(guò)濾掉。所以,如我們上面的測(cè)試代碼,其實(shí)只引入了Redis部分的配置。
但是,這兩個(gè)特殊的緩存配置類卻是滿足導(dǎo)入要求的,因此,我們?cè)诓榭?code>configClasses集合包含的類定義的時(shí)候,確實(shí)是有這兩個(gè)配置類的定義的:

ConfigurationClassPostProcessor.processConfigBeanDefinitions 方法中的 configClasses

configuration.png

到這里暫時(shí)還沒(méi)思緒,問(wèn)題還是SpringBoot為何選擇的是Redis緩存配置,而不是選擇其他緩存配置。帶著這個(gè)問(wèn)題再次閱讀一遍三個(gè)可選的緩存配置的導(dǎo)入條件。我們發(fā)現(xiàn)
RedisCacheConfiguration, NoOpCacheConfiguration, SimpleCacheConfiguration的條件都有一個(gè)是@ConditionalOnMissingBean(CacheManager.class)

并且我們看到三個(gè)配置類中都有對(duì)這個(gè)CacheManager的Bean輸出。那么,這個(gè)問(wèn)題的思路可能就變成這樣了,肯定是三個(gè)配置類中的其中之一個(gè)搶跑了,輸出了CacheManager的實(shí)例Bean定義,另外兩個(gè)就導(dǎo)致判斷@ConditionalOnMissingBean(CacheManager.class)條件失敗而被廢棄了。

5. 真相大白

到這里有了基本的思路,一定是SpringBoot根據(jù)某種條件對(duì)這三個(gè)配置類的加載順序做了定義,現(xiàn)在我們需要做的是看這三個(gè)配置類的加載先后順序SpringBoot究竟是怎么定義的。

在初始化的時(shí)候,SpringBoot首先會(huì)去尋找用戶項(xiàng)目代碼中是否包含@EnableAutoConfiguration注解,其實(shí)這個(gè)注解默認(rèn)情況下已經(jīng)被@SpringBootApplication所包含,找到之后會(huì)處理@EnableAutoConfiguration注解所導(dǎo)入的AutoConfigurationImportSelector類,這個(gè)類會(huì)根據(jù)spring-boot-autoconfigure包中預(yù)定義的spring.factories文件中去搜尋包中符合條件的配置類導(dǎo)入。這其中,包含了spring.factories列舉的配置之一org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration。

我們看到這個(gè)CacheAutoConfiguration配置類中也有一個(gè)@Import定義,我們打開(kāi)它的定義查看一下,他包含一個(gè)相當(dāng)長(zhǎng)的注解列表:

@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)

查看這個(gè)CacheConfigurationImportSelector類,發(fā)現(xiàn)如下代碼

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;
    }
}

這個(gè)CacheType就立刻映入眼簾了,查看一下發(fā)現(xiàn)是一個(gè)枚舉,到這里就明白了,在這個(gè)枚舉里,定義了配置類的加載順序,我們看到了,REDIS是排在SIMPLENONE之前,這樣子,優(yōu)先被載入的肯定是Redis的配置類也就是RedisCacheConfiguration。那么載入RedisCacheConfiguration之后呢?RedisCacheConfiguration有一個(gè)CacheManager的Bean導(dǎo)出定義,那么,在RedisCacheConfiguration加載之后,另外兩個(gè)緩存配置類的加載條件就不滿足@ConditionalOnMissingBean(CacheManager.class)條件,自然被Spring所拋棄了。

6. 總結(jié)

到這里我們分析了SpringBoot對(duì)緩存加載選擇的內(nèi)部處理流程,SpringBoot由于支持多元化的緩存方案,因此在配置的時(shí)候,需要對(duì)這一塊有所了解,防止掉坑。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容