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對當前程序運行期間的環(huán)境的封裝束铭。主要提供了兩大功能:profile和property(父接口PropertyResolver提供)廓块。目前主要有StandardEnvironment、StandardServletEnvironment和MockEnvironment3種實現契沫,分別代表普通程序带猴、Web程序以及測試程序的環(huán)境。
下面這段代碼就是SpringBoot的run方法內調用的懈万,它會在Spring容器構造之前調用拴清,創(chuàng)建環(huán)境信息:
// SpringApplication.class
private ConfigurableApplicationContext createAndRefreshContext(
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
ConfigurableApplicationContext context;
// 如果是web環(huán)境靶病,創(chuàng)建StandardServletEnvironment
// 否則,創(chuàng)建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啟動參數,設置到環(huán)境信息中
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 觸發(fā)ApplicationEnvironmentPreparedEvent事件
listeners.environmentPrepared(environment);
...
}
在SpringBoot源碼分析之SpringBoot的啟動過程這篇文章中木张,我們分析過SpringApplication啟動的時候會使用工廠加載機制初始化一些初始化器和監(jiān)聽器众辨。其中org.springframework.boot.context.config.ConfigFileApplicationListener這個監(jiān)聽器會被加載:
// spring-boot-version.release/META-INF/spring.factories
org.springframework.context.ApplicationListener=\
...
org.springframework.boot.context.config.ConfigFileApplicationListener,\
...
ConfigFileApplicationListener會監(jiān)聽SpringApplication啟動的時候發(fā)生的事件,它的監(jiān)聽代碼:
@Override
public void onApplicationEvent(ApplicationEvent event) {
// 應用環(huán)境信息準備好的時候對應的事件舷礼。此時Spring容器尚未創(chuàng)建鹃彻,但是環(huán)境已經創(chuàng)建
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent(
(ApplicationEnvironmentPreparedEvent) event);
}
// Spring容器創(chuàng)建完成并在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) {
// 添加屬性源到環(huán)境中
addPropertySources(environment, application.getResourceLoader());
// 配置需要ignore的beaninfo
configureIgnoreBeanInfo(environment);
// 從環(huán)境中綁定一些參數到SpringApplication中
bindToSpringApplication(environment, application);
}
protected void addPropertySources(ConfigurableEnvironment environment,
ResourceLoader resourceLoader) {
// 添加一個RandomValuePropertySource到環(huán)境中
// 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的處理過程整理如下:
- 創(chuàng)建PropertySourcesLoader泳挥。PropertySourcesLoader內部有2個屬性,分別是PropertySourceLoader集合和MutablePropertySources(內部有PropertySource的集合)至朗。最終加載完畢之后MutablePropertySources屬性中的PropertySource會被添加到環(huán)境Environment中的屬性源列表中屉符。PropertySourcesLoader被構造的時候會使用工廠加載機制獲得PropertySourceLoader集合(默認就2個:PropertiesPropertySourceLoader和YamlPropertySourceLoader;可以自己擴展)锹引,然后設置到屬性中
- 獲取環(huán)境信息中激活的profile(啟動項目時設置的spring.profiles.active參數)矗钟。如果沒設置profile,默認使用default這個profile嫌变,并添加到profiles隊列中吨艇。最后會添加一個null到profiles隊列中(為了獲取沒有指定profile的配置文件。比如環(huán)境中有application.yml和appliation-dev.yml腾啥,這個null就保證優(yōu)先加載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文件,如果找到啊奄,執(zhí)行第4步
- 找出的屬性源文件被加載渐苏,然后添加到PropertySourcesLoader內部的PropertySourceLoader集合中掀潮。如果該屬性源文件中存在spring.profiles.active配置,識別出來并加入第2步中的profiles隊列琼富,然后重復第3步
- 第4步找到的屬性源從PropertySourcesLoader中全部添加到環(huán)境信息Environment中仪吧。如果這些屬性源存在defaultProperties配置,那么會添加到Environment中的屬性源集合頭部鞠眉,否則添加到尾部
比如項目中classpath下存在application.yml文件和application-dev.yml邑商,application.yml文件的內容如下:
spring.profiles.active: dev
直接啟動項目,開始解析凡蚜,過程如下:
- 從環(huán)境信息中找出是否設置profile人断,發(fā)現沒有設置。 添加默認的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。尋找文件發(fā)現沒有找到宰闰。結束
這里需要注意一下一些常用的額外參數的問題茬贵,整理如下:
- 如果啟動程序的時候設置了系統(tǒng)參數spring.profiles.active,那么這個參數會被設置到環(huán)境信息中(由于設置了系統(tǒng)參數移袍,在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砂轻。注意這是程序參數奔誓,不是系統(tǒng)參數。比如java -jar yourjar.jar --spring.profiles.active=prod搔涝。會去解析prod這個profile(不論是系統(tǒng)參數還是程序參數厨喂,都會被封裝成多個PropertySource存在于環(huán)境信息中。最終獲取profile的時候會去環(huán)境信息中拿庄呈,且都可以拿到)
- 上面說的每個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的數據彩届。
優(yōu)先級的問題:由于環(huán)境信息Environment中保存的PropertySource是MutablePropertySources伪冰,那么會去配置值的時候就存在優(yōu)先級的問題。比如PropertySource1和PropertySource2都存在custom.name配置樟蠕,那么會從哪個PropertySource中獲取這個custom.name配置呢贮聂?它會遍歷內部的PropertySource列表,越在前面的PropertySource寨辩,越先獲认判浮;比如PropertySource1在PropertySource2前面靡狞,那么會先獲取PropertySource1的配置耻警。MutablePropertySources內部添加PropertySource的時候可以選擇元素的位置,可以addFirst,也可以addLast甘穿,也可以自定義位置腮恩。
總結:SpringApplication啟動的時候會構造環(huán)境信息Environment,如果是web環(huán)境温兼,創(chuàng)建StandardServletEnvironment秸滴,否則,創(chuàng)建StandardEnvironment募判。這兩種環(huán)境創(chuàng)建的時候都會在內部的propertySources屬性中加入一些PropertySource荡含。比如屬性屬性的配置信息封裝成MapPropertySource,系統(tǒng)環(huán)境配置信息封裝成SystemEnvironmentPropertySource等届垫。這些PropertySource集合存在在環(huán)境信息中释液,從環(huán)境信息中讀取配置的話會遍歷這些PropertySource并找到相對應的配置和值。Environment構造完成之后會讀取springboot相應的配置文件装处,從3個角度去查找:目錄均澳、文件名和profile。這3個角度有默認值符衔,可以進行覆蓋找前。springboot相關的配置文件讀取完成之后會被封裝成PropertySource并添加到環(huán)境信息中。
@ConfigurationProperties和@EnableConfigurationProperties注解的原理
SpringBoot內部規(guī)定了一套配置和配置屬性類映射規(guī)則判族,可以使用@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
// 所以實際上條件里的代碼只會執(zhí)行一次
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);
// 設置屬性源,這里的屬性源從環(huán)境信息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
// 會使用環(huán)境信息中的屬性源進行綁定
// 這樣配置類就讀取到了配置文件中的配置
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內部規(guī)定了一套配置和配置屬性類映射規(guī)則横漏,可以使用@ConfigurationProperties注解配合前綴屬性完成屬性類的讀冉魃琛;再通過@EnableConfigurationProperties注解設置配置類就可以把這個配置類注入進來绊茧。由于這個配置類是被注入進來的铝宵,所以它肯定在Spring容器中存在打掘;這是因為在ConfigurationPropertiesBeanRegistrar內部會注冊配置類到Spring容器中华畏,這個配置類的實例化過程在ConfigurationPropertiesBindingPostProcessor這個BeanPostProcessor完成,它會在實例化bean之前會判斷bean是否被@ConfigurationProperties注解修飾尊蚁,如果有亡笑,使用PropertiesConfigurationFactory從環(huán)境信息Environment中進行值的綁定。這個ConfigurationPropertiesBeanRegistrar是在使用@EnableConfigurationProperties注解的時候被創(chuàng)建的(通過EnableConfigurationPropertiesImportSelector)横朋。配置類內部屬性的綁定成功與否是通過環(huán)境信息Environment中的屬性源PropertySource決定的仑乌。