真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

7min到40s:SpringBoot啟動(dòng)優(yōu)化實(shí)踐-創(chuàng)新互聯(lián)

背景

公司 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è)階段有很大的性能瓶頸。

平南ssl適用于網(wǎng)站、小程序/APP、API接口等需要進(jìn)行數(shù)據(jù)傳輸應(yīng)用場(chǎng)景,ssl證書未來(lái)市場(chǎng)廣闊!成為成都創(chuàng)新互聯(lián)的ssl證書銷售渠道,可以享受市場(chǎng)價(jià)格4-6折優(yōu)惠!如果有意向歡迎電話聯(lián)系或者加微信:13518219792(備注:SSL證書合作)期待與您的合作!

通過(guò) JavaConfig 注冊(cè) Bean, 減少 SpringBoot 的掃描路徑,同時(shí)基于 Springboot 自動(dòng)配置原理對(duì)第三方依賴優(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 改造;
1 耗時(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 類似,分為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);
}
復(fù)制代碼

ApplicationContext對(duì)象構(gòu)造過(guò)程,主要做了自定義 Banner 設(shè)置、應(yīng)用類型推斷、配置源設(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);
}
復(fù)制代碼

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

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

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

public ConfigurableApplicationContext run(String... args) {
    ...
    // 加載所有 SpringApplicationRunListener 的實(shí)現(xiàn)類
    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;
}
復(fù)制代碼

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

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

@Slf4j
public 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());
    }
}
復(fù)制代碼

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

resources文件下的META-INF/spring.factotries文件中配置上該類:

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
com.xxx.ad.diagnostic.tools.api.MySpringApplicationRunListener
復(fù)制代碼

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

重啟服務(wù),觀察MySpringApplicationRunListener的日志輸出,發(fā)現(xiàn)主要耗時(shí)都在contextLoadedstarted兩個(gè)階段之間,在這兩個(gè)階段之間調(diào)用了2個(gè)方法:refreshContextafterRefresh方法,而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í)都花在主配置類的@ComponentScan解析上,而且主要耗時(shí)還是在解析屬性basePackages;

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

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

  1. 作為數(shù)據(jù)平臺(tái),我們的服務(wù)引用了很多第三方依賴服務(wù),這些依賴往往提供了對(duì)應(yīng)業(yè)務(wù)的完整功能,所以提供的 jar 包非常大;
  2. 掃描這些包路徑下的 class 非常耗時(shí),很多 class 都不提供 Bean,但還是花時(shí)間掃描了;
  3. 每添加一個(gè)服務(wù)的依賴,都會(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;
    }   
}
復(fù)制代碼

對(duì)于BeanPostProcessor接口的實(shí)現(xiàn)類,其前后置處理過(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;
}
復(fù)制代碼

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

@Component
public class TimeCostBeanPostProcessor implements BeanPostProcessor {
    private MapcostMap = 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;
    }
}
復(fù)制代碼

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ù)查詢大量配置元數(shù)據(jù),并更新到 Redis 緩存中,所以初始化非常慢:

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

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

2 優(yōu)化方案 2.1 如何解決掃描路徑過(guò)多?

想到的解決方案比較簡(jiǎn)單粗暴:
梳理要引入的 Bean,刪掉主配置類上掃描路徑,使用 JavaConfig 的方式顯式手動(dòng)注入。
以 UPM 的依賴為例,之前的注入方式 是,項(xiàng)目依賴其 UpmResourceClient 對(duì)象,Pom 已經(jīng)引用了其 Maven 坐標(biāo),并在主配置類上的scanBasePackages中添加了其服務(wù)路徑:"com.xxx.ad.upm",通過(guò)掃描整個(gè)服務(wù)路徑下的 class,找到 UpmResourceClient 并注入,因?yàn)樵擃愖⒔饬?code>@Service,因此會(huì)注入到服務(wù)的 Spring 上下文中,UpmResourceClient 源碼片段及主配置類如下:

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

@Configuration
public class ThirdPartyBeanConfig {
    @Bean
    public UpmResourceClient upmResourceClient() {
        return new UpmResourceClient();
    }
}
復(fù)制代碼

Tips:如果該 Bean 還依賴其他 Bean,則需要把所依賴的 Bean 都注入; 針對(duì) Bean 依賴情況復(fù)雜的場(chǎng)景梳理起來(lái)就比較麻煩了,所幸項(xiàng)目用到的服務(wù) Bean 依賴關(guān)系都比較簡(jiǎn)單,一些依賴關(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)解決。

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

緩存組件代碼片段:

本次優(yōu)化中,我是通過(guò) 每次刪除一條掃描路徑,啟動(dòng)服務(wù)后根據(jù)啟動(dòng)日志中 Bean 缺失錯(cuò)誤的信息,來(lái)逐個(gè)梳理、添加依賴的 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類型的對(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)在主配置類上復(fù)合注解@SpringBootApplication中的@EnableAutoConfiguration上,該注解開(kāi)啟了 SpringBoot 的自動(dòng)配置功能。該注解中的@Import(AutoConfigurationImportSelector.class)通過(guò)加載META-INF/spring.factotries下配置一系列 *AutoConfiguration 配置類,根據(jù)現(xiàn)有條件推斷,盡可能地為我們配置需要的 Bean。這些配置類負(fù)責(zé)各個(gè)功能的自動(dòng)化配置,其中用于 SpringBoot Cache 的自動(dòng)配置類是CacheAutoConfiguration,接下來(lái)重點(diǎn)分析這個(gè)配置類就行了。

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

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

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

看下RedisCacheConfiguration的實(shí)現(xiàn): 這個(gè)配置類上有很多條件注解,當(dāng)這些條件都滿足的話,這個(gè)自動(dòng)配置類就會(huì)生效,而本項(xiàng)目恰恰都滿足,同時(shí)項(xiàng)目主配置類上還加上了@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注解的類;
2、導(dǎo)入實(shí)現(xiàn)了ImportSelectorImportBeanDefinitionRegistrar的類;
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配置類自動(dòng)引入,而不需要掃描"com.xxx.ad.rediscache"整個(gè)路徑了:

# EnableAutoConfigurations
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxx.ad.rediscache.XxxAdCacheConfiguration
復(fù)制代碼

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

你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機(jī)房具備T級(jí)流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級(jí)服務(wù)器適合批量采購(gòu),新人活動(dòng)首月15元起,快前往官網(wǎng)查看詳情吧


新聞名稱:7min到40s:SpringBoot啟動(dòng)優(yōu)化實(shí)踐-創(chuàng)新互聯(lián)
網(wǎng)站地址:http://weahome.cn/article/dopdhj.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部