大家好蒙挑,這篇文章跟大家來聊下 Spring 中提供的常用擴展點宗侦、Spring SPI 機制、以及 SpringBoot 自動裝配原理忆蚀,重點介紹下 Spring 基于這些擴展點怎么跟配置中心(Apollo矾利、Nacos懊悯、Zookeeper、Consul)等做集成梦皮。
寫在前面
我們大多數(shù) Java 程序員的日常工作基本都是在做業(yè)務(wù)開發(fā)炭分,俗稱 crudboy。
作為 crudboy 的你有沒有這些煩惱呢剑肯?
隨著業(yè)務(wù)的迭代捧毛,新功能的加入,代碼變得越來越臃腫让网,可維護性越來越低呀忧,慢慢變成了屎山
遇到一些框架層的問題不知道怎么解決
面試被問到使用的框架、中間件原理溃睹、源碼層?xùn)|西而账,不知道怎么回答
寫了 5 年代碼了,感覺自己的技術(shù)沒有理想的長進
如果你有上述這些煩惱因篇,我想看優(yōu)秀框架的源碼會是一個很好的提升方式泞辐。通過看源碼,我們能學(xué)到業(yè)界大佬們優(yōu)秀的設(shè)計理念竞滓、編碼風(fēng)格咐吼、設(shè)計模式的使用、高效數(shù)據(jù)結(jié)構(gòu)算法的使用商佑、魔鬼細(xì)節(jié)的巧妙應(yīng)用等等锯茄。這些東西都是助力我們成為一個優(yōu)秀工程師不可或缺的。
如果你打算要看源碼了茶没,優(yōu)先推薦 Spring肌幽、Netty、Mybatis抓半、JUC 包喂急。
Spring 擴展
我們知道 Spring 提供了很多的擴展點,第三方框架整合 Spring 其實大多也都是基于這些擴展點來做的琅关。所以熟練的掌握 Spring 擴展能讓我們在閱讀源碼的時候能快速的找到入口煮岁,然后斷點調(diào)試,一步步深入框架內(nèi)核涣易。
這些擴展包括但不限于以下接口:
BeanFactoryPostProcessor:在 Bean 實例化之前對 BeanDefinition 進行修改
BeanPostProcessor:在 Bean 初始化前后對 Bean 進行一些修改包裝增強,比如返回代理對象
Aware:一個標(biāo)記接口冶伞,實現(xiàn)該接口及子接口的類會收到 Spring 的通知回調(diào)新症,賦予某種 Spring 框架的能力,比如 ApplicationContextAware响禽、EnvironmentAware 等
ApplicationContextInitializer:在上下文準(zhǔn)備階段徒爹,容器刷新之前做一些初始化工作荚醒,比如我們常用的配置中心 client 基本都是繼承該初始化器,在容器刷新前將配置從遠(yuǎn)程拉到本地隆嗅,然后封裝成 PropertySource 放到 Environment 中供使用
ApplicationListener:Spring 事件機制界阁,監(jiān)聽特定的應(yīng)用事件(ApplicationEvent),觀察者模式的一種實現(xiàn)
FactoryBean:用來自定義 Bean 的創(chuàng)建邏輯(Mybatis胖喳、Feign 等等)
ImportBeanDefinitionRegistrar:定義@EnableXXX 注解泡躯,在注解上 Import 了一個 ImportBeanDefinitionRegistrar,實現(xiàn)注冊 BeanDefinition 到容器中
InitializingBean:在 Bean 初始化時會調(diào)用執(zhí)行一些初始化邏輯
ApplicationRunner/CommandLineRunner:容器啟動后回調(diào)丽焊,執(zhí)行一些初始化工作
上述列出了幾個比較常用的接口较剃,但是 Spring 擴展遠(yuǎn)不于此,還有很多擴展接口大家可以自己去了解技健。
Spring SPI 機制
在講接下來內(nèi)容之前写穴,我們先說下 Spring 中的 SPI 機制。Spring 中的 SPI 主要是利用 META-INF/spring.factories 文件來實現(xiàn)的雌贱,文件內(nèi)容由多個 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 文件可能是位于多個 jar 包中,Spring 容器啟動時會通過 ClassLoader.getResources() 獲取這些 spring.factories 文件的全路徑欣孤。然后遍歷路徑以字節(jié)流的形式讀取所有的 k = list(v) 封裝到到一個 Map 中删掀,key 為接口全限定類名,value 為所有實現(xiàn)類的全限定類名列表导街。
上述說的這些加載操作都封裝在 SpringFactoriesLoader 類里披泪。該類很簡單,提供三個加載方法搬瑰、一個實例化方法款票,還有一個 cache 屬性,首次加載到的數(shù)據(jù)會保存在 cache 里泽论,供后續(xù)使用艾少。
[圖片上傳失敗...(image-b8885-1665312357294)]
SpringBoot 核心要點
上面講的 SPI 其實就是我們 SpringBoot 自動裝配的核心。
何為自動裝配翼悴?
自動裝配對應(yīng)的就是手動裝配缚够,在沒 SpringBoot 之前,我們使用 Spring 就是用的手動裝配模式鹦赎。在使用某項第三方功能時谍椅,我們需要引入該功能依賴的所有包,并測試保證這些引入包版本兼容古话。然后在 XML 文件里進行大量標(biāo)簽配置雏吭,非常繁瑣。后來 Spring4 里引入了 JavaConfig 功能陪踩,利用 @Configuration + @Bean 來代替 XML 配置杖们,雖然對開發(fā)來說是友好了許多悉抵,但是這些模板式配置代碼還是很繁瑣,會浪費大量時間做配置摘完。Java 重可能也就是這個時候給人留的一種印象姥饰。
在該背景下出現(xiàn)了 SpringBoot,SpringBoot 可以說是穩(wěn)住了 Java 的地位孝治。SpringBoot 提供了自動裝配功能列粪,自動裝配簡單來說就是將某種功能(如 web 相關(guān)、redis 相關(guān)荆秦、logging 相關(guān)等)打包在一起篱竭,統(tǒng)一管理依賴包版本,并且約定好相關(guān)功能 Bean 的裝配規(guī)則步绸,使用者只需引入一個依賴掺逼,通過少量注解或簡單配置就可以使用第三方組件提供的功能了。
在 SpringBoot 中這類功能組件有一個好聽的名字叫做 starter瓤介。比如 spring-boot-starter-web吕喘、spring-boot-starter-data-redis、spring-boot-starter-logging 等刑桑。starter 里會通過 @Configuration + @Bean + @ConditionalOnXXX 等注解定義要注入 Spring 中的 Bean氯质,然后在 spring.factories 文件中配置為 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的實現(xiàn),就可以完成自動裝配了祠斧。
具體裝配流程怎么樣的呢闻察?
其實也很簡單,基本都是 Spring 中的知識琢锋,沒啥新穎的辕漂。主要依托于@EnableAutoConfiguration 注解,該注解上會 Import 一個 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);
}
方法解讀
通過 spring.boot.enableautoconfiguration 配置項判斷是否啟用自動裝配鲸阻,默認(rèn)為 true
使用上述說的 SpringFactoriesLoader.loadFactoryNames() 加載所有 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的實現(xiàn)類的全限定類名跋涣,借助 HashSet 進行去重
獲取 @EnableAutoConfiguration 注解上配置的要 exclude 的類沦补,然后排除這些特定類
通過 @ConditionalOnXXX 進行過濾汇荐,滿足條件的類才會留下,封裝到 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 容器刷新時的幾個關(guān)鍵步驟遣臼,在步驟二 invokeBeanFactoryPostProcessors() 中會調(diào)用所有已經(jīng)注冊的 BeanFactoryPostProcessor 進行處理性置。此處調(diào)用也是有順序的,優(yōu)先會調(diào)用所有 BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry()揍堰,BeanDefinitionRegistryPostProcessor 是一個特殊的 BeanFactoryPostProcessor鹏浅,然后再調(diào)用所有 BeanFactoryPostProcessor#postProcessBeanFactory()。
ConfigurationClassPostProcessor 是 BeanDefinitionRegistryPostProcessor 的一個實現(xiàn)類屏歹,該類主要用來處理 @Configuration 注解標(biāo)注的類隐砸。我們用 @Configuration 標(biāo)注的類會被 ConfigurationClassParser 解析包裝成 ConfigurationClass 對象,然后再調(diào)用 ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass() 進行 BeanDefination 的注冊蝙眶。
其中 ConfigurationClassParser 解析時會遞歸處理源配置類上的注解(@PropertySource季希、@ComponentScan、@Import幽纷、@ImportResource)式塌、 @Bean 標(biāo)注的方法、接口上的 default 方法友浸,進行 ConfigurationClass 類的補全填充峰尝,同時如果該配置類有父類,同樣會遞歸進行處理收恢。具體代碼請看 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 對象武学,主要填充下圖框中的四部分。
[圖片上傳失敗...(image-76254-1665312357294)]
[圖片上傳失敗...(image-5c0080-1665312357294)]
2)this.reader.loadBeanDefinitions(configClasses) 根據(jù)框中的四部分進行 BeanDefination 的注冊伦意。
[圖片上傳失敗...(image-5780a8-1665312357294)]
在上述 processImports() 過程中會將 DeferredImportSelector 的實現(xiàn)類放在 deferredImportSelectorHandler 中以便延遲到所有的解析工作完成后進行處理火窒。deferredImportSelectorHandler 中就存放了 AutoConfigurationImportSelector 類的實例。process() 方法里經(jīng)過幾步走會調(diào)用到 AutoConfigurationImportSelector#getAutoConfigurationEntry() 方法上獲取到自動裝配需要的類驮肉,然后進行與上述同樣的 ConfigurationClass 解析封裝工作熏矿。
[圖片上傳失敗...(image-aeca6b-1665312357294)]
[圖片上傳失敗...(image-c76052-1665312357294)]
代碼層次太深,調(diào)用太復(fù)雜离钝,建議自己斷點調(diào)試源碼跟一遍印象會更深刻票编。
ApplicationContextInitializer 調(diào)用時機
我們就以 SpringBoot 項目為例來看,在 SpringApplication 的構(gòu)造函數(shù)中會進行 ApplicationContextInitializer 的初始化奈辰。
[圖片上傳失敗...(image-6fc3d8-1665312357294)]
上圖中的 getSpringFactoriesInstances 方法內(nèi)部其實就是調(diào)用 SpringFactoriesLoader.loadFactoryNames 獲取所有 ApplicationContextInitializer 接口的實現(xiàn)類栏妖,然后反射創(chuàng)建對象,并對這些對象進行排序(實現(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;
}
至此吊趾,項目中所有 ApplicationContextInitializer 的實現(xiàn)已經(jīng)加載并且創(chuàng)建好了。在 prepareContext 階段會進行所有已注冊的 ApplicationContextInitializer#initialize() 方法的調(diào)用瑟啃。在此之前prepareEnvironment 階段已經(jīng)準(zhǔn)備好了環(huán)境信息论泛,此處接入配置中心就可以拉到遠(yuǎn)程配置信息然后填充到 Spring 環(huán)境中供應(yīng)用使用。
[圖片上傳失敗...(image-598313-1665312357294)]
SpringBoot 集成 Apollo
ApolloApplicationContextInitializer 實現(xiàn) ApplicationContextInitializer 接口蛹屿,并且在 spring.factories 文件中配置如下
[圖片上傳失敗...(image-74709c-1665312357294)]
org.springframework.context.ApplicationContextInitializer=\
com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer
initialize() 方法中會根據(jù) apollo.bootstrap.namespaces 配置的 namespaces 進行配置的拉去屁奏,拉去到的配置會封裝成 ConfigPropertySource 添加到 Spring 環(huán)境 ConfigurableEnvironment 中。具體的拉去流程就不展開講了错负,感興趣的可以自己去閱讀源碼了解坟瓢。
SpringCloud 集成 Nacos勇边、Zk、Consul
在 SpringCloud 場景下折联,SpringCloud 規(guī)范中提供了 PropertySourceBootstrapConfiguration 繼承 ApplicationContextInitializer粒褒,另外還提供了個 PropertySourceLocator,二者配合完成配置中心的接入诚镰。
[圖片上傳失敗...(image-b8e0eb-1665312357294)]
initialize 方法根據(jù)注入的 PropertySourceLocator 進行配置的定位獲取奕坟,獲取到的配置封裝成 PropertySource 對象,然后添加到 Spring 環(huán)境 Environment 中清笨。
[圖片上傳失敗...(image-eecbc0-1665312357294)]
Nacos月杉、Zookeeper、Consul 都有提供相應(yīng) PropertySourceLocator 的實現(xiàn)
[圖片上傳失敗...(image-58ed26-1665312357294)]
我們來分析下 Nacos 提供的 NacosPropertySourceLocator抠艾,locate 方法只提取了主要流程代碼苛萎,可以看到 Nacos 啟動會加載以下三種配置文件,也就是我們在 bootstrap.yml 文件里配置的擴展配置 extension-configs跌帐、共享配置 shared-configs 以及應(yīng)用自己的配置首懈,加載到配置文件后會封裝成 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)用配置時谨敛,同時會加載以下三種配置究履,分別是
不帶擴展名后綴,application
帶擴展名后綴脸狸,application.yml
帶環(huán)境最仑,帶擴展名后綴,application-prod.yml
并且從上到下炊甲,優(yōu)先級依次增高
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 唯一定位一個配置文件
首先獲取本地緩存的配置,如果有直接返回
如果步驟1從本地沒找到相應(yīng)配置文件卿啡,開始從遠(yuǎn)處拉去吟吝,Nacos 2.0 以上版本使用 Grpc 協(xié)議進行遠(yuǎn)程通信,1.0 及以下使用 Http 協(xié)議進行遠(yuǎn)程通信
對拉去到的字符串進行解析颈娜,封裝成 NacosPropertySource 返回
具體細(xì)節(jié)就不展開講了剑逃,可以自己看源碼了解
Zookeeper、Consul 的接入也是非常簡單官辽,可以自己分析一遍蛹磺。如果我們有自研的配置中心,需要在 SpringCloud 環(huán)境下使用同仆,可以根據(jù) SpringCloud 提供的這些擴展參考以上幾種實現(xiàn)快速的寫個 starter 進行接入萤捆。
總結(jié)
本篇文章主要講了下 Spring SPI 機制、SpringBoot 自動裝配原理,以及擴展點 ApplicationContextInitializer 在集成配置中心時的應(yīng)用俗或。篇幅有限市怎,一些具體代碼細(xì)節(jié)就沒展開講了,以后會出些文章針對某一個點進行詳細(xì)講解蕴侣。
個人開源項目
DynamicTp 是一個基于配置中心實現(xiàn)的輕量級動態(tài)線程池管理工具焰轻,主要功能可以總結(jié)為動態(tài)調(diào)參臭觉、通知報警昆雀、運行監(jiān)控、三方包線程池管理等幾大類蝠筑。
[圖片上傳失敗...(image-d86dcc-1665312357294)]
目前累計 2k star狞膘,代碼優(yōu)雅,使用了大量設(shè)計模式什乙,如果你覺得看這些大型框架源碼費勁挽封,那么可以嘗試從 DynamicTp 源碼入手,歡迎大家了解試用
官網(wǎng):https://dynamictp.cn