javascript
Spring Boot - 自动配置实现原理
文章目錄
- Pre
- @SpringBootApplication 注解
- @ComponentScan 注解
- @SpringBootConfiguration 注解
- @EnableAutoConfiguration 注解
- @AutoConfigurationPackage
- @Import
- @Import(AutoConfigurationImportSelector.class)
- SPI 機(jī)制和 SpringFactoriesLoader
- JDK 中的 SPI 機(jī)制
- SpringFactoriesLoader
- @ConditionalOn 系列條件注解
- @ConditionalOn 系列條件注解的實(shí)現(xiàn)原理
- 小結(jié)
Pre
Spring Boot 中的配置體系是一套強(qiáng)大而復(fù)雜的體系,其中最基礎(chǔ)、最核心的要數(shù)自動配置(AutoConfiguration)機(jī)制了。
今天我們將圍繞這個話題詳細(xì)展開討論,看看 Spring Boot 如何實(shí)現(xiàn)自動配置。那我們就先從 @SpringBootApplication 注解開始講起。
@SpringBootApplication 注解
@SpringBootApplication 注解位于** spring-boot-autoconfigure** 工程的 org.springframework.boot.autoconfigure 包中,定義如下:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = {@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication {@AliasFor(annotation = EnableAutoConfiguration.class)Class<?>[] exclude() default {};@AliasFor(annotation = EnableAutoConfiguration.class)String[] excludeName() default {};@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")String[] scanBasePackages() default {};@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")Class<?>[] scanBasePackageClasses() default {};@AliasFor(annotation = Configuration.class)boolean proxyBeanMethods() default true;}相較一般的注解,@SpringBootApplication 注解顯得有點(diǎn)復(fù)雜。我們可以
- 通過 exclude 和 excludeName 屬性來配置不需要實(shí)現(xiàn)自動裝配的類或類名,
- 也可以通過 scanBasePackages 和 scanBasePackageClasses 屬性來配置需要進(jìn)行掃描的包路徑和類路徑。
注意到 @SpringBootApplication 注解實(shí)際上是一個組合注解,它由三個注解組合而成,分別是 @SpringBootConfiguration、@EnableAutoConfiguration 和 @ComponentScan。
@ComponentScan 注解
@ComponentScan 注解不是 Spring Boot 引入的新注解,而是屬于 Spring 容器管理的內(nèi)容。@ComponentScan 注解就是掃描基于 @Component 等注解所標(biāo)注的類所在包下的所有需要注入的類,并把相關(guān) Bean 定義批量加載到容器中。顯然,Spring Boot 應(yīng)用程序中同樣需要這個功能。
@SpringBootConfiguration 注解
@SpringBootConfiguration 注解比較簡單,事實(shí)上它是一個空注解,只是使用了 Spring 中的 @Configuration 注解。@Configuration 注解比較常見,提供了 JavaConfig 配置類實(shí)現(xiàn)。
@EnableAutoConfiguration 注解
@EnableAutoConfiguration 注解是我們需要重點(diǎn)剖析的對象,下面進(jìn)行重點(diǎn)展開。該注解的定義如下代碼所示:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration {String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";Class<?>[] exclude() default {};String[] excludeName() default {};}這里我們關(guān)注兩個新注解,@AutoConfigurationPackage 和 @Import(AutoConfigurationImportSelector.class)。
@AutoConfigurationPackage
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import(AutoConfigurationPackages.Registrar.class) public @interface AutoConfigurationPackage {}從命名上講,在這個注解中我們對該注解所在包下的類進(jìn)行自動配置,而在實(shí)現(xiàn)方式上用到了 Spring 中的 @Import 注解。在使用 Spring Boot 時,@Import 也是一個非常常見的注解,可以用來動態(tài)創(chuàng)建 Bean。為了便于理解后續(xù)內(nèi)容,這里有必要對 @Import 注解的運(yùn)行機(jī)制做一些展開,該注解定義如下:
@Import
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Import {Class<?>[] value(); }在 @Import 注解的屬性中可以設(shè)置需要引入的類名,例如 @AutoConfigurationPackage 注解上的 @Import(AutoConfigurationPackages.Registrar.class)。根據(jù)該類的不同類型,Spring 容器針對 @Import 注解有以下四種處理方式:
- 如果該類實(shí)現(xiàn)了 ImportSelector 接口,Spring 容器就會實(shí)例化該類,并且調(diào)用其 selectImports 方法;
- 如果該類實(shí)現(xiàn)了 DeferredImportSelector 接口,則 Spring 容器也會實(shí)例化該類并調(diào)用其 selectImports方法。DeferredImportSelector 繼承了 ImportSelector,區(qū)別在于 DeferredImportSelector 實(shí)例的 selectImports 方法調(diào)用時機(jī)晚于 ImportSelector 的實(shí)例,要等到 @Configuration 注解中相關(guān)的業(yè)務(wù)全部都處理完了才會調(diào)用;
- 如果該類實(shí)現(xiàn)了 ImportBeanDefinitionRegistrar 接口,Spring 容器就會實(shí)例化該類,并且調(diào)用其 registerBeanDefinitions 方法;
- 如果該類沒有實(shí)現(xiàn)上述三種接口中的任何一個,Spring 容器就會直接實(shí)例化該類。
有了對 @Import 注解的基本理解,我們再來看 AutoConfigurationPackages.Registrar 類,定義如下:
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {@Overridepublic void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {register(registry, new PackageImport(metadata).getPackageName());}@Overridepublic Set<Object> determineImports(AnnotationMetadata metadata) {return Collections.singleton(new PackageImport(metadata));}}可以看到這個 Registrar 類實(shí)現(xiàn)了前面第三種情況中提到的 ImportBeanDefinitionRegistrar 接口并重寫了 registerBeanDefinitions 方法,該方法中調(diào)用 AutoConfigurationPackages 自身的 register 方法
public static void register(BeanDefinitionRegistry registry, String... packageNames) {if (registry.containsBeanDefinition(BEAN)) {BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();constructorArguments.addIndexedArgumentValue(0,addBasePackages(constructorArguments, packageNames));}else {GenericBeanDefinition beanDefinition = new GenericBeanDefinition();beanDefinition.setBeanClass(BasePackages.class);beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0,packageNames);beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);registry.registerBeanDefinition(BEAN, beanDefinition);} }這個方法的邏輯是先判斷整個 Bean 有沒有被注冊,如果已經(jīng)注冊則獲取 Bean 的定義,通過 Bean 獲取構(gòu)造函數(shù)的參數(shù)并添加參數(shù)值;如果沒有,則創(chuàng)建一個新的 Bean 的定義,設(shè)置 Bean 的類型為 AutoConfigurationPackages 類型并進(jìn)行 Bean 的注冊。
@Import(AutoConfigurationImportSelector.class)
然后我們再來看 @EnableAutoConfiguration 注解中的 @Import(AutoConfigurationImportSelector.class) 部分,首先我們明確 AutoConfigurationImportSelector 類實(shí)現(xiàn)了 @Import 注解第二種情況中的 DeferredImportSelector 接口,所以會執(zhí)行如下所示的 selectImports 方法:
@Overridepublic String[] selectImports(AnnotationMetadata annotationMetadata) {if (!isEnabled(annotationMetadata)) {return NO_IMPORTS;}AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);//獲取 configurations 集合AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,annotationMetadata);return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());}看一下
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,AnnotationMetadata annotationMetadata) {if (!isEnabled(annotationMetadata)) {return EMPTY_ENTRY;}AnnotationAttributes attributes = getAttributes(annotationMetadata);List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);configurations = removeDuplicates(configurations);Set<String> exclusions = getExclusions(annotationMetadata, attributes);checkExcludedClasses(configurations, exclusions);configurations.removeAll(exclusions);configurations = filter(configurations, autoConfigurationMetadata);fireAutoConfigurationImportEvents(configurations, exclusions);return new AutoConfigurationEntry(configurations, exclusions);}這段代碼的核心是通過 getCandidateConfigurations 方法獲取 configurations 集合并進(jìn)行過濾。
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "+ "are using a custom packaging, make sure that file is correct.");return configurations;}Assert 校驗(yàn),該校驗(yàn)是一個非空校驗(yàn),會提示 “在 META-INF/spring.factories 中沒有找到自動配置類” 這個異常信息。
看到這里,不得不提到 JDK 中的 SPI 機(jī)制,因?yàn)闊o論從 SpringFactoriesLoader 這個類的命名上,還是 META-INF/spring.factories 這個文件目錄,兩者之間都存在很大的相通性。
從類名上看,**AutoConfigurationImportSelector 類是一種選擇器,負(fù)責(zé)從各種配置項(xiàng)中找到需要導(dǎo)入的具體配置類。**如下圖所示
顯然,AutoConfigurationImportSelector 所依賴的最關(guān)鍵組件就是 SpringFactoriesLoader,下面我們對其進(jìn)行具體展開。
SPI 機(jī)制和 SpringFactoriesLoader
要想理解 SpringFactoriesLoader 類,我們首先需要了解 JDK 中 SPI(Service Provider Interface,服務(wù)提供者接口)機(jī)制。
JDK 中的 SPI 機(jī)制
JDK 提供了用于服務(wù)查找的一個工具類 java.util.ServiceLoader 來實(shí)現(xiàn) SPI 機(jī)制。當(dāng)服務(wù)提供者提供了服務(wù)接口的一種實(shí)現(xiàn)之后,我們可以在 jar 包的 META-INF/services/ 目錄下創(chuàng)建一個以服務(wù)接口命名的文件,該文件里配置著一組 Key-Value,用于指定服務(wù)接口與實(shí)現(xiàn)該服務(wù)接口具體實(shí)現(xiàn)類的映射關(guān)系。而當(dāng)外部程序裝配這個 jar 包時,就能通過該 jar 包 META-INF/services/ 目錄中的配置文件找到具體的實(shí)現(xiàn)類名,并裝載實(shí)例化,從而完成模塊的注入。
SPI 提供了一種約定,基于該約定就能很好地找到服務(wù)接口的實(shí)現(xiàn)類,而不需要在代碼里硬編碼指定。
JDK 中 SPI 機(jī)制開發(fā)流程如下圖所示:
SpringFactoriesLoader
SpringFactoriesLoader 類似這種 SPI 機(jī)制,只不過以服務(wù)接口命名的文件是放在 META-INF/spring.factories 文件夾下,對應(yīng)的 Key 為 EnableAutoConfiguration。SpringFactoriesLoader 會查找所有 META-INF/spring.factories 文件夾中的配置文件,并把 Key 為 EnableAutoConfiguration 所對應(yīng)的配置項(xiàng)通過反射實(shí)例化為配置類并加載到容器中。
這一點(diǎn)我們可以在 SpringFactoriesLoader 的 loadSpringFactories 方法中進(jìn)行印證:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "+ "are using a custom packaging, make sure that file is correct.");return configurations;}查看看 getSpringFactoriesLoaderFactoryClass()
protected Class<?> getSpringFactoriesLoaderFactoryClass() {return EnableAutoConfiguration.class;}可知 Key 為 EnableAutoConfiguration
緊接著看
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());繼續(xù) loadSpringFactories
以下就是 spring-boot-autoconfigure 工程中所使用的 spring.factories 配置文件片段,可以看到 EnableAutoConfiguration 項(xiàng)中包含了各式各樣的配置項(xiàng),這些配置項(xiàng)在 Spring Boot 啟動過程中都能夠通過 SpringFactoriesLoader 加載到運(yùn)行時環(huán)境,從而實(shí)現(xiàn)自動化配置:
以上就是 Spring Boot 中基于 @SpringBootApplication 注解實(shí)現(xiàn)自動配置的基本過程和原理。當(dāng)然,@SpringBootApplication 注解也可以基于外部配置文件加載配置信息。基于約定優(yōu)于配置思想,Spring Boot 在加載外部配置文件的過程中大量使用了默認(rèn)配置。
@ConditionalOn 系列條件注解
Spring Boot 默認(rèn)提供了 100 多個 AutoConfiguration 類,顯然我們不可能會全部引入。所以在自動裝配時,系統(tǒng)會去類路徑下尋找是否有對應(yīng)的配置類。如果有對應(yīng)的配置類,則按條件進(jìn)行判斷,決定是否需要裝配。這里就引出了在閱讀 Spring Boot 代碼時經(jīng)常會碰到的另一批注解,即 @ConditionalOn 系列條件注解。
我們先通過一個簡單的示例來了解 @ConditionalOn 系列條件注解的使用方式,例如以下代碼就是這類注解的一種典型應(yīng)用,該代碼位于 Spring Cloud Config 的客戶端代碼工程 spring-cloud-config-client 中:
@Bean @ConditionalOnMissingBean(ConfigServicePropertySourceLocator.class) @ConditionalOnProperty(value = "spring.cloud.config.enabled", matchIfMissing = true)public ConfigServicePropertySourceLocator configServicePropertySource(ConfigClientProperties properties) {ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator( properties);return locator;}可以看到,這里運(yùn)用了兩個 @ConditionalOn 注解,一個是 @ConditionalOnMissingBean,一個是 @ConditionalOnProperty。
再比如在 Spring Cloud Config 的服務(wù)器端代碼工程 spring-cloud-config-server 中,存在如下 ConfigServerAutoConfiguration 自動配置類:
@Configuration @ConditionalOnBean(ConfigServerConfiguration.Marker.class) @EnableConfigurationProperties(ConfigServerProperties.class) @Import({ EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class, ResourceRepositoryConfiguration.class,ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class }) public class ConfigServerAutoConfiguration {}這里我們運(yùn)用了 @ConditionalOnBean 注解。實(shí)際上,Spring Boot 中提供了一系列的條件注解,常見的包括:
-
@ConditionalOnProperty:只有當(dāng)所提供的屬性屬于 true 時才會實(shí)例化 Bean
-
@ConditionalOnBean:只有在當(dāng)前上下文中存在某個對象時才會實(shí)例化 Bean
-
@ConditionalOnClass:只有當(dāng)某個 Class 位于類路徑上時才會實(shí)例化 Bean
-
@ConditionalOnExpression:只有當(dāng)表達(dá)式為 true 的時候才會實(shí)例化 Bean
-
@ConditionalOnMissingBean:只有在當(dāng)前上下文中不存在某個對象時才會實(shí)例化 Bean
-
@ConditionalOnMissingClass:只有當(dāng)某個 Class 在類路徑上不存在的時候才會實(shí)例化 Bean
-
@ConditionalOnNotWebApplication:只有當(dāng)不是 Web 應(yīng)用時才會實(shí)例化 Bean
當(dāng)然 Spring Boot 還提供了一些不大常用的 @ConditionalOnXXX 注解,這些注解都定義在 org.springframework.boot.autoconfigure.condition 包中。
顯然上述 ConfigServicePropertySourceLocator 類中只有在 “spring.cloud.config.enabled” 屬性為 true(通過 matchIfMissing 配置項(xiàng)表示默認(rèn)即為 true)以及類路徑上不存在 ConfigServicePropertySourceLocator 時才會進(jìn)行實(shí)例化。
而 ConfigServerAutoConfiguration 只有在類路徑上存在 ConfigServerConfiguration.Marker 類時才會進(jìn)行實(shí)例化,這是一種常用的自動配置控制技巧。
@ConditionalOn 系列條件注解的實(shí)現(xiàn)原理
@ConditionalOn 系列條件注解非常多,我們無意對所有這些組件進(jìn)行展開。事實(shí)上這些注解的實(shí)現(xiàn)原理也大致相同,我們只需要深入了解其中一個就能做到觸類旁通。這里我們挑選 @ConditionalOnClass 注解進(jìn)行展開,該注解定義如下:
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(OnClassCondition.class) public @interface ConditionalOnClass {Class<?>[] value() default {};String[] name() default {}; }可以看到, @ConditionalOnClass 注解本身帶有兩個屬性,一個 Class 類型的 value,一個 String 類型的 name,所以我們可以采用這兩種方式中的任意一種來使用該注解。同時 ConditionalOnClass 注解本身還帶了一個 @Conditional(OnClassCondition.class) 注解。所以, ConditionalOnClass 注解的判斷條件其實(shí)就包含在 OnClassCondition 這個類中。
OnClassCondition 是 SpringBootCondition 的子類,而 SpringBootCondition 又實(shí)現(xiàn)了Condition 接口。
Condition 接口只有一個 matches 方法,如下所示:
@FunctionalInterface public interface Condition {/*** Determine if the condition matches.* @param context the condition context* @param metadata metadata of the {@link org.springframework.core.type.AnnotationMetadata class}* or {@link org.springframework.core.type.MethodMetadata method} being checked* @return {@code true} if the condition matches and the component can be registered,* or {@code false} to veto the annotated component's registration*/boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);}SpringBootCondition 中的 matches 方法實(shí)現(xiàn)如下:
這里的 getClassOrMethodName 方法獲取被添加了@ConditionalOnClass 注解的類或者方法的名稱,而 getMatchOutcome 方法用于獲取匹配的輸出。
我們看到 getMatchOutcome 方法實(shí)際上是一個抽象方法,需要交由 SpringBootCondition 的各個子類完成實(shí)現(xiàn),這里的子類就是 OnClassCondition 類。
在理解 OnClassCondition 時,我們需要明白在 Spring Boot 中,@ConditionalOnClass 或者 @ConditionalOnMissingClass 注解對應(yīng)的條件類都是 OnClassCondition,所以在 OnClassCondition 的 getMatchOutcome 中會同時處理兩種情況。這里我們挑選處理 @ConditionalOnClass 注解的代碼,核心邏輯如下所示:
@Overridepublic ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {ClassLoader classLoader = context.getClassLoader();ConditionMessage matchMessage = ConditionMessage.empty();List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);if (onClasses != null) {List<String> missing = filter(onClasses, ClassNameFilter.MISSING, classLoader);if (!missing.isEmpty()) {return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class).didNotFind("required class", "required classes").items(Style.QUOTE, missing));}matchMessage = matchMessage.andCondition(ConditionalOnClass.class).found("required class", "required classes").items(Style.QUOTE, filter(onClasses, ClassNameFilter.PRESENT, classLoader));}List<String> onMissingClasses = getCandidates(metadata, ConditionalOnMissingClass.class);if (onMissingClasses != null) {List<String> present = filter(onMissingClasses, ClassNameFilter.PRESENT, classLoader);if (!present.isEmpty()) {return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnMissingClass.class).found("unwanted class", "unwanted classes").items(Style.QUOTE, present));}matchMessage = matchMessage.andCondition(ConditionalOnMissingClass.class).didNotFind("unwanted class", "unwanted classes").items(Style.QUOTE, filter(onMissingClasses, ClassNameFilter.MISSING, classLoader));}return ConditionOutcome.match(matchMessage);}這里有兩個方法值得注意,一個是 getCandidates 方法,一個是 getMatches 方法。首先通過 getCandidates 方法獲取了 ConditionalOnClass 的 name 屬性和 value 屬性。然后通過 getMatches 方法將這些屬性值進(jìn)行比對,得到這些屬性所指定的但在類加載器中不存在的類。如果發(fā)現(xiàn)類加載器中應(yīng)該存在但事實(shí)上又不存在的類,則返回一個匹配失敗的 Condition;反之,如果類加載器中存在對應(yīng)類的話,則把匹配信息進(jìn)行記錄并返回一個 ConditionOutcome。
小結(jié)
自動配置是 Spring Boot 最核心和最基本的功能,而 @SpringBootApplication 注解又是 Spring Boot 應(yīng)用程序的入口。本課時從 @SpringBootApplication 注解入手,分析了自動配置機(jī)制的實(shí)現(xiàn)過程。涉及的知識點(diǎn)比較多,包含 JDK 中的 SPI 機(jī)制,以及 @ConditionalOn 系列條件注解。
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的Spring Boot - 自动配置实现原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SpringBoot - Spring
- 下一篇: Spring Boot - 构建数据访