<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          公司入職一個(gè)阿里大佬,把SpringBoot項(xiàng)目啟動(dòng)從420秒優(yōu)化到了40秒!

          共 14295字,需瀏覽 29分鐘

           ·

          2023-03-07 23:54

          點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)

          作者:Debugger
          鏈接:https://juejin.cn/post/7181342523728592955


          # 背景


          公司 SpringBoot 項(xiàng)目在日常開(kāi)發(fā)過(guò)程中發(fā)現(xiàn)服務(wù)啟動(dòng)過(guò)程異常緩慢,常常需要6-7分鐘才能暴露端口,嚴(yán)重降低開(kāi)發(fā)效率。通過(guò) SpringBoot 的 SpringApplicationRunListener 、BeanPostProcessor 原理和源碼調(diào)試等手段排查發(fā)現(xiàn),在 Bean 掃描和 Bean 注入這個(gè)兩個(gè)階段有很大的性能瓶頸。


          通過(guò) JavaConfig 注冊(cè) Bean, 減少 SpringBoot 的掃描路徑,同時(shí)基于 Springboot 自動(dòng)配置原理對(duì)第三方依賴(lài)優(yōu)化改造,將服務(wù)本地啟動(dòng)時(shí)間從7min 降至40s 左右的過(guò)程。本文會(huì)涉及以下知識(shí)點(diǎn):


          • 基于 SpringApplicationRunListener 原理觀察 SpringBoot 啟動(dòng) run 方法;

          • 基于 BeanPostProcessor 原理監(jiān)控 Bean 注入耗時(shí);

          • SpringBoot Cache 自動(dòng)化配置原理;

          • SpringBoot 自動(dòng)化配置原理及 starter 改造;


          # 耗時(shí)問(wèn)題排查


          SpringBoot 服務(wù)啟動(dòng)耗時(shí)排查,目前有2個(gè)思路:


          1. 排查 SpringBoot 服務(wù)的啟動(dòng)過(guò)程;

          2. 排查 Bean 的初始化耗時(shí);




          1.1 觀察 SpringBoot 啟動(dòng) run 方法


          該項(xiàng)目使用基于 SpringBoot 改造的內(nèi)部微服務(wù)組件 XxBoot 作為服務(wù)端實(shí)現(xiàn),其啟動(dòng)流程與 SpringBoot 類(lèi)似,分為 ApplicationContext 構(gòu)造和 ApplicationContext 啟動(dòng)兩部分,即通過(guò)構(gòu)造函數(shù)實(shí)例化 ApplicationContext 對(duì)象,并調(diào)用其 run 方法啟動(dòng)服務(wù):


          public class Application {    public static void main(String[] args) {        SpringApplication.run(Application.class, args);    }}
          public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) { return new SpringApplication(primarySources).run(args);}

          ApplicationContext 對(duì)象構(gòu)造過(guò)程,主要做了自定義 Banner 設(shè)置、應(yīng)用類(lèi)型推斷、配置源設(shè)置等工作,不做特殊擴(kuò)展的話,大部分項(xiàng)目都是差不多的,不太可能引起耗時(shí)問(wèn)題。通過(guò)在 run 方法中打斷點(diǎn),啟動(dòng)后很快就運(yùn)行到斷點(diǎn)位置,也能驗(yàn)證這一點(diǎn)。


          接下就是重點(diǎn)排查 run 方法的啟動(dòng)過(guò)程中有哪些性能瓶頸?SpringBoot 的啟動(dòng)過(guò)程非常復(fù)雜,慶幸的是 SpringBoot 本身提供的一些機(jī)制,將 SpringBoot 的啟動(dòng)過(guò)程劃分了多個(gè)階段,這個(gè)階段劃分的過(guò)程就體現(xiàn)在 SpringApplicationRunListener 接口中,該接口將 ApplicationContext 對(duì)象的 run 方法劃分成不同的階段:


          public interface SpringApplicationRunListener {    // run 方法第一次被執(zhí)行時(shí)調(diào)用,早期初始化工作    void starting();    // environment 創(chuàng)建后,ApplicationContext 創(chuàng)建前    void environmentPrepared(ConfigurableEnvironment environment);    // ApplicationContext 實(shí)例創(chuàng)建,部分屬性設(shè)置了    void contextPrepared(ConfigurableApplicationContext context);    // ApplicationContext 加載后,refresh 前    void contextLoaded(ConfigurableApplicationContext context);    // refresh 后    void started(ConfigurableApplicationContext context);    // 所有初始化完成后,run 結(jié)束前    void running(ConfigurableApplicationContext context);    // 初始化失敗后    void failed(ConfigurableApplicationContext context, Throwable exception);}

          目前,SpringBoot 中自帶的 SpringApplicationRunListener 接口只有一個(gè)實(shí)現(xiàn)類(lèi):EventPublishingRunListener,該實(shí)現(xiàn)類(lèi)作用:通過(guò)觀察者模式的事件機(jī)制,在 run 方法的不同階段觸發(fā) Event 事件,ApplicationListener 的實(shí)現(xiàn)類(lèi)們通過(guò)監(jiān)聽(tīng)不同的 Event 事件對(duì)象觸發(fā)不同的業(yè)務(wù)處理邏輯。

          通過(guò)自定義實(shí)現(xiàn) ApplicationListener 實(shí)現(xiàn)類(lèi),可以在 SpringBoot 啟動(dòng)的不同階段,實(shí)現(xiàn)一定的處理,可見(jiàn)SpringApplicationRunListener 接口給 SpringBoot 帶來(lái)了擴(kuò)展性。


          這里我們不必深究實(shí)現(xiàn)類(lèi) EventPublishingRunListener 的功能,但是可以通過(guò) SpringApplicationRunListener 原理,添加一個(gè)自定義的實(shí)現(xiàn)類(lèi),在不同階段結(jié)束時(shí)打印下當(dāng)前時(shí)間,通過(guò)計(jì)算不同階段的運(yùn)行時(shí)間,就能大體定位哪些階段耗時(shí)比較高,然后重點(diǎn)排查這些階段的代碼。


          先看下 SpringApplicationRunListener 的實(shí)現(xiàn)原理,其劃分不同階段的邏輯體現(xiàn)在 ApplicationContext 的 run 方法中:

          public ConfigurableApplicationContext run(String... args) {    ...    // 加載所有 SpringApplicationRunListener 的實(shí)現(xiàn)類(lèi)    SpringApplicationRunListeners listeners = getRunListeners(args);    // 調(diào)用了 starting    listeners.starting();    try {        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);        // 調(diào)用了 environmentPrepared        ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);        configureIgnoreBeanInfo(environment);        Banner printedBanner = printBanner(environment);        context = createApplicationContext();        exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context);        // 內(nèi)部調(diào)用了 contextPrepared、contextLoaded        prepareContext(context, environment, listeners, applicationArguments, printedBanner);        refreshContext(context);        afterRefresh(context, applicationArguments);        stopWatch.stop();        if (this.logStartupInfo) {            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);        }        // 調(diào)用了 started        listeners.started(context);        callRunners(context, applicationArguments);    }    catch (Throwable ex) {        // 內(nèi)部調(diào)用了 failed        handleRunFailure(context, ex, exceptionReporters, listeners);        throw new IllegalStateException(ex);    }    try {        // 調(diào)用了 running        listeners.running(context);    }    catch (Throwable ex) {        handleRunFailure(context, ex, exceptionReporters, null);        throw new IllegalStateException(ex);    }    return context;}

          run 方法中 getRunListeners(args) 通過(guò) SpringFactoriesLoader 加載 classpath 下 META-INF/spring.factotries 中配置的所有 SpringApplicationRunListener 的實(shí)現(xiàn)類(lèi),通過(guò)反射實(shí)例化后,存到局部變量 listeners 中,其類(lèi)型為 SpringApplicationRunListeners;然后在 run 方法不同階段通過(guò)調(diào)用 listeners 的不同階段方法來(lái)觸發(fā) SpringApplicationRunListener 所有實(shí)現(xiàn)類(lèi)的階段方法調(diào)用。


          因此,只要編寫(xiě)一個(gè) SpringApplicationRunListener 的自定義實(shí)現(xiàn)類(lèi),在實(shí)現(xiàn)接口不同階段方法時(shí),打印當(dāng)前時(shí)間;并在 META-INF/spring.factotries 中配置該類(lèi)后,該類(lèi)也會(huì)實(shí)例化,存到 listeners 中;在不同階段結(jié)束時(shí)打印結(jié)束時(shí)間,以此來(lái)評(píng)估不同階段的執(zhí)行耗時(shí)。


          在項(xiàng)目中添加實(shí)現(xiàn)類(lèi) MySpringApplicationRunListener :

          @Slf4jpublic class MySpringApplicationRunListener implements SpringApplicationRunListener {    // 這個(gè)構(gòu)造函數(shù)不能少,否則反射生成實(shí)例會(huì)報(bào)錯(cuò)    public MySpringApplicationRunListener(SpringApplication sa, String[] args) {    }    @Override    public void starting() {        log.info("starting {}", LocalDateTime.now());    }    @Override    public void environmentPrepared(ConfigurableEnvironment environment) {        log.info("environmentPrepared {}", LocalDateTime.now());    }    @Override    public void contextPrepared(ConfigurableApplicationContext context) {        log.info("contextPrepared {}", LocalDateTime.now());    }    @Override    public void contextLoaded(ConfigurableApplicationContext context) {        log.info("contextLoaded {}", LocalDateTime.now());    }    @Override    public void started(ConfigurableApplicationContext context) {        log.info("started {}", LocalDateTime.now());    }    @Override    public void running(ConfigurableApplicationContext context) {        log.info("running {}", LocalDateTime.now());    }    @Override    public void failed(ConfigurableApplicationContext context, Throwable exception) {        log.info("failed {}", LocalDateTime.now());    }}

          這邊 (SpringApplication sa, String[] args) 參數(shù)類(lèi)型的構(gòu)造函數(shù)不能少,因?yàn)樵创a中限定了使用該參數(shù)類(lèi)型的構(gòu)造函數(shù)反射生成實(shí)例。


          在 resources 文件下的 META-INF/spring.factotries 文件中配置上該類(lèi):

          # Run Listenersorg.springframework.boot.SpringApplicationRunListener=\com.xxx.ad.diagnostic.tools.api.MySpringApplicationRunListener

          run 方法中是通過(guò) getSpringFactoriesInstances 方法來(lái)獲取 META-INF/spring.factotries 下配置的 SpringApplicationRunListener 的實(shí)現(xiàn)類(lèi),其底層是依賴(lài) SpringFactoriesLoader 來(lái)獲取配置的類(lèi)的全限定類(lèi)名,然后反射生成實(shí)例;
          這種方式在 SpringBoot 用的非常多,如 EnableAutoConfiguration、ApplicationListener、ApplicationContextInitializer 等。


          重啟服務(wù),觀察 MySpringApplicationRunListener 的日志輸出,發(fā)現(xiàn)主要耗時(shí)都在 contextLoaded 和 started 兩個(gè)階段之間,在這兩個(gè)階段之間調(diào)用了2個(gè)方法:


          refreshContext 和 afterRefresh 方法,而 refreshContext 底層調(diào)用的是 AbstractApplicationContext#refresh,Spring 初始化 context 的核心方法之一就是這個(gè) refresh。




          至此基本可以斷定,高耗時(shí)的原因就是在初始化 Spring 的 context,然而這個(gè)方法依然十分復(fù)雜,好在 refresh 方法也將初始化 Spring 的 context 的過(guò)程做了整理,并詳細(xì)注釋了各個(gè)步驟的作用:


          通過(guò)簡(jiǎn)單調(diào)試,很快就定位了高耗時(shí)的原因:


          1. 在 invokeBeanFactoryPostProcessors(beanFactory) 方法中,調(diào)用了所有注冊(cè)的 BeanFactory 的后置處理器;

          2. 其中,ConfigurationClassPostProcessor 這個(gè)后置處理器貢獻(xiàn)了大部分的耗時(shí);

          3. 查閱相關(guān)資料,該后置處理器相當(dāng)重要,主要負(fù)責(zé)@Configuration、@ComponentScan、@Import、@Bean 等注解的解析;

          4. 繼續(xù)調(diào)試發(fā)現(xiàn),主要耗時(shí)都花在主配置類(lèi)的 @ComponentScan 解析上,而且主要耗時(shí)還是在解析屬性 basePackages;


          即項(xiàng)目主配置類(lèi)上 @SpringBootApplication 注解的 scanBasePackages 屬性:




          通過(guò)該方法 JavaDoc、查看相關(guān)代碼,大體了解到該過(guò)程是在遞歸掃描、解析 basePackages 所有路徑下的 class,對(duì)于可作為 Bean 的對(duì)象,生成其 BeanDefinition;如果遇到 @Configuration 注解的配置類(lèi),還得遞歸解析其 @ComponentScan。至此,服務(wù)啟動(dòng)緩慢的原因就找到了:


          1. 作為數(shù)據(jù)平臺(tái),我們的服務(wù)引用了很多第三方依賴(lài)服務(wù),這些依賴(lài)往往提供了對(duì)應(yīng)業(yè)務(wù)的完整功能,所以提供的 jar 包非常大;

          2. 掃描這些包路徑下的 class 非常耗時(shí),很多 class 都不提供 Bean,但還是花時(shí)間掃描了;

          3. 每添加一個(gè)服務(wù)的依賴(lài),都會(huì)線性增加掃描的時(shí)間;


          弄明白耗時(shí)的原因后,我有2個(gè)疑問(wèn):


          1. 是否所有的 class 都需要掃描,是否可以只掃描那些提供 Bean 的 class?

          2. 掃描出來(lái)的 Bean 是否都需要?我只接入一個(gè)功能,但是注入了所有的 Bean,這似乎不太合理?


          1.2 監(jiān)控 Bean 注入耗時(shí)


          第二個(gè)優(yōu)化的思路是監(jiān)控所有 Bean 對(duì)象初始化的耗時(shí),即每個(gè) Bean 對(duì)象實(shí)例化、初始化、注冊(cè)所花費(fèi)的時(shí)間,有沒(méi)有特別耗時(shí) Bean 對(duì)象?


          同樣的,我們可以利用 SpringBoot 提供了 BeanPostProcessor 接口來(lái)監(jiān)控 Bean 的注入耗時(shí),BeanPostProcessor 是 Spring 提供的 Bean 初始化前后的 IOC 鉤子,用于在 Bean 初始化的前后執(zhí)行一些自定義的邏輯:

          public interface BeanPostProcessor {    // 初始化前    default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {        return bean;    }    // 初始化后    default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {        return bean;    }   }

          對(duì)于 BeanPostProcessor 接口的實(shí)現(xiàn)類(lèi),其前后置處理過(guò)程體現(xiàn)在 AbstractAutowireCapableBeanFactory#doCreateBean,這也是 Spring 中非常重要的一個(gè)方法,用于真正實(shí)例化 Bean 對(duì)象,通過(guò) BeanFactory#getBean 方法一路 Debug 就能找到。在該方法中調(diào)用了 initializeBean 方法:


          protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {    ...    Object wrappedBean = bean;    if (mbd == null || !mbd.isSynthetic()) {        // 應(yīng)用所有 BeanPostProcessor 的前置方法        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);    }    try {        invokeInitMethods(beanName, wrappedBean, mbd);    }    catch (Throwable ex) {        throw new BeanCreationException(                (mbd != null ? mbd.getResourceDescription() : null),                beanName, "Invocation of init method failed", ex);    }    if (mbd == null || !mbd.isSynthetic()) {        // 應(yīng)用所有 BeanPostProcessor 的后置方法        wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);    }    return wrappedBean;}

          通過(guò) BeanPostProcessor 原理,在前置處理時(shí)記錄下當(dāng)前時(shí)間,在后置處理時(shí),用當(dāng)前時(shí)間減去前置處理時(shí)間,就能知道每個(gè) Bean 的初始化耗時(shí),下面是我的實(shí)現(xiàn):

          @Componentpublic class TimeCostBeanPostProcessor implements BeanPostProcessor {    private Map<String, Long> costMap = Maps.newConcurrentMap();
          @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { costMap.put(beanName, System.currentTimeMillis()); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (costMap.containsKey(beanName)) { Long start = costMap.get(beanName); long cost = System.currentTimeMillis() - start; if (cost > 0) { costMap.put(beanName, cost); System.out.println("bean: " + beanName + "\ttime: " + cost); } } return bean; }}

          BeanPostProcessor 的邏輯是在 Beanfactory 準(zhǔn)備好后處理的,就不需要通過(guò) SpringFactoriesLoader 加載了,直接 @Component 注入即可。


          重啟服務(wù),通過(guò)以上方法排查 Bean 初始化過(guò)程,還真的有所發(fā)現(xiàn):



          這個(gè) Bean 初始化耗時(shí)43s,具體看下這個(gè) Bean 的初始化方法,發(fā)現(xiàn)會(huì)從數(shù)據(jù)庫(kù)查詢(xún)大量配置元數(shù)據(jù),并更新到 Redis 緩存中,所以初始化非常慢:




          另外,還發(fā)現(xiàn)了一些非項(xiàng)目自身服務(wù)的service、controller對(duì)象,這些 Bean 來(lái)自于第三方依賴(lài):UPM服務(wù),項(xiàng)目中并不需要:




          其實(shí),原因上文已經(jīng)提到:我只接入一個(gè)功能,但我注入了該服務(wù)路徑下所有的 Bean,也就是說(shuō),服務(wù)里注入其他服務(wù)的、對(duì)自身無(wú)用的 Bean。


          # 優(yōu)化方案


          2.1 如何解決掃描路徑過(guò)多?


          想到的解決方案比較簡(jiǎn)單粗暴:


          梳理要引入的 Bean,刪掉主配置類(lèi)上掃描路徑,使用 JavaConfig 的方式顯式手動(dòng)注入。


          以 UPM 的依賴(lài)為例,之前的注入方式 是,項(xiàng)目依賴(lài)其 UpmResourceClient 對(duì)象,Pom 已經(jīng)引用了其 Maven 坐標(biāo),并在主配置類(lèi)上的 scanBasePackages 中添加了其服務(wù)路徑:"com.xxx.ad.upm",通過(guò)掃描整個(gè)服務(wù)路徑下的 class,找到 UpmResourceClient 并注入,因?yàn)樵擃?lèi)注解了 @Service,因此會(huì)注入到服務(wù)的 Spring 上下文中,UpmResourceClient 源碼片段及主配置類(lèi)如下:




          使用 JavaConfig 的改造方式是:不再掃描 UPM 的服務(wù)路徑,而是主動(dòng)注入。刪除"com.xxx.ad.upm",并在服務(wù)路徑下添加以下配置類(lèi):


          @Configurationpublic class ThirdPartyBeanConfig {    @Bean    public UpmResourceClient upmResourceClient() {        return new UpmResourceClient();    }}

          Tips:如果該 Bean 還依賴(lài)其他 Bean,則需要把所依賴(lài)的 Bean 都注入;針對(duì) Bean 依賴(lài)情況復(fù)雜的場(chǎng)景梳理起來(lái)就比較麻煩了,所幸項(xiàng)目用到的服務(wù) Bean 依賴(lài)關(guān)系都比較簡(jiǎn)單,一些依賴(lài)關(guān)系復(fù)雜的服務(wù),觀察到其路徑掃描耗時(shí)也不是很高,就不處理了。

          同時(shí),通過(guò) JavaConfig 按需注入的方式,就不存在冗余 Bean 的情況了,也有利于降低服務(wù)的內(nèi)存消耗;解決了上面的引入無(wú)關(guān)的 upmService、upmController 的問(wèn)題。


          2.2 如何解決 Bean 初始化高耗時(shí)?


          Bean 初始化耗時(shí)高,就需要 case by case 地處理了,比如項(xiàng)目中遇到的初始化配置元數(shù)據(jù)的問(wèn)題,可以考慮通過(guò)將該任務(wù)提交到線程池的方式異步處理或者懶加載的方式來(lái)解決。


          # 新的問(wèn)題


          完成以上優(yōu)化后,本地啟動(dòng)時(shí)間從之前的 7min 左右降低至 40s,效果還是非常顯著的。本地自測(cè)通過(guò)后,便發(fā)布到預(yù)發(fā)進(jìn)行驗(yàn)證,驗(yàn)證過(guò)程中,有同學(xué)發(fā)現(xiàn)項(xiàng)目接入的 Redis 緩存組件失效了。


          該組件接入方式與上文描述的接入方式類(lèi)似,通過(guò)添加掃描服務(wù)的根路徑"com.xxx.ad.rediscache",注入對(duì)應(yīng)的 Bean 對(duì)象;查看該緩存組件項(xiàng)目的源碼,發(fā)現(xiàn)該路徑下有一個(gè) config 類(lèi)注入了一個(gè)緩存管理對(duì)象 CacheManager,其實(shí)現(xiàn)類(lèi)是 RedisCacheManager:



          緩存組件代碼片段:




          本次優(yōu)化中,我是通過(guò) 每次刪除一條掃描路徑,啟動(dòng)服務(wù)后根據(jù)啟動(dòng)日志中 Bean 缺失錯(cuò)誤的信息,來(lái)逐個(gè)梳理、添加依賴(lài)的 Bean,保證服務(wù)正常啟動(dòng) 的方式來(lái)改造的,而刪除"com.xxx.ad.rediscache"后啟動(dòng)服務(wù)并無(wú)異常,因此就沒(méi)有進(jìn)一步的操作,直接上預(yù)發(fā)驗(yàn)證了。這就奇怪了,既然不掃描該組件的業(yè)務(wù)代碼根路徑,也就沒(méi)有執(zhí)行注入該組件中定義的 CacheManager 對(duì)象,為啥用到緩存的地方?jīng)]有報(bào)錯(cuò)呢?


          嘗試在未添加掃描路徑的情況下,從 ApplicationContext 中獲取 CacheManager 類(lèi)型的對(duì)象看下是否存在?結(jié)果發(fā)現(xiàn)確實(shí)存在 RedisCacheManager 對(duì)象:




          其實(shí),前面的分析并沒(méi)有錯(cuò),刪除掃描路徑后生成的 RedisCacheManager 并不是緩存組件代碼中配置的,而是 SpringBoot 的自動(dòng)化配置生成的,也就是說(shuō)該對(duì)象并不是我們想要的對(duì)象,是不符合預(yù)期的,下文介紹其原因。


          3.1 SpringBoot 自動(dòng)化裝配,讓人防不勝防


          查閱 SpringBoot Cache 相關(guān)資料,發(fā)現(xiàn) SpringBoot Cache 做了一些自動(dòng)推斷和注入的工作,原來(lái)是 SpringBoot 自動(dòng)化裝配的鍋呀,接下來(lái)就分析下 SpringBoot Cache 原理,明確出現(xiàn)以上問(wèn)題的原因。


          SpringBoot 自動(dòng)化配置,體現(xiàn)在主配置類(lèi)上復(fù)合注解 @SpringBootApplication 中的@EnableAutoConfiguration 上,該注解開(kāi)啟了 SpringBoot 的自動(dòng)配置功能。該注解中的@Import(AutoConfigurationImportSelector.class) 通過(guò)加載 META-INF/spring.factotries 下配置一系列 *AutoConfiguration 配置類(lèi),根據(jù)現(xiàn)有條件推斷,盡可能地為我們配置需要的 Bean。這些配置類(lèi)負(fù)責(zé)各個(gè)功能的自動(dòng)化配置,其中用于 SpringBoot Cache 的自動(dòng)配置類(lèi)是 CacheAutoConfiguration,接下來(lái)重點(diǎn)分析這個(gè)配置類(lèi)就行了。


          @SpringBootApplication 復(fù)合注解中集成了三個(gè)非常重要的注解:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan,其中 @EnableAutoConfiguration 就是負(fù)責(zé)開(kāi)啟自動(dòng)化配置功能;
          SpringBoot 中有多@EnableXXX 的注解,都是用來(lái)開(kāi)啟某一方面的功能,其實(shí)現(xiàn)原理也是類(lèi)似的:通過(guò) @Import 篩選、導(dǎo)入滿(mǎn)足條件的自動(dòng)化配置類(lèi)。


          可以看到 CacheAutoConfiguration 上有許多注解,重點(diǎn)關(guān)注下@Import({CacheConfigurationImportSelector.class}),CacheConfigurationImportSelector 實(shí)現(xiàn)了 ImportSelector 接口,該接口用于動(dòng)態(tài)選擇想導(dǎo)入的配置類(lèi),這個(gè) CacheConfigurationImportSelector 用來(lái)導(dǎo)入不同類(lèi)型的 Cache 的自動(dòng)配置類(lèi):



          通過(guò)調(diào)試 CacheConfigurationImportSelector 發(fā)現(xiàn),根據(jù) SpringBoot 支持的緩存類(lèi)型(CacheType),提供了10種 cache 的自動(dòng)配置類(lèi),按優(yōu)先級(jí)排序,最終只有一個(gè)生效,而本項(xiàng)目中恰恰就是 RedisCacheConfiguration,其內(nèi)部提供的是 RedisCacheManager,和引入第三方緩存組件一樣,所以造成了困惑:




          看下 RedisCacheConfiguration 的實(shí)現(xiàn):



          這個(gè)配置類(lèi)上有很多條件注解,當(dāng)這些條件都滿(mǎn)足的話,這個(gè)自動(dòng)配置類(lèi)就會(huì)生效,而本項(xiàng)目恰恰都滿(mǎn)足,同時(shí)項(xiàng)目主配置類(lèi)上還加上了 @EnableCaching,開(kāi)啟了緩存功能,即使緩存組件沒(méi)生效,SpringBoot 也會(huì)自動(dòng)生成一個(gè)緩存管理對(duì)象;


          即:緩存組件服務(wù)掃描路徑存在的話,緩存組件中的代碼生成緩存管理對(duì)象,@ConditionalOnMissingBean(CacheManager.class) 失效;掃描路徑不存在的話,SpringBoot 通過(guò)推斷,自動(dòng)生成一個(gè)緩存管理對(duì)象。


          這個(gè)也很好驗(yàn)證,在 RedisCacheConfiguration 中打斷點(diǎn),不刪除掃描路徑是走不到這邊的SpringBoot 自動(dòng)裝配過(guò)程的(緩存組件顯式生成過(guò)了),刪除了掃描路徑是能走到的(SpringBoot 自動(dòng)生成)。

          上文多次提到@Import,這是 SpringBoot 中重要注解,主要有以下作用:
          1、導(dǎo)入 @Configuration 注解的類(lèi);
          2、導(dǎo)入實(shí)現(xiàn)了 ImportSelector 或 ImportBeanDefinitionRegistrar 的類(lèi);
          3、導(dǎo)入普通的 POJO。


          3.2 使用 starter 機(jī)制,開(kāi)箱即用


          了解緩存失效的原因后,就有解決的辦法了,因?yàn)槭亲约簣F(tuán)隊(duì)的組件,就沒(méi)必要通過(guò) JavaConfig 顯式手動(dòng)導(dǎo)入的方式改造,而是通過(guò) SpringBoot 的 starter 機(jī)制,優(yōu)化下緩存組件的實(shí)現(xiàn),可以做到自動(dòng)注入、開(kāi)箱即用。只要改造下緩存組件的代碼,在 resources 文件中添加一個(gè) META-INF/spring.factotries 文件,在下面配置一個(gè) EnableAutoConfiguration 即可,這樣項(xiàng)目在啟動(dòng)時(shí)也會(huì)掃描到這個(gè) jar 中的 spring.factotries 文件,將 XxxAdCacheConfiguration 配置類(lèi)自動(dòng)引入,而不需要掃描"com.xxx.ad.rediscache"整個(gè)路徑了:

          # EnableAutoConfigurationsorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.xxx.ad.rediscache.XxxAdCacheConfiguration

          SpringBoot 的 EnableAutoConfiguration 自動(dòng)配置原理還是比較復(fù)雜的,在加載自動(dòng)配置類(lèi)前還要先加載自動(dòng)配置的元數(shù)據(jù),對(duì)所有自動(dòng)配置類(lèi)做有效性篩選,具體可查閱 EnableAutoConfiguration 相關(guān)代碼;

            

          1、社區(qū)糾紛不斷:程序員何苦為難程序員?

          2、該死的單元測(cè)試,寫(xiě)起來(lái)到底有多痛?

          3、互聯(lián)網(wǎng)人為什么學(xué)不會(huì)擺爛

          4、為什么國(guó)外JetBrains做 IDE 就可以養(yǎng)活自己,國(guó)內(nèi)不行?區(qū)別在哪?

          5、相比高人氣的Rust、Go,為何 Java、C 在工具層面進(jìn)展緩慢?

          6、讓程序員早點(diǎn)下班的《技術(shù)寫(xiě)作指南》

          點(diǎn)

          點(diǎn)

          點(diǎn)點(diǎn)

          點(diǎn)在看

          瀏覽 40
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  人人看,人人摸 | 国产一级片99 | 18禁成人在线网站 | 日本色情在线视频 | 蜜桃秘 av无码一区二区三区 |