一、springboot 自動配置原理
先說說我們自己的應用程式中Bean加入容器的辦法:
package com.ynunicom.dc.dingdingcontractapp; import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author jinye.Bai */ @SpringBootApplication( scanBasePackages ={"com.ynunicom.dc.dingdingcontractapp"} ) public class DingdingContractAppApplication { public static void main(String[] args) { SpringApplication.run(DingdingContractAppApplication.class, args); } }bean加入容器
我們在應用程式的入口設定了 @SpringBootApplication標簽,默認情況下他會掃描所有次級目錄,
如果增加了 scanBasePackages屬性,就會掃描所有被指定的路徑及其次級目錄,
那么它在掃描的是什么東西呢?
是這個:@Component
所有被掃描到的 @Component,都會成為一個默認的singleton(單例,即一個容器里只有一個物件物體)加入到容器中,
認識到以上這點,便于我們理解springboot自動配置的機制,
接下來讓我們看看在自己的應用程式中實作配置的方法,
如圖:
package com.ynunicom.dc.dingdingcontractapp.configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; /** * @author: jinye.Bai * @date: 2020/5/22 15:51 */ @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } }RestTemplateConfig
這里我們設定了一個配置,往容器中加入了一個RestTemplate,
首先說 @Configuration,這個標簽繼承了 @Component標簽,我們可以在標簽內容看到:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.context.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; import org.springframework.stereotype.Component; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Configuration { @AliasFor( annotation = Component.class ) String value() default ""; }Configuration
可以看到其中是有 @Component標簽的,所以,@Configuration會被 @SpringBootApplication掃描到,進而把它和它下面的 @Bean加入容器,于是我們 RestTemplate的內容就配置完成了,在后續的使用中,我們就可以直接從容器中拿出RestTemplate使用它,
對于在maven中參考的其他外部包加入容器的程序,需要用到spring.factories,
二、spring.factories檔案的作用
在springboot運行時,SpringFactoriesLoader 類會去尋找
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
我們以mybatis-plus為例,
首先我們引入:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
maven引包
然后去maven的依賴里看它的自動配置類MybatisPlusAutoConfiguration
@Configuration @ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class}) @ConditionalOnSingleCandidate(DataSource.class) @EnableConfigurationProperties({MybatisPlusProperties.class}) @AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class}) public class MybatisPlusAutoConfiguration implements InitializingBean {MybatisPlusAutoConfiguration
可以看到有上文提到的 @Configuration,還有從application.yml載入自動配置的 @EnableConfigurationProperties({MybatisPlusProperties.class})
這個注解的具體內容請查看我另一篇博文,對其進行了解釋:
迅速學會@ConfigurationProperties的使用
也就是說,springboot只要能掃描到MybatisPlusAutoConfiguration類的 @Configuration注解,其中的所有配置就能自動加入到容器中,這一程序由上面提到的SpringFactoriesLoader 起作用,它會去尋找 “META-INF/spring.factories” 檔案,我們可以在 mybatis-plus的依賴中找到它:
SpringFactoriesLoader為什么要讀取它呢?因為它內部是這樣的
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.mybatisplus.autoconfigure.MybatisPlusLanguageDriverAutoConfiguration,\
com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration
factories
spring.factories用鍵值對的方式記錄了所有需要加入容器的類,EnableAutoConfigurationImportSelector的selectImports方法回傳的類名,來自spring.factories檔案內的配置資訊,這些配置資訊的key等于EnableAutoConfiguration,因為spring boot應用啟動時使用了EnableAutoConfiguration注解,所以EnableAutoConfiguration注解通過import注解將EnableAutoConfigurationImportSelector類實體化,并且將其selectImports方法回傳的類名實體化后注冊到spring容器,
以上內容是springboot獲得這些類的方式,如果你想要實作自己的自動配置,就將你的類通過鍵值對的方式寫在你的spring.factories即可,注意,值是你的自動配置類,鍵必須是org.springframework.boot.autoconfigure.EnableAutoConfiguration
spring.factories 的妙用
現象
在閱讀 Spring-Boot 相關原始碼時,常常見到 spring.factories 檔案,里面寫了自動配置(AutoConfiguration)相關的類名,因此產生了一個疑問:“明明自動配置的類已經打上了 @Configuration 的注解,為什么還要寫 spring.factories 檔案?
用過 Spring Boot 的都知道
@ComponentScan 注解的作用是掃描 @SpringBootApplication 所在的 Application 類所在的包(basepackage)下所有的 @component 注解(或拓展了 @component 的注解)標記的 bean,并注冊到 spring 容器中,
那么問題來了
在 Spring Boot 專案中,如果你想要被 Spring 容器管理的 bean 不在 Spring Boot 包掃描路徑下,怎么辦?
解決 Spring Boot 中不能被默認路徑掃描的配置類的方式,有 2 種:
(1)在 Spring Boot 主類上使用 @Import 注解
(2)使用 spring.factories 檔案
以下是對 使用 spring.factories 檔案的簡單理解
Spring Boot 的擴展機制之 Spring Factories
Spring Boot 中有一種非常解耦的擴展機制:Spring Factories,這種擴展機制實際上是仿照Java中的SPI擴展機制來實作的,
什么是 SPI 機制?
SPI 的全名為 Service Provider Interface.大多數開發人員可能不熟悉,因為這個是針對廠商或者插件的,在java.util.ServiceLoader的檔案里有比較詳細的介紹,
簡單的總結下 java SPI 機制的思想,我們系統里抽象的各個模塊,往往有很多不同的實作方案,比如日志模塊的方案,xml決議模塊、jdbc模塊的方案等,面向的物件的設計里,我們一般推薦模塊之間基于介面編程,模塊之間不對實作類進行硬編碼,一旦代碼里涉及具體的實作類,就違反了可拔插的原則,如果需要替換一種實作,就需要修改代碼,為了實作在模塊裝配的時候能不在程式里動態指明,這就需要一種服務發現機制,java SPI
java SPI 就是提供這樣的一個機制:為某個介面尋找服務實作的機制,有點類似IOC的思想,就是將裝配的控制權移到程式之外,在模塊化設計中這個機制尤其重要,
Spring Boot 中的 SPI 機制
在 Spring 中也有一種類似與 Java SPI 的加載機制,它在 resources/META-INF/spring.factories 檔案中配置介面的實作類名稱,然后在程式中讀取這些組態檔并實體化,
在 Spring 中也有一種類似與 Java SPI 的加載機制,它在 resources/META-INF/spring.factories 檔案中配置介面的實作類名稱,然后在程式中讀取這些組態檔并實體化,
這種自定義的SPI機制是 Spring Boot Starter 實作的基礎,

Spring Factories 實作原理是什么?
spring-core 包里定義了 SpringFactoriesLoader 類,這個類實作了檢索 META-INF/spring.factories 檔案,并獲取指定介面的配置的功能,在這個類中定義了兩個對外的方法:
loadFactories 根據介面類獲取其實作類的實體,這個方法回傳的是物件串列, loadFactoryNames 根據介面獲取其介面類的名稱,這個方法回傳的是類名的串列,實作
上面的兩個方法的關鍵都是從指定的 ClassLoader 中獲取 spring.factories 檔案,并決議得到類名串列,具體代碼如下
public final class SpringFactoriesLoader { public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class); private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap(); private SpringFactoriesLoader() {} public static <T> List<T> loadFactories(Class<T> factoryClass, @Nullable ClassLoader classLoader) { Assert.notNull(factoryClass, "'factoryClass' must not be null"); ClassLoader classLoaderToUse = classLoader; if (classLoader == null) { classLoaderToUse = SpringFactoriesLoader.class.getClassLoader(); } List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse); if (logger.isTraceEnabled()) { logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames); } List<T> result = new ArrayList(factoryNames.size()); Iterator var5 = factoryNames.iterator(); while(var5.hasNext()) { String factoryName = (String)var5.next(); result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse)); } AnnotationAwareOrderComparator.sort(result); return result; } public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList()); } private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader); if (result != null) { return result; } else { try { Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories"); LinkedMultiValueMap result = new LinkedMultiValueMap(); while(urls.hasMoreElements()) { URL url = (URL)urls.nextElement(); UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); Iterator var6 = properties.entrySet().iterator(); while(var6.hasNext()) { Entry<?, ?> entry = (Entry)var6.next(); String factoryClassName = ((String)entry.getKey()).trim(); String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue()); int var10 = var9.length; for(int var11 = 0; var11 < var10; ++var11) { String factoryName = var9[var11]; result.add(factoryClassName, factoryName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException var13) { throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13); } } } private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) { try { Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader); if (!factoryClass.isAssignableFrom(instanceClass)) { throw new IllegalArgumentException("Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]"); } else { return ReflectionUtils.accessibleConstructor(instanceClass, new Class[0]).newInstance(); } } catch (Throwable var4) { throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), var4); } } }SpringFactoriesLoader
從代碼中我們可以知道,在這個方法中會遍歷整個 spring-boot 專案的 classpath 下 ClassLoader 中所有 jar 包下的 spring.factories檔案,也就是說我們可以在自己的 jar 中配置 spring.factories 檔案,不會影響到其它地方的配置,也不會被別人的配置覆寫,
Spring Factories 在 Spring Boot 中的應用
在 Spring Boot 的很多包中都能夠找到 spring.factories 檔案,接下來我們以 spring-boot-autoconfigure 包為例進行介紹
# Initializers org.springframework.context.ApplicationContextInitializer=\ org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\ org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener # Application Listeners org.springframework.context.ApplicationListener=\ org.springframework.boot.autoconfigure.BackgroundPreinitializer # Auto Configuration Import Listeners org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\ org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener # Auto Configuration Import Filters org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\ org.springframework.boot.autoconfigure.condition.OnBeanCondition,\ org.springframework.boot.autoconfigure.condition.OnClassCondition,\ org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\ org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\ org.springframework.boot.autoconfigure.cache.CacheAutoConfigurationspring-boot-autoconfigure 包
結合前面的內容,可以看出 spring.factories 檔案可以將 spring-boot 專案包以外的 bean(即在 pom 檔案中添加依賴中的 bean)注冊到 spring-boot 專案的 spring 容器,
由于@ComponentScan 注解只能掃描 spring-boot 專案包內的 bean 并注冊到 spring 容器中,因此需要 @EnableAutoConfiguration 注解來注冊專案包外的bean,
而 spring.factories 檔案,則是用來記錄專案包外需要注冊的bean類名,
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持我們,
轉自:https://www.zhangshengrong.com/p/281oqxEqNw/
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/543846.html
標籤:Java
上一篇:1.redis面試
