這些不知道,別說你熟悉 Spring

大家好,這篇文章跟大家來聊下 Spring 中提供的常用擴(kuò)展點(diǎn)、Spring SPI 機(jī)制、以及 SpringBoot 自動(dòng)裝配原理,重點(diǎn)介紹下 Spring 基于這些擴(kuò)展點(diǎn)怎么跟配置中心(Apollo、Nacos、Zookeeper、Consul)等做集成。

寫在前面

我們大多數(shù) Java 程序員的日常工作基本都是在做業(yè)務(wù)開發(fā),俗稱 crudboy。

作為 crudboy 的你有沒有這些煩惱呢?

  1. 隨著業(yè)務(wù)的迭代,新功能的加入,代碼變得越來越臃腫,可維護(hù)性越來越低,慢慢變成了屎山

  2. 遇到一些框架層的問題不知道怎么解決

  3. 面試被問到使用的框架、中間件原理、源碼層?xùn)|西,不知道怎么回答

  4. 寫了 5 年代碼了,感覺自己的技術(shù)沒有理想的長(zhǎng)進(jìn)

如果你有上述這些煩惱,我想看優(yōu)秀框架的源碼會(huì)是一個(gè)很好的提升方式。通過看源碼,我們能學(xué)到業(yè)界大佬們優(yōu)秀的設(shè)計(jì)理念、編碼風(fēng)格、設(shè)計(jì)模式的使用、高效數(shù)據(jù)結(jié)構(gòu)算法的使用、魔鬼細(xì)節(jié)的巧妙應(yīng)用等等。這些東西都是助力我們成為一個(gè)優(yōu)秀工程師不可或缺的。

如果你打算要看源碼了,優(yōu)先推薦 Spring、Netty、Mybatis、JUC 包。

Spring 擴(kuò)展

我們知道 Spring 提供了很多的擴(kuò)展點(diǎn),第三方框架整合 Spring 其實(shí)大多也都是基于這些擴(kuò)展點(diǎn)來做的。所以熟練的掌握 Spring 擴(kuò)展能讓我們?cè)陂喿x源碼的時(shí)候能快速的找到入口,然后斷點(diǎn)調(diào)試,一步步深入框架內(nèi)核。

這些擴(kuò)展包括但不限于以下接口:

BeanFactoryPostProcessor:在 Bean 實(shí)例化之前對(duì) BeanDefinition 進(jìn)行修改

BeanPostProcessor:在 Bean 初始化前后對(duì) Bean 進(jìn)行一些修改包裝增強(qiáng),比如返回代理對(duì)象

Aware:一個(gè)標(biāo)記接口,實(shí)現(xiàn)該接口及子接口的類會(huì)收到 Spring 的通知回調(diào),賦予某種 Spring 框架的能力,比如 ApplicationContextAware、EnvironmentAware 等

ApplicationContextInitializer:在上下文準(zhǔn)備階段,容器刷新之前做一些初始化工作,比如我們常用的配置中心 client 基本都是繼承該初始化器,在容器刷新前將配置從遠(yuǎn)程拉到本地,然后封裝成 PropertySource 放到 Environment 中供使用

ApplicationListener:Spring 事件機(jī)制,監(jiān)聽特定的應(yīng)用事件(ApplicationEvent),觀察者模式的一種實(shí)現(xiàn)

FactoryBean:用來自定義 Bean 的創(chuàng)建邏輯(Mybatis、Feign 等等)

ImportBeanDefinitionRegistrar:定義@EnableXXX 注解,在注解上 Import 了一個(gè) ImportBeanDefinitionRegistrar,實(shí)現(xiàn)注冊(cè) BeanDefinition 到容器中

InitializingBean:在 Bean 初始化時(shí)會(huì)調(diào)用執(zhí)行一些初始化邏輯

ApplicationRunner/CommandLineRunner:容器啟動(dòng)后回調(diào),執(zhí)行一些初始化工作

上述列出了幾個(gè)比較常用的接口,但是 Spring 擴(kuò)展遠(yuǎn)不于此,還有很多擴(kuò)展接口大家可以自己去了解。

Spring SPI 機(jī)制

在講接下來內(nèi)容之前,我們先說下 Spring 中的 SPI 機(jī)制。Spring 中的 SPI 主要是利用 META-INF/spring.factories 文件來實(shí)現(xiàn)的,文件內(nèi)容由多個(gè) k = list(v) 的格式組成,比如:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.dtp.starter.adapter.dubbo.autoconfigure.ApacheDubboTpAutoConfiguration,\
  com.dtp.starter.adapter.dubbo.autoconfigure.AlibabaDubboTpAutoConfiguration

org.springframework.boot.env.EnvironmentPostProcessor=\
  com.dtp.starter.zookeeper.autoconfigure.ZkConfigEnvironmentProcessor

這些 spring.factories 文件可能是位于多個(gè) jar 包中,Spring 容器啟動(dòng)時(shí)會(huì)通過 ClassLoader.getResources() 獲取這些 spring.factories 文件的全路徑。然后遍歷路徑以字節(jié)流的形式讀取所有的 k = list(v) 封裝到到一個(gè) Map 中,key 為接口全限定類名,value 為所有實(shí)現(xiàn)類的全限定類名列表。

上述說的這些加載操作都封裝在 SpringFactoriesLoader 類里。該類很簡(jiǎn)單,提供三個(gè)加載方法、一個(gè)實(shí)例化方法,還有一個(gè) cache 屬性,首次加載到的數(shù)據(jù)會(huì)保存在 cache 里,供后續(xù)使用。

[圖片上傳失敗...(image-b8885-1665312357294)]

SpringBoot 核心要點(diǎn)

上面講的 SPI 其實(shí)就是我們 SpringBoot 自動(dòng)裝配的核心。

何為自動(dòng)裝配?

自動(dòng)裝配對(duì)應(yīng)的就是手動(dòng)裝配,在沒 SpringBoot 之前,我們使用 Spring 就是用的手動(dòng)裝配模式。在使用某項(xiàng)第三方功能時(shí),我們需要引入該功能依賴的所有包,并測(cè)試保證這些引入包版本兼容。然后在 XML 文件里進(jìn)行大量標(biāo)簽配置,非常繁瑣。后來 Spring4 里引入了 JavaConfig 功能,利用 @Configuration + @Bean 來代替 XML 配置,雖然對(duì)開發(fā)來說是友好了許多,但是這些模板式配置代碼還是很繁瑣,會(huì)浪費(fèi)大量時(shí)間做配置。Java 重可能也就是這個(gè)時(shí)候給人留的一種印象。

在該背景下出現(xiàn)了 SpringBoot,SpringBoot 可以說是穩(wěn)住了 Java 的地位。SpringBoot 提供了自動(dòng)裝配功能,自動(dòng)裝配簡(jiǎn)單來說就是將某種功能(如 web 相關(guān)、redis 相關(guān)、logging 相關(guān)等)打包在一起,統(tǒng)一管理依賴包版本,并且約定好相關(guān)功能 Bean 的裝配規(guī)則,使用者只需引入一個(gè)依賴,通過少量注解或簡(jiǎn)單配置就可以使用第三方組件提供的功能了。

在 SpringBoot 中這類功能組件有一個(gè)好聽的名字叫做 starter。比如 spring-boot-starter-web、spring-boot-starter-data-redis、spring-boot-starter-logging 等。starter 里會(huì)通過 @Configuration + @Bean + @ConditionalOnXXX 等注解定義要注入 Spring 中的 Bean,然后在 spring.factories 文件中配置為 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的實(shí)現(xiàn),就可以完成自動(dòng)裝配了。

具體裝配流程怎么樣的呢?

其實(shí)也很簡(jiǎn)單,基本都是 Spring 中的知識(shí),沒啥新穎的。主要依托于@EnableAutoConfiguration 注解,該注解上會(huì) Import 一個(gè) AutoConfigurationImportSelector,看下繼承關(guān)系,該類繼承于 DeferredImportSelector。

[圖片上傳失敗...(image-4393cb-1665312357294)]

主要方法為 getAutoConfigurationEntry()

    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
      // 1
      if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
      }
      AnnotationAttributes attributes = getAttributes(annotationMetadata);
      // 2
      List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
      configurations = removeDuplicates(configurations);
      // 3
      Set<String> exclusions = getExclusions(annotationMetadata, attributes);
      checkExcludedClasses(configurations, exclusions);
      configurations.removeAll(exclusions);
      // 4
      configurations = getConfigurationClassFilter().filter(configurations);
      fireAutoConfigurationImportEvents(configurations, exclusions);
      return new AutoConfigurationEntry(configurations, exclusions);
    }

方法解讀

  1. 通過 spring.boot.enableautoconfiguration 配置項(xiàng)判斷是否啟用自動(dòng)裝配,默認(rèn)為 true

  2. 使用上述說的 SpringFactoriesLoader.loadFactoryNames() 加載所有 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的實(shí)現(xiàn)類的全限定類名,借助 HashSet 進(jìn)行去重

  3. 獲取 @EnableAutoConfiguration 注解上配置的要 exclude 的類,然后排除這些特定類

  4. 通過 @ConditionalOnXXX 進(jìn)行過濾,滿足條件的類才會(huì)留下,封裝到 AutoConfigurationEntry 里返回

那 getAutoConfigurationEntry() 方法在哪兒調(diào)用呢?

public void refresh() throws BeansException, IllegalStateException {
                // Allows post-processing of the bean factory in context subclasses.
                postProcessBeanFactory(beanFactory);

                StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
                // Invoke factory processors registered as beans in the context.
                invokeBeanFactoryPostProcessors(beanFactory);

                // Register bean processors that intercept bean creation.
                registerBeanPostProcessors(beanFactory);
                beanPostProcess.end();

                // Initialize message source for this context.
                initMessageSource();

                // Initialize event multicaster for this context.
                initApplicationEventMulticaster();

                // Initialize other special beans in specific context subclasses.
                onRefresh();

                // Check for listener beans and register them.
                registerListeners();

                // Instantiate all remaining (non-lazy-init) singletons.
                finishBeanFactoryInitialization(beanFactory);

                // Last step: publish corresponding event.
                finishRefresh();
    }

以上是 Spring 容器刷新時(shí)的幾個(gè)關(guān)鍵步驟,在步驟二 invokeBeanFactoryPostProcessors() 中會(huì)調(diào)用所有已經(jīng)注冊(cè)的 BeanFactoryPostProcessor 進(jìn)行處理。此處調(diào)用也是有順序的,優(yōu)先會(huì)調(diào)用所有 BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry(),BeanDefinitionRegistryPostProcessor 是一個(gè)特殊的 BeanFactoryPostProcessor,然后再調(diào)用所有 BeanFactoryPostProcessor#postProcessBeanFactory()。

ConfigurationClassPostProcessor 是 BeanDefinitionRegistryPostProcessor 的一個(gè)實(shí)現(xiàn)類,該類主要用來處理 @Configuration 注解標(biāo)注的類。我們用 @Configuration 標(biāo)注的類會(huì)被 ConfigurationClassParser 解析包裝成 ConfigurationClass 對(duì)象,然后再調(diào)用 ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass() 進(jìn)行 BeanDefination 的注冊(cè)。

其中 ConfigurationClassParser 解析時(shí)會(huì)遞歸處理源配置類上的注解(@PropertySource、@ComponentScan、@Import、@ImportResource)、 @Bean 標(biāo)注的方法、接口上的 default 方法,進(jìn)行 ConfigurationClass 類的補(bǔ)全填充,同時(shí)如果該配置類有父類,同樣會(huì)遞歸進(jìn)行處理。具體代碼請(qǐng)看 ConfigurationClassParser#doProcessConfigurationClass() 方法

protected final SourceClass doProcessConfigurationClass(
            ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
            throws IOException {
      
        // Process any @PropertySource annotations

        // Process any @ComponentScan annotations

        // Process any @Import annotations
        processImports(configClass, sourceClass, getImports(sourceClass), filter, true);

        // Process any @ImportResource annotations

        // Process individual @Bean methods
        Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
        for (MethodMetadata methodMetadata : beanMethods) {
             configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
        }

        // Process default methods on interfaces
        processInterfaces(configClass, sourceClass);

        // Process superclass, if any
        if (sourceClass.getMetadata().hasSuperClass()) {
             String superclass = sourceClass.getMetadata().getSuperClassName();
             if (superclass != null && !superclass.startsWith("java") &&
                    !this.knownSuperclasses.containsKey(superclass)) {
                  this.knownSuperclasses.put(superclass, configClass);
                // Superclass found, return its annotation metadata and recurse
                  return sourceClass.getSuperClass();
            }
        }

        // No superclass -> processing is complete
        return null;
    }

1)parser.parse(candidates) 解析得到完整的 ConfigurationClass 對(duì)象,主要填充下圖框中的四部分。

[圖片上傳失敗...(image-76254-1665312357294)]

[圖片上傳失敗...(image-5c0080-1665312357294)]

2)this.reader.loadBeanDefinitions(configClasses) 根據(jù)框中的四部分進(jìn)行 BeanDefination 的注冊(cè)。

[圖片上傳失敗...(image-5780a8-1665312357294)]

在上述 processImports() 過程中會(huì)將 DeferredImportSelector 的實(shí)現(xiàn)類放在 deferredImportSelectorHandler 中以便延遲到所有的解析工作完成后進(jìn)行處理。deferredImportSelectorHandler 中就存放了 AutoConfigurationImportSelector 類的實(shí)例。process() 方法里經(jīng)過幾步走會(huì)調(diào)用到 AutoConfigurationImportSelector#getAutoConfigurationEntry() 方法上獲取到自動(dòng)裝配需要的類,然后進(jìn)行與上述同樣的 ConfigurationClass 解析封裝工作。

[圖片上傳失敗...(image-aeca6b-1665312357294)]

[圖片上傳失敗...(image-c76052-1665312357294)]

代碼層次太深,調(diào)用太復(fù)雜,建議自己斷點(diǎn)調(diào)試源碼跟一遍印象會(huì)更深刻。

ApplicationContextInitializer 調(diào)用時(shí)機(jī)

我們就以 SpringBoot 項(xiàng)目為例來看,在 SpringApplication 的構(gòu)造函數(shù)中會(huì)進(jìn)行 ApplicationContextInitializer 的初始化。

[圖片上傳失敗...(image-6fc3d8-1665312357294)]

上圖中的 getSpringFactoriesInstances 方法內(nèi)部其實(shí)就是調(diào)用 SpringFactoriesLoader.loadFactoryNames 獲取所有 ApplicationContextInitializer 接口的實(shí)現(xiàn)類,然后反射創(chuàng)建對(duì)象,并對(duì)這些對(duì)象進(jìn)行排序(實(shí)現(xiàn)了 Ordered 接口或者加了 @Order 注解)。

    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
      ClassLoader classLoader = getClassLoader();
      // Use names and ensure unique to protect against duplicates
      Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
      List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
      AnnotationAwareOrderComparator.sort(instances);
      return instances;
    }

至此,項(xiàng)目中所有 ApplicationContextInitializer 的實(shí)現(xiàn)已經(jīng)加載并且創(chuàng)建好了。在 prepareContext 階段會(huì)進(jìn)行所有已注冊(cè)的 ApplicationContextInitializer#initialize() 方法的調(diào)用。在此之前prepareEnvironment 階段已經(jīng)準(zhǔn)備好了環(huán)境信息,此處接入配置中心就可以拉到遠(yuǎn)程配置信息然后填充到 Spring 環(huán)境中供應(yīng)用使用。

[圖片上傳失敗...(image-598313-1665312357294)]

SpringBoot 集成 Apollo

ApolloApplicationContextInitializer 實(shí)現(xiàn) ApplicationContextInitializer 接口,并且在 spring.factories 文件中配置如下

[圖片上傳失敗...(image-74709c-1665312357294)]

org.springframework.context.ApplicationContextInitializer=\
com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer

initialize() 方法中會(huì)根據(jù) apollo.bootstrap.namespaces 配置的 namespaces 進(jìn)行配置的拉去,拉去到的配置會(huì)封裝成 ConfigPropertySource 添加到 Spring 環(huán)境 ConfigurableEnvironment 中。具體的拉去流程就不展開講了,感興趣的可以自己去閱讀源碼了解。

SpringCloud 集成 Nacos、Zk、Consul

在 SpringCloud 場(chǎng)景下,SpringCloud 規(guī)范中提供了 PropertySourceBootstrapConfiguration 繼承 ApplicationContextInitializer,另外還提供了個(gè) PropertySourceLocator,二者配合完成配置中心的接入。

[圖片上傳失敗...(image-b8e0eb-1665312357294)]

initialize 方法根據(jù)注入的 PropertySourceLocator 進(jìn)行配置的定位獲取,獲取到的配置封裝成 PropertySource 對(duì)象,然后添加到 Spring 環(huán)境 Environment 中。

[圖片上傳失敗...(image-eecbc0-1665312357294)]

Nacos、Zookeeper、Consul 都有提供相應(yīng) PropertySourceLocator 的實(shí)現(xiàn)

[圖片上傳失敗...(image-58ed26-1665312357294)]

我們來分析下 Nacos 提供的 NacosPropertySourceLocator,locate 方法只提取了主要流程代碼,可以看到 Nacos 啟動(dòng)會(huì)加載以下三種配置文件,也就是我們?cè)?bootstrap.yml 文件里配置的擴(kuò)展配置 extension-configs、共享配置 shared-configs 以及應(yīng)用自己的配置,加載到配置文件后會(huì)封裝成 NacosPropertySource 放到 Spring 的 Environment 中。

[圖片上傳失敗...(image-a540f3-1665312357294)]

public PropertySource<?> locate(Environment env) {
         loadSharedConfiguration(composite);
         loadExtConfiguration(composite);
         loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
         return composite;
    }

loadApplicationConfiguration 加載應(yīng)用配置時(shí),同時(shí)會(huì)加載以下三種配置,分別是

  1. 不帶擴(kuò)展名后綴,application

  2. 帶擴(kuò)展名后綴,application.yml

  3. 帶環(huán)境,帶擴(kuò)展名后綴,application-prod.yml

并且從上到下,優(yōu)先級(jí)依次增高

private void loadApplicationConfiguration(
            CompositePropertySource compositePropertySource, String dataIdPrefix,
            NacosConfigProperties properties, Environment environment) {
        String fileExtension = properties.getFileExtension();
        String nacosGroup = properties.getGroup();
        // load directly once by default
        loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
                fileExtension, true);
        // load with suffix, which have a higher priority than the default
        loadNacosDataIfPresent(compositePropertySource,
                dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
        // Loaded with profile, which have a higher priority than the suffix
        for (String profile : environment.getActiveProfiles()) {
            String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
            loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
                    fileExtension, true);
        }
    }

加載過程中,通過 namespace, dataId, group 唯一定位一個(gè)配置文件

  1. 首先獲取本地緩存的配置,如果有直接返回

  2. 如果步驟1從本地沒找到相應(yīng)配置文件,開始從遠(yuǎn)處拉去,Nacos 2.0 以上版本使用 Grpc 協(xié)議進(jìn)行遠(yuǎn)程通信,1.0 及以下使用 Http 協(xié)議進(jìn)行遠(yuǎn)程通信

  3. 對(duì)拉去到的字符串進(jìn)行解析,封裝成 NacosPropertySource 返回

具體細(xì)節(jié)就不展開講了,可以自己看源碼了解

Zookeeper、Consul 的接入也是非常簡(jiǎn)單,可以自己分析一遍。如果我們有自研的配置中心,需要在 SpringCloud 環(huán)境下使用,可以根據(jù) SpringCloud 提供的這些擴(kuò)展參考以上幾種實(shí)現(xiàn)快速的寫個(gè) starter 進(jìn)行接入。

總結(jié)

本篇文章主要講了下 Spring SPI 機(jī)制、SpringBoot 自動(dòng)裝配原理,以及擴(kuò)展點(diǎn) ApplicationContextInitializer 在集成配置中心時(shí)的應(yīng)用。篇幅有限,一些具體代碼細(xì)節(jié)就沒展開講了,以后會(huì)出些文章針對(duì)某一個(gè)點(diǎn)進(jìn)行詳細(xì)講解。

個(gè)人開源項(xiàng)目

DynamicTp 是一個(gè)基于配置中心實(shí)現(xiàn)的輕量級(jí)動(dòng)態(tài)線程池管理工具,主要功能可以總結(jié)為動(dòng)態(tài)調(diào)參、通知報(bào)警、運(yùn)行監(jiān)控、三方包線程池管理等幾大類。

[圖片上傳失敗...(image-d86dcc-1665312357294)]

目前累計(jì) 2k star,代碼優(yōu)雅,使用了大量設(shè)計(jì)模式,如果你覺得看這些大型框架源碼費(fèi)勁,那么可以嘗試從 DynamicTp 源碼入手,歡迎大家了解試用

官網(wǎng)https://dynamictp.cn

gitee地址https://gitee.com/dromara/dynamic-tp

github地址https://github.com/dromara/dynamic-tp

?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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