SpringBoot原始碼分析之配置環境的構造過程
SpringBoot把配置檔案的載入封裝成了PropertySourceLoader介面,該介面的定義如下:
public interface PropertySourceLoader {
// 支援的檔案字尾
String[] getFileExtensions();
// 把資源Resource載入成屬性源PropertySource
PropertySource<?> load(String name, Resource resource, String profile)
throws IOException;
}
PropertySource是Spring對name/value鍵值對的封裝介面。該定義了getSource()方法,這個方法會返回得到屬性源的源頭。比如MapPropertySource的源頭就是一個Map,PropertiesPropertySource的源頭就是一個Properties。
PropertySource目前的實現類有不少,比如上面提到的MapPropertySource和PropertiesPropertySource,還有RandomValuePropertySource(source是Random)、SimpleCommandLinePropertySource(source是CommandLineArgs,命令列引數)、ServletConfigPropertySource(source是ServletConfig)等等。
PropertySourceLoader介面目前有兩個實現類:PropertiesPropertySourceLoader和YamlPropertySourceLoader。
PropertiesPropertySourceLoader支援從xml或properties格式的檔案中載入資料。
YamlPropertySourceLoader支援從yml或者yaml格式的檔案中載入資料。
Environment的構造以及PropertySource的生成
Environment介面是Spring對當前程式執行期間的環境的封裝。主要提供了兩大功能:profile和property(父介面PropertyResolver提供)。目前主要有StandardEnvironment、StandardServletEnvironment和MockEnvironment3種實現,分別代表普通程式、Web程式以及測試程式的環境。
下面這段程式碼就是SpringBoot的run方法內呼叫的,它會在Spring容器構造之前呼叫,建立環境資訊:
// SpringApplication.class
private ConfigurableApplicationContext createAndRefreshContext(
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
ConfigurableApplicationContext context;
// 如果是web環境,建立StandardServletEnvironment
// 否則,建立StandardEnvironment
// StandardServletEnvironment繼承自StandardEnvironment,StandardEnvironment繼承AbstractEnvironment
// AbstractEnvironment內部有個MutablePropertySources型別的propertySources屬性,用於儲存多個屬性源PropertySource
// StandardEnvironment構造的時候會預設加上2個PropertySource。分別是MapPropertySource(呼叫System.getProperties()配置)和SystemEnvironmentPropertySource(呼叫System.getenv()配置)
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 如果設定了一些啟動引數args,新增基於args的SimpleCommandLinePropertySource
// 還會配置profile資訊,比如設定了spring.profiles.active啟動引數,設定到環境資訊中
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 觸發ApplicationEnvironmentPreparedEvent事件
listeners.environmentPrepared(environment);
...
}
在SpringBoot原始碼分析之SpringBoot的啟動過程這篇文章中,我們分析過SpringApplication啟動的時候會使用工廠載入機制初始化一些初始化器和監聽器。其中org.springframework.boot.context.config.ConfigFileApplicationListener這個監聽器會被載入:
// spring-boot-version.release/META-INF/spring.factories
org.springframework.context.ApplicationListener=\
...
org.springframework.boot.context.config.ConfigFileApplicationListener,\
...
ConfigFileApplicationListener會監聽SpringApplication啟動的時候發生的事件,它的監聽程式碼:
@Override
public void onApplicationEvent(ApplicationEvent event) {
// 應用環境資訊準備好的時候對應的事件。此時Spring容器尚未建立,但是環境已經建立
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent(
(ApplicationEnvironmentPreparedEvent) event);
}
// Spring容器建立完成並在refresh方法呼叫之前對應的事件
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
}
private void onApplicationEnvironmentPreparedEvent(
ApplicationEnvironmentPreparedEvent event) {
// 使用工廠載入機制讀取key為org.springframework.boot.env.EnvironmentPostProcessor的實現類
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
// 加上自己。ConfigFileApplicationListener也是一個EnvironmentPostProcessor介面的實現類
postProcessors.add(this);
// 排序
AnnotationAwareOrderComparator.sort(postProcessors);
// 遍歷這些EnvironmentPostProcessor,並呼叫postProcessEnvironment方法
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(),
event.getSpringApplication());
}
}
ConfigFileApplicationListener也是一個EnvironmentPostProcessor介面的實現類,在這裡會被呼叫:
// ConfigFileApplicationListener的postProcessEnvironment方法
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
// 新增屬性源到環境中
addPropertySources(environment, application.getResourceLoader());
// 配置需要ignore的beaninfo
configureIgnoreBeanInfo(environment);
// 從環境中繫結一些引數到SpringApplication中
bindToSpringApplication(environment, application);
}
protected void addPropertySources(ConfigurableEnvironment environment,
ResourceLoader resourceLoader) {
// 新增一個RandomValuePropertySource到環境中
// RandomValuePropertySource是一個用於處理隨機數的PropertySource,內部儲存一個Random類的例項
RandomValuePropertySource.addToEnvironment(environment);
try {
// 構造一個內部類Loader,並呼叫它的load方法
new Loader(environment, resourceLoader).load();
}
catch (IOException ex) {
throw new IllegalStateException("Unable to load configuration files", ex);
}
}
內部類Loader的處理過程整理如下:
- 建立PropertySourcesLoader。PropertySourcesLoader內部有2個屬性,分別是PropertySourceLoader集合和MutablePropertySources(內部有PropertySource的集合)。最終載入完畢之後MutablePropertySources屬性中的PropertySource會被新增到環境Environment中的屬性源列表中。PropertySourcesLoader被構造的時候會使用工廠載入機制獲得PropertySourceLoader集合(預設就2個:PropertiesPropertySourceLoader和YamlPropertySourceLoader;可以自己擴充套件),然後設定到屬性中
- 獲取環境資訊中啟用的profile(啟動專案時設定的spring.profiles.active引數)。如果沒設定profile,預設使用default這個profile,並新增到profiles佇列中。最後會新增一個null到profiles佇列中(為了獲取沒有指定profile的配置檔案。比如環境中有application.yml和appliation-dev.yml,這個null就保證優先載入application.yml檔案)
- profiles佇列取出profile資料,使用PropertySourcesLoader內部的各個PropertySourceLoader支援的字尾去目錄(預設識別4種目錄classpath:/[類載入目錄],classpath:/config/[類載入目錄下的config目錄],file:./[當前目錄],file:./config/[當前目錄下的config目錄])查詢application檔名(這4個目錄是預設的,可以通過啟動引數spring.config.location新增新的目錄,檔名可以通過啟動引數spring.config.name修改)。比如目錄是file:/,檔名是application,字尾為properties,那麼就會查詢file:/application.properties檔案,如果找到,執行第4步
- 找出的屬性原始檔被載入,然後新增到PropertySourcesLoader內部的PropertySourceLoader集合中。如果該屬性原始檔中存在spring.profiles.active配置,識別出來並加入第2步中的profiles佇列,然後重複第3步
- 第4步找到的屬性源從PropertySourcesLoader中全部新增到環境資訊Environment中。如果這些屬性源存在defaultProperties配置,那麼會新增到Environment中的屬性源集合頭部,否則新增到尾部
比如專案中classpath下存在application.yml檔案和application-dev.yml,application.yml檔案的內容如下:
spring.profiles.active: dev
直接啟動專案,開始解析,過程如下:
- 從環境資訊中找出是否設定profile,發現沒有設定。 新增預設的profile - default,然後新增到佇列裡,最後新增null的profile。此時profiles佇列中有2個元素:default和null
- profiles佇列中先拿出null的profile。然後遍歷4個目錄和2個PropertySourceLoader中的4個字尾(PropertiesPropertySourceLoader的properties和xml以及YamlPropertySourceLoader的yml和yaml)的application檔名。file:./config/application.properties、file:./application.properties、classpath:/config/application.properties、classpath:/application.properties、file:./config/application.xml; file:./application.xml ....
- 找到classpath:/application.yml檔案,解析成PropertySource並新增到PropertySourcesLoader裡的MutablePropertySources中。由於該檔案存在spring.profiles.active配置,把dev新增到profiles佇列中
- profiles佇列拿出dev這個profile。由於存在profile,尋找檔案的時候會帶上profile,重複第3步,比如classpath:/application-dev.yml...
- 找到classpath:/application-dev.yml檔案,解析成PropertySource並新增到PropertySourcesLoader裡的MutablePropertySources中
- profiles佇列拿出default這個profile。尋找檔案發現沒有找到。結束
這裡需要注意一下一些常用的額外引數的問題,整理如下:
- 如果啟動程式的時候設定了系統引數spring.profiles.active,那麼這個引數會被設定到環境資訊中(由於設定了系統引數,在StandardEnvironment的鉤子方法customizePropertySources中被封裝成MapPropertySource並新增到Environment中)。這樣PropertySourcesLoader載入的時候不會加上default這個預設profile,但是還是會讀取profile為null的配置資訊。spring.profiles.active支援多個profile,比如java -Dspring.profiles.active="dev,custom" -jar yourjar.jar
- 如果設定程式引數spring.config.location,那麼查詢目錄的時候會多出設定的目錄,也支援多個目錄的設定。這些會在SpringApplication裡的configureEnvironment方法中被封裝成SimpleCommandLinePropertySource並新增到Environment中。比如java -jar yourjar.jar --spring.config.location=classpath:/custom,file:./custom 1 2 3。有4個引數會被設定到SimpleCommandLinePropertySource中。解析檔案的時候會多出2個目錄,分別是classpath:/custom和file:./custom
- 如果設定程式引數spring.config.name,那麼查詢的檔名就是這個引數值。原理跟spring.config.location一樣,都封裝到了SimpleCommandLinePropertySource中。比如java -jar yourjar.jar --spring.config.name=myfile。 這樣會去查詢myfile檔案,而不是預設的application檔案
- 如果設定程式引數spring.profiles.active。注意這是程式引數,不是系統引數。比如java -jar yourjar.jar --spring.profiles.active=prod。會去解析prod這個profile(不論是系統引數還是程式引數,都會被封裝成多個PropertySource存在於環境資訊中。最終獲取profile的時候會去環境資訊中拿,且都可以拿到)
- 上面說的每個profile都是在不同檔案裡的。不同profile也可以存在在一個檔案裡。因為有profile會去載入帶profile的檔案的同時也會去載入不帶profile的檔案,並解析出這個檔案中spring.profiles對應的值是profile的資料。比如profile為prod,會去查詢application-prod.yml檔案,也會去查詢application.yml檔案,其中application.yml檔案只會查詢spring.profiles為prod的資料
比如第6點中profile.yml的資料如下:
spring:
profiles: prod
my.name: 1
---
spring:
profiles: dev
my.name: 2
這裡會解析出spring.profiles為prod的資料,也就是my.name為1的資料。
優先順序的問題:由於環境資訊Environment中儲存的PropertySource是MutablePropertySources,那麼會去配置值的時候就存在優先順序的問題。比如PropertySource1和PropertySource2都存在custom.name配置,那麼會從哪個PropertySource中獲取這個custom.name配置呢?它會遍歷內部的PropertySource列表,越在前面的PropertySource,越先獲取;比如PropertySource1在PropertySource2前面,那麼會先獲取PropertySource1的配置。MutablePropertySources內部新增PropertySource的時候可以選擇元素的位置,可以addFirst,也可以addLast,也可以自定義位置。
總結:SpringApplication啟動的時候會構造環境資訊Environment,如果是web環境,建立StandardServletEnvironment,否則,建立StandardEnvironment。這兩種環境建立的時候都會在內部的propertySources屬性中加入一些PropertySource。比如屬性屬性的配置資訊封裝成MapPropertySource,系統環境配置資訊封裝成SystemEnvironmentPropertySource等。這些PropertySource集合存在在環境資訊中,從環境資訊中讀取配置的話會遍歷這些PropertySource並找到相對應的配置和值。Environment構造完成之後會讀取springboot相應的配置檔案,從3個角度去查詢:目錄、檔名和profile。這3個角度有預設值,可以進行覆蓋。springboot相關的配置檔案讀取完成之後會被封裝成PropertySource並新增到環境資訊中。
@ConfigurationProperties和@EnableConfigurationProperties註解的原理
SpringBoot內部規定了一套配置和配置屬性類對映規則,可以使用@ConfigurationProperties註解配合字首屬性完成屬性類的讀取;再通過@EnableConfigurationProperties註解設定配置類就可以把這個配置類注入進來。
比如ES的配置類ElasticsearchProperties和對應的@EnableConfigurationProperties修飾的類ElasticsearchAutoConfiguration:
// 使用字首為spring.data.elasticsearch的配置
@ConfigurationProperties(prefix = "spring.data.elasticsearch")
public class ElasticsearchProperties {
private String clusterName = "elasticsearch";
private String clusterNodes;
private Map<String, String> properties = new HashMap<String, String>();
...
}
@Configuration
@ConditionalOnClass({ Client.class, TransportClientFactoryBean.class,
NodeClientFactoryBean.class })
// 使用@EnableConfigurationProperties註解讓ElasticsearchProperties配置生效
// 這樣ElasticsearchProperties就會自動注入到屬性中
@EnableConfigurationProperties(ElasticsearchProperties.class)
public class ElasticsearchAutoConfiguration implements DisposableBean {
...
@Autowired
private ElasticsearchProperties properties;
...
}
我們分析下這個過程的實現。
@EnableConfigurationProperties註解有個屬性value,是個Class陣列,它會匯入一個selector:EnableConfigurationPropertiesImportSelector。這個selector的selectImport方法:
@Override
public String[] selectImports(AnnotationMetadata metadata) {
// 獲取@EnableConfigurationProperties註解的屬性
MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(
EnableConfigurationProperties.class.getName(), false);
// 得到value屬性,是個Class陣列
Object[] type = attributes == null ? null
: (Object[]) attributes.getFirst("value");
if (type == null || type.length == 0) { // 如果value屬性不存在
return new String[] {
// 返回Registrar,Registrar內部會註冊bean
ConfigurationPropertiesBindingPostProcessorRegistrar.class
.getName() };
}
// 如果value屬性存在
// 返回Registrar,Registrar內部會註冊bean
return new String[] { ConfigurationPropertiesBeanRegistrar.class.getName(),
ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };
}
ConfigurationPropertiesBeanRegistrar和ConfigurationPropertiesBindingPostProcessorRegistrar都實現了ImportBeanDefinitionRegistrar介面,會額外註冊bean。
// ConfigurationPropertiesBeanRegistrar的registerBeanDefinitions方法
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 獲取@EnableConfigurationProperties註解中的屬性值Class陣列
MultiValueMap<String, Object> attributes = metadata
.getAllAnnotationAttributes(
EnableConfigurationProperties.class.getName(), false);
List<Class<?>> types = collectClasses(attributes.get("value"));
// 遍歷這些Class陣列
for (Class<?> type : types) {
// 如果這個class被@ConfigurationProperties註解修飾
// 獲取@ConfigurationProperties註解中的字首屬性
// 否則該字首為空字串
String prefix = extractPrefix(type);
// 構造bean的名字: 字首-類全名
// 比如ElasticsearchProperties對應的bean名字就是spring.data.elasticsearch-org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchProperties
String name = (StringUtils.hasText(prefix) ? prefix + "-" + type.getName()
: type.getName());
if (!registry.containsBeanDefinition(name)) {
// 這個bean沒被註冊的話進行註冊
registerBeanDefinition(registry, type, name);
}
}
}
// ConfigurationPropertiesBindingPostProcessorRegistrar的registerBeanDefinitions方法
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
// 先判斷Spring容器裡是否有ConfigurationPropertiesBindingPostProcessor型別的bean
// 由於條件裡面會判斷是否已經存在這個ConfigurationPropertiesBindingPostProcessor型別的bean
// 所以實際上條件裡的程式碼只會執行一次
if (!registry.containsBeanDefinition(BINDER_BEAN_NAME)) {
BeanDefinitionBuilder meta = BeanDefinitionBuilder
.genericBeanDefinition(ConfigurationBeanFactoryMetaData.class);
BeanDefinitionBuilder bean = BeanDefinitionBuilder.genericBeanDefinition(
ConfigurationPropertiesBindingPostProcessor.class);
bean.addPropertyReference("beanMetaDataStore", METADATA_BEAN_NAME);
registry.registerBeanDefinition(BINDER_BEAN_NAME, bean.getBeanDefinition());
registry.registerBeanDefinition(METADATA_BEAN_NAME, meta.getBeanDefinition());
}
}
ConfigurationPropertiesBindingPostProcessor在ConfigurationPropertiesBindingPostProcessorRegistrar中被註冊到Spring容器中,它是一個BeanPostProcessor,它的postProcessBeforeInitialization方法如下:
// Spring容器中bean被例項化之前要做的事
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
// 先獲取bean對應的Class中的@ConfigurationProperties註解
ConfigurationProperties annotation = AnnotationUtils
.findAnnotation(bean.getClass(), ConfigurationProperties.class);
// 如果@ConfigurationProperties註解,說明這是一個配置類。比如ElasticsearchProperties
if (annotation != null) {
// 呼叫postProcessBeforeInitialization方法
postProcessBeforeInitialization(bean, beanName, annotation);
}
// 同樣的方法使用beanName去查詢
annotation = this.beans.findFactoryAnnotation(beanName,
ConfigurationProperties.class);
if (annotation != null) {
postProcessBeforeInitialization(bean, beanName, annotation);
}
return bean;
}
private void postProcessBeforeInitialization(Object bean, String beanName,
ConfigurationProperties annotation) {
Object target = bean;
// 構造一個PropertiesConfigurationFactory
PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>(
target);
// 設定屬性源,這裡的屬性源從環境資訊Environment中得到
factory.setPropertySources(this.propertySources);
// 設定驗證器
factory.setValidator(determineValidator(bean));
// 設定ConversionService
factory.setConversionService(this.conversionService == null
? getDefaultConversionService() : this.conversionService);
if (annotation != null) {
// 設定@ConfigurationProperties註解對應的屬性到PropertiesConfigurationFactory中
// 比如是否忽略不合法的屬性ignoreInvalidFields、忽略未知的欄位、忽略巢狀屬性、驗證器驗證不合法後是否丟擲異常
factory.setIgnoreInvalidFields(annotation.ignoreInvalidFields());
factory.setIgnoreUnknownFields(annotation.ignoreUnknownFields());
factory.setExceptionIfInvalid(annotation.exceptionIfInvalid());
factory.setIgnoreNestedProperties(annotation.ignoreNestedProperties());
if (StringUtils.hasLength(annotation.prefix())) {
// 設定字首
factory.setTargetName(annotation.prefix());
}
}
try {
// 繫結屬性到配置類中,比如ElasticsearchProperties
// 會使用環境資訊中的屬性源進行繫結
// 這樣配置類就讀取到了配置檔案中的配置
factory.bindPropertiesToTarget();
}
catch (Exception ex) {
String targetClass = ClassUtils.getShortName(target.getClass());
throw new BeanCreationException(beanName, "Could not bind properties to "
+ targetClass + " (" + getAnnotationDetails(annotation) + ")", ex);
}
}
總結:SpringBoot內部規定了一套配置和配置屬性類對映規則,可以使用@ConfigurationProperties註解配合字首屬性完成屬性類的讀取;再通過@EnableConfigurationProperties註解設定配置類就可以把這個配置類注入進來。由於這個配置類是被注入進來的,所以它肯定在Spring容器中存在;這是因為在ConfigurationPropertiesBeanRegistrar內部會註冊配置類到Spring容器中,這個配置類的例項化過程在ConfigurationPropertiesBindingPostProcessor這個BeanPostProcessor完成,它會在例項化bean之前會判斷bean是否被@ConfigurationProperties註解修飾,如果有,使用PropertiesConfigurationFactory從環境資訊Environment中進行值的繫結。這個ConfigurationPropertiesBeanRegistrar是在使用@EnableConfigurationProperties註解的時候被建立的(通過EnableConfigurationPropertiesImportSelector)。配置類內部屬性的繫結成功與否是通過環境資訊Environment中的屬性源PropertySource決定的。
相關文章
- 從SpringBoot原始碼分析 主程式配置類載入過程Spring Boot原始碼
- Spring原始碼分析之`BeanFactoryPostProcessor`呼叫過程Spring原始碼Bean
- MyCAT原始碼分析——分析環境部署原始碼
- vue原始碼分析系列之入debug環境搭建Vue原始碼
- SpringBoot配置檔案讀取過程分析Spring Boot
- Spring原始碼分析之Bean的建立過程詳解Spring原始碼Bean
- SpringBoot多環境配置Spring Boot
- MHA原始碼分析——環境部署原始碼
- Android 原始碼分析(一)專案構建過程Android原始碼
- Glide的load()過程原始碼分析IDE原始碼
- React Native Android 原始碼分析之啟動過程React NativeAndroid原始碼
- 軟體構造過程與配置管理
- JDK1.8原始碼分析03之idea搭建原始碼閱讀環境JDK原始碼Idea
- mybatis plus原始碼解析(一) ---基於springboot配置載入和SqlSessionFactory的構造MyBatis原始碼Spring BootSQLSession
- 深入Vue - 原始碼目錄及構建過程分析Vue原始碼
- Spring原始碼分析(一) -- 環境搭建Spring原始碼
- docker下springboot的多環境配置DockerSpring Boot
- 原始碼分析OKHttp的執行過程原始碼HTTP
- Zookeeper原始碼分析(一) ----- 原始碼執行環境搭建原始碼
- Springboot 載入配置檔案原始碼分析Spring Boot原始碼
- Spring原始碼解析之環境搭建Spring原始碼
- SpringBoot多環境日誌配置Spring Boot
- SpringBoot配置Profile多環境支援Spring Boot
- Spring啟動過程——原始碼分析Spring原始碼
- Netty NioEventLoop 建立過程原始碼分析NettyOOP原始碼
- 從原始碼角度解析 Springboot 2.6.2 的啟動過程原始碼Spring Boot
- [原始碼分析] 訊息佇列 Kombu 之 啟動過程原始碼佇列
- Swift-構造過程Swift
- springboot打包不同環境配置與shell指令碼部署Spring Boot指令碼
- SpringBoot自動配置原理原始碼級別分析Spring Boot原始碼
- SpringBoot原始碼分析Spring Boot原始碼
- 配置環境之vscodeVSCode
- btcpool礦池原始碼分析(1)環境搭建TCP原始碼
- Netty NioEventLoop 啟動過程原始碼分析NettyOOP原始碼
- Spring Boot原始碼分析-啟動過程Spring Boot原始碼
- Spring MVC 啟動過程原始碼分析SpringMVC原始碼
- 精盡MyBatis原始碼分析 - SQL執行過程(二)之 StatementHandlerMyBatis原始碼SQL
- 精盡MyBatis原始碼分析 - SQL執行過程(三)之 ResultSetHandlerMyBatis原始碼SQL
- 精盡MyBatis原始碼分析 - SQL執行過程(一)之 ExecutorMyBatis原始碼SQL