本文基于spring boot 2.2.0 release版本
spring boot配置文件加载是通过ConfigFileApplicationListener监听器完成的。
先来看一下该类的注释:
* {@link EnvironmentPostProcessor} that configures the context environment by loading
* properties from well known file locations. By default properties will be loaded from
* 'application.properties' and/or 'application.yml' files in the following locations:
* file:./config/
* file:./
* classpath:config/
* classpath:
* The list is ordered by precedence (properties defined in locations higher in the list
* override those defined in lower locations).
* Alternative search locations and names can be specified using
* {@link #setSearchLocations(String)} and {@link #setSearchNames(String)}.
* Additional files will also be loaded based on active profiles. For example if a 'web'
* profile is active 'application-web.properties' and 'application-web.yml' will be
* considered.
* The 'spring.config.name' property can be used to specify an alternative name to load
* and the 'spring.config.location' property can be used to specify alternative search
* locations or specific files.
上面注释的大概意思是说,该类默认加载file:./config/、file:./、classpath:config/、classpath:路径下的’application.properties’和’application.yml’文件,且这些路径是按照优先级排序的,前面路径下的文件会覆盖后面路径的。可以调用setSearchLocations方法修改上述路径位置,该类也会根据激活的profile加载对应环境的配置文件,属性spring.config.name和spring.config.location也可以用来设置加载配置文件的文件名和路径。
下面详细分析该类加载配置文件的原理。
ConfigFileApplicationListener是监听器,实现ApplicationListener接口。我们使用spring boot,需要先创建SpringApplication对象,那么先来看一下SpringApplication类的构造方法:
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
//加载ApplicationListener
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
getSpringFactoriesInstances方法用于从spring.factories文件中加载ApplicationListener实现类。ConfigFileApplicationListener就配置在spring.factories文件中。
对配置文件加载是通过事件触发的。
spring boot启动过程会发布ApplicationEnvironmentPreparedEvent事件,然后调用ConfigFileApplicationListener.onApplicationEvent方法处理该事件。
下面我们看一下onApplicationEvent方法:
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
//处理ApplicationEnvironmentPreparedEvent事件
onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
}
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
//从spring.factories文件加载EnvironmentPostProcessor对象
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
//ConfigFileApplicationListener也实现了EnvironmentPostProcessor
postProcessors.add(this);
//对EnvironmentPostProcessor实现类排序
AnnotationAwareOrderComparator.sort(postProcessors);
for (EnvironmentPostProcessor postProcessor : postProcessors) {
//调用postProcessEnvironment
postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
}
}
从onApplicationEnvironmentPreparedEvent中可以看到接下来将继续调用ConfigFileApplicationListener.postProcessEnvironment方法。
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
addPropertySources(environment, application.getResourceLoader());
}
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
//添加与随机数相关的配置源
RandomValuePropertySource.addToEnvironment(environment);
//Load类最终负责加载配置文件
new Loader(environment, resourceLoader).load();
}
postProcessEnvironment在最后调用了Load类的load方法,该方法便是完成对配置文件的加载。
Loader类是ConfigFileApplicationListener的内部私有类,只有ConfigFileApplicationListener可以创建。下面我们先来看一下该类的构造方法。
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
this.environment = environment;
this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
//从spring.factories文件中加载PropertySourceLoader
//PropertySourceLoader有两个实现类:PropertiesPropertySourceLoader和
//YamlPropertySourceLoader,分别用于加载文件名后缀为properties和yaml的文件
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader());
}
下面看一下load方法:
void load() {
FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
(defaultProperties) -> {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
//initializeProfiles从多个配置源加载设置的profile,
//配置源可以是:环境变量、启动参数"--"设置、Environment对象设置等
//可以通过属性名spring.profiles.include或者spring.profiles.active指定profile
//无论上述配置源没有设置profile,都会在profiles属性中增加null,
//这是为了保证能首先处理默认的配置文件
initializeProfiles();
//遍历profiles
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
if (isDefaultProfile(profile)) {
addProfileToEnvironment(profile.getName());
}
//读取配置文件,下面分析该方法
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
//读取application.properties配置文件
//如果application.properties中没有配置spring.profiles属性,那么下面这个方法不会加载任何内容
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
//将配置文件作为配置源添加到Environment对象中
//以后获取配置可以通过Environment获取
addLoadedPropertySources();
//将profile设置到Environment对象中
applyActiveProfiles(defaultProperties);
});
}
private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
//getSearchLocations方法获得加载配置文件的路径
//然后遍历这些路径
getSearchLocations().forEach((location) -> {
boolean isFolder = location.endsWith("/");
//查找配置文件名,可以通过spring.config.name指定文件名
//如果没有设置,使用默认名application
Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
//下面介绍load方法
names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
});
}
//获得加载配置文件的路径
//可以通过spring.config.location配置设置路径,如果没有配置,则使用默认
//默认路径由DEFAULT_SEARCH_LOCATIONS指定:
//String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/"
private Set<String> getSearchLocations() {
if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
return getSearchLocations(CONFIG_LOCATION_PROPERTY);
}
Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
locations.addAll(
asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
return locations;
}
private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
//下面的if分支默认是不走的,除非我们设置spring.config.name为空或者null
//或者是spring.config.location指定了配置文件的完整路径,也就是入参location的值
if (!StringUtils.hasText(name)) {
for (PropertySourceLoader loader : this.propertySourceLoaders) {
//检查配置文件名的后缀是否符合要求,
//文件名后缀要求是properties、xml、yml或者yaml
if (canLoadFileExtension(loader, location)) {
//加载location指定的文件,下面的load方法不做介绍,
//其原理和下面将要调用的loadForFileExtension方法类似
load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
return;
}
}
throw new IllegalStateException("File extension of config file location '" + location
+ "' is not known to any PropertySourceLoader. If the location is meant to reference "
+ "a directory, it must end in '/'");
}
Set<String> processed = new HashSet<>();
//propertySourceLoaders属性是在Load类的构造方法中设置的,可以加载文件后缀为properties、xml、yml或者yaml的文件
for (PropertySourceLoader loader : this.propertySourceLoaders) {
//fileExtension表示文件名后缀
for (String fileExtension : loader.getFileExtensions()) {
if (processed.add(fileExtension)) {
//将路径、文件名、后缀组合起来形成完成文件名
loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
consumer);
}
}
}
}
private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
if (profile != null) {
//在文件名上加上profile值,之后调用load方法加载配置文件,入参带有过滤器,可以防止重复加载
String profileSpecificFile = prefix + "-" + profile + fileExtension;
load(loader, profileSpecificFile, profile, defaultFilter, consumer);
load(loader, profileSpecificFile, profile, profileFilter, consumer);
for (Profile processedProfile : this.processedProfiles) {
if (processedProfile != null) {
String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
load(loader, previouslyLoaded, profile, profileFilter, consumer);
}
}
}
//加载不带profile的配置文件
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}
//加载配置文件
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
DocumentConsumer consumer) {
try {
//调用Resource类加载配置文件
Resource resource = this.resourceLoader.getResource(location);
if (resource == null || !resource.exists()) {
if (this.logger.isTraceEnabled()) {
StringBuilder description = getDescription("Skipped missing config ", location, resource,
profile);
this.logger.trace(description);
}
return;
}
if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
if (this.logger.isTraceEnabled()) {
StringBuilder description = getDescription("Skipped empty config extension ", location,
resource, profile);
this.logger.trace(description);
}
return;
}
String name = "applicationConfig: [" + location + "]";
//读取配置文件内容,将其封装到Document类中,解析文件内容主要是找到
//配置spring.profiles.active和spring.profiles.include的值
List<Document> documents = loadDocuments(loader, name, resource);
//如果文件没有配置数据,则跳过
if (CollectionUtils.isEmpty(documents)) {
if (this.logger.isTraceEnabled()) {
StringBuilder description = getDescription("Skipped unloaded config ", location, resource,
profile);
this.logger.trace(description);
}
return;
}
List<Document> loaded = new ArrayList<>();
//遍历配置文件,处理里面配置的profile
for (Document document : documents) {
if (filter.match(document)) {
//将配置文件中配置的spring.profiles.active和
//spring.profiles.include的值写入集合profiles中,
//上层调用方法会读取profiles集合中的值,并读取对应的配置文件
//addActiveProfiles方法只在第一次调用时会起作用,里面有判断
addActiveProfiles(document.getActiveProfiles());
addIncludedProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
Collections.reverse(loaded);
if (!loaded.isEmpty()) {
loaded.forEach((document) -> consumer.accept(profile, document));
if (this.logger.isDebugEnabled()) {
StringBuilder description = getDescription("Loaded config file ", location, resource, profile);
this.logger.debug(description);
}
}
}
catch (Exception ex) {
throw new IllegalStateException("Failed to load property source from location '" + location + "'", ex);
}
}
在最后一个load方法中可以看到spring boot通过Resource类加载了配置文件。
用下图梳理一下整个加载流程:
还有一点要注意,如果定义了多个环境文件,同时也通过spring.profiles.active激活了多个环境,那么spring将加载所有激活环境的配置文件,最后加载配置文件的配置会覆盖前面加载的配置。