一起來分析一下SpringBoot配置環境的構造過程?

一起來分析一下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決定的。

所謂技多不壓身,我們所讀過的每一本書,所學過的每一門語言,在未來指不定都能給我們意想不到的回饋呢。其實做為一個開發者,有一個學習的氛圍跟一個交流圈子特別重要這里我推薦一個Java學習交流群342016322,不管你是小白還是大牛歡迎入駐,大家一起交流成長。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容