您的位置:首页 > 编程语言 > Java开发

Spring Boot 源码分析(5) -- applicationContext.properties资源文件分析

2020-01-12 10:13 543 查看

Spring Boot 源码分析(5) -- applicationContext.properties资源文件分析

在前面分析监听器时,我们提到过ConfigFileApplicationListener,这个监听器在Spring Boot中负责处理applicationContext.properties属性文件的读取。

在Spring Boot 源码分析(2)中我们分析过监听器和监听事件。在环境准备完毕时会触发ApplicationEnvironmentPreparedEvent事件,在上下文加载完成时会触发ApplicationPreparedEvent事件。

在ConfigFileApplicationListener类中,监听的入口在onApplicationEvent(ApplicationEvent event)方法。代码如下:

public void onApplicationEvent(ApplicationEvent event) {
// 监听环境准备完毕事件
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent(
(ApplicationEnvironmentPreparedEvent) event);
}
// 监听上下文准备完毕事件
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
}

监听环境准备完毕

从/META-INF/spirng.fatories中获取EnvironmentPostProcessor实例,当然ConfigFileApplicationListener本身也实现了这个接口。然后处理EnvironmentPostProcessor的postProcessEnvironment方法。很显然这个接口是对环境做后置处理。

private void onApplicationEnvironmentPreparedEvent(
ApplicationEnvironmentPreparedEvent event) {
// 从/META-INF/spirng.fatories中获取EnvironmentPostProcessor实例
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
// ConfigFileApplicationListener本身也实现了这个接口
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
// 环境后置处理
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(),
event.getSpringApplication());
}
}

我们看在ConfigFileApplicationListener中对环境的后置处理:

public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
// 添加属性源
addPropertySources(environment, application.getResourceLoader());
// 配置忽略的Bean信息
configureIgnoreBeanInfo(environment);
// 绑定到SpringApplication
bindToSpringApplication(environment, application);
}

添加属性源

首先将随机值属性源添加到环境中来,随机值属性源对象为RandomValuePropertySource。然后使用内部类Loader对环境进行加载。由于Loader类分析比较复杂,我们放到后面分析。

protected void addPropertySources(ConfigurableEnvironment environment,
ResourceLoader resourceLoader) {
// 将随机值属性添加到环境中
RandomValuePropertySource.addToEnvironment(environment);
// 加载器加载属性源
new Loader(environment, resourceLoader).load();
}
配置忽略Bean信息

可以从系统环境中设置spring.beaninfo.ignore属性来表示是否忽略Bean信息,这个属性是Boolean类型的值。

private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {
// 判断系统属性中是否存在spring.beaninfo.ignore
if (System.getProperty(
CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) {
// 如果不存在,放宽限制获取前缀为spring.beaninfo.的属性
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment,
"spring.beaninfo.");
// 判断是否存在ignore属性,提供默认值true
Boolean ignore = resolver.getProperty("ignore", Boolean.class, Boolean.TRUE);
// 设置到系统属性中去
System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME,
ignore.toString());
}
}

绑定环境到SpringApplication

protected void bindToSpringApplication(ConfigurableEnvironment environment,
SpringApplication application) {
PropertiesConfigurationFactory<SpringApplication> binder = new PropertiesConfigurationFactory<SpringApplication>(
application);
binder.setTargetName("spring.main");
binder.setConversionService(this.conversionService);
binder.setPropertySources(environment.getPropertySources());
try {
binder.bindPropertiesToTarget();
}
catch (BindException ex) {
throw new IllegalStateException("Cannot bind to SpringApplication", ex);
}
}

内部类Loader分析

在分析Loader之前,我们先对ConfigFileApplicationListener属性域做一些介绍,便于后面理解。

  1. 常量定义
// 定义默认属性值
private static final String DEFAULT_PROPERTIES = "defaultProperties";

// 定义默认的文件搜索路径,优先级从低到高
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";

// 默认配置文件名称
private static final String DEFAULT_NAMES = "application";

/**
* 定义profile行为的属性
*/
public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";

/**
* 定义profile包含的属性
*/
public static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";

/**
* 定义配置文件名称的属性
*/
public static final String CONFIG_NAME_PROPERTY = "spring.config.name";

/**
* 定义配置路径的属性
*/
public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";

/**
* 定义Order排序值
*/
public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;

/**
* 定义应用配置的属性源名称
*/
public static final String APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME = "applicationConfigurationProperties";
  1. 属性域
// 文件搜索路径
private String searchLocations;
// 配置文件名称
private String names;
// Order排序值
private int order = DEFAULT_ORDER;

Loader属性和构造方法

在Loader中定义了profile信息,以及资源加载器。

  1. 属性
// 环境
private final ConfigurableEnvironment environment;
// 资源加载器
private final ResourceLoader resourceLoader;
// 属性加载器
private PropertySourcesLoader propertiesLoader;
// profile队列
private Queue<Profile> profiles;
// 被处理的profiles
private List<Profile> processedProfiles;
// 处理活动状态的profiles
private boolean activatedProfiles;
  1. 构造方法

构造方法从SpringAppliaction中获取了环境和资源加载器,如果资源加载器为空,则使用默认的加载器DefaultResourceLoader。

Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
this.environment = environment;
this.resourceLoader = resourceLoader == null ? new DefaultResourceLoader()
: resourceLoader;
}

Loader#load()方法分析

首先从环境中获取spring.profiles.active和spring.profiles.include这两个配置,存放到initialActiveProfiles集合中去。注意不是从配置文件中,因为在这时还没有加载配置文件。然后通过environment.getActiveProfiles()方法获取已经存在的集合,将此集合和initialActiveProfiles集合做差集,获得未在environment中的profiles添加到profiles队列中去,如果没有则添加默认的profiles。最后添加一个null到队列中去。

public void load() {
this.propertiesLoader = new PropertySourcesLoader();
this.activatedProfiles = false;
this.profiles = Collections.asLifoQueue(new LinkedList<Profile>());
this.processedProfiles = new LinkedList<Profile>();

// 从环境中获取spring.profiles.active和spring.profiles.include配置的profiles
Set<Profile> initialActiveProfiles = initializeActiveProfiles();
// 获取环境中还未处理的profiles
this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
if (this.profiles.isEmpty()) {
// 获取默认profiles
for (String defaultProfileName : this.environment.getDefaultProfiles()) {
Profile defaultProfile = new Profile(defaultProfileName, true);
if (!this.profiles.contains(defaultProfile)) {
this.profiles.add(defaultProfile);
}
}
}

// 添加null,null会第一个出队列
this.profiles.add(null);
// 循环处理,队列中存在[null, default]
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
// 搜索文件路径,以/结尾表示文件夹搜索,否则为文件。
for (String location : getSearchLocations()) {
if (!location.endsWith("/")) {
load(location, null, profile);
}
else {
// 获取需要搜索的文件名称
for (String name : getSearchNames()) {
load(location, name, profile);
}
}
}
this.processedProfiles.add(profile);
}
// 添加配置属性到环境中去
addConfigurationProperties(this.propertiesLoader.getPropertySources());
}

从profiles队列中逐一获取,然后进行文件路径搜索,/结尾的表示文件夹,否则为文件。使用load(location, name, profile)进行文件的加载。加载的属性源文件存放在PropertySourcesLoader中,然后添加到配置属性中去。

getSearchLocations()方法获取需要搜索的文件路径,默认使用的是classpath:/,classpath:/config/,file:./,file:./config/。首先判断环境中是否存在spring.config.location属性配置了指定的路径,如果存在则添加路径到locations中。然后将从searchLocations变量中获取路径,如果没有则使用默认的classpath:/,classpath:/config/,file:./,file:./config/。注意searchLocations可以使用逗号分隔多个路径地址,最后的优先级最高。

private Set<String> getSearchLocations() {
Set<String> locations = new LinkedHashSet<String>();
// 判断环境中是否存在spring.config.location属性
if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
// 遍历获取配置路径
// 如果是文件路径,需要加file:前缀
for (String path : asResolvedSet(
this.environment.getProperty(CONFIG_LOCATION_PROPERTY), null)) {
if (!path.contains("$")) {
path = StringUtils.cleanPath(path);
if (!ResourceUtils.isUrl(path)) {
path = ResourceUtils.FILE_URL_PREFIX + path;
}
}
locations.add(path);
}
}
// asResolvedSet方法会对List数据进行顺序反转
locations.addAll(
asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
DEFAULT_SEARCH_LOCATIONS));
return locations;
}

​getSearchNames()方法获取需要搜索的文件名称,默认名称是application。首先从环境中获取spring.config.name属性,如果存在则使用环境中配置的文件名称,可以配置多个,用逗号分隔。如果没有则使用默认的文件名称application

private Set<String> getSearchNames() {
if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
return asResolvedSet(this.environment.getProperty(CONFIG_NAME_PROPERTY),
null);
}
return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
}

在获取到配置的locations之后,对路径中搜索的文件进行加载,使用load(String location, String name, Profile profile)方法。

private void load(String location, String name, Profile profile) {
// 分组,第一个prifile为空,当location为文件时name为空
String group = "profile=" + (profile == null ? "" : profile);
if (!StringUtils.hasText(name)) {
// 直接加载路径(文件)
loadIntoGroup(group, location, profile);
}
else {
// 获取支持加载的文件后缀 .properties、.yaml、.yam
for (String ext : this.propertiesLoader.getAllFileExtensions()) {
if (profile != null) {
// 加载profile文件,例如:classpath:/application-dev.properties
loadIntoGroup(group, location + name + "-" + profile + "." + ext,
null);
// 加载被处理过的profile,为什么?
for (Profile processedProfile : this.processedProfiles) {
if (processedProfile != null) {
loadIntoGroup(group, location + name + "-"
+ processedProfile + "." + ext, profile);
}
}
// Sometimes people put "spring.profiles: dev" in
// application-dev.yml (gh-340). Arguably we should try and error
// out on that, but we can be kind and load it anyway.
//
loadIntoGroup(group, location + name + "-" + profile + "." + ext,
profile);
}
// 首先会加载最通用的文件,例如: application.proerties
loadIntoGroup(group, location + name + "." + ext, profile);
}
}
}

从上面分析看,在加载的过程中会对搜索文件路径和支持的后缀文件进行遍历处理,都调用loadIntoGroup方法,添加到分组中去。loadIntoGroup方法只调用了doLoadIntoGroup方法。下面继续分析doLoadIntoGroup方法,看文件加载的过程。

private PropertySource<?> doLoadIntoGroup(String identifier, String location,
Profile profile) throws IOException {
// 使用资源加载器获取文件资源
Resource resource = this.resourceLoader.getResource(location);
PropertySource<?> propertySource = null;
StringBuilder msg = new StringBuilder();
// 如果加载到文件就处理,否则跳过处理
if (resource != null && resource.exists()) {
String name = "applicationConfig: [" + location + "]";
String group = "applicationConfig: [" + identifier + "]";
// 使用属性源加载器处理资源文件
propertySource = this.propertiesLoader.load(resource, group, name,
(profile == null ? null : profile.getName()));
if (propertySource != null) {
msg.append("Loaded ");
// 将获取的属性源中的profiles存放到profiles属性域中。
handleProfileProperties(propertySource);
}
else {
msg.append("Skipped (empty) ");
}
}
else {
msg.append("Skipped ");
}
msg.append("config file ");
msg.append(getResourceDescription(location, resource));
if (profile != null) {
msg.append(" for profile ").append(profile);
}
if (resource == null || !resource.exists()) {
msg.append(" resource not found");
this.logger.trace(msg);
}
else {
this.logger.debug(msg);
}
return propertySource;
}

PropertySourcesLoader 属性源加载器

​在上面的doLoadIntoGroup(String group, String location,Profile profile)方法中,将搜索到的文件进行分组后使用PropertySourcesLoader的load方法进行处理资源文件。下面我们具体分析下这个类。

先从构造方法分析:

public PropertySourcesLoader() {
this(new MutablePropertySources());
}

public PropertySourcesLoader(MutablePropertySources propertySources) {
Assert.notNull(propertySources, "PropertySources must not be null");
this.propertySources = propertySources;
this.loaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader());
}

在Loader#load()方法调用时首先就通过this.propertiesLoader = new PropertySourcesLoader()创建了实例对象。通过上面的构造方法得知this.loaders从/META-INF/spring.fatories中获取PropertySourceLoader实例,这两个实例对象是PropertiesPropertySourceLoader和YamlPropertySourceLoader,分别处理properties文件和yaml文件。

在spring-boot-1.5.8.RELEASE.jar包中的/META-INF/spring.fatories文件中配置如下:

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

PropertySourcesLoader类是属性资源加载的工具类,相当于PropertySourceLoader接口的代理层。下面看看load方法。

public PropertySource<?> load(Resource resource, String group, String name,
String profile) throws IOException {
// 判断资源是否是文件
if (isFile(resource)) {
// 组合成文件名称
String sourceName = generatePropertySourceName(name, profile);
// 遍历资源加载器
for (PropertySourceLoader loader : this.loaders) {
// 根据后缀判断文件是否可以被加载
if (canLoadFileExtension(loader, resource)) {
// 开始加载文件
PropertySource<?> specific = loader.load(sourceName, resource,
profile);
// 将获得的属性源添加到this.propertySources中去
addPropertySource(group, specific, profile);
return specific;
}
}
}
return null;
}

PropertiesPropertySourceLoader加载器负责加载properties、xml文件,YamlPropertySourceLoader加载器负责加载yaml、yam文件。下面是这两个类的getFileExtensions()方法。

public class PropertiesPropertySourceLoader implements PropertySourceLoader {
@Override
public String[] getFileExtensions() {
return new String[] { "properties", "xml" };
}
}

public class YamlPropertySourceLoader implements PropertySourceLoader {

@Override
public String[] getFileExtensions() {
return new String[] { "yml", "yaml" };
}
}

分析完了属性资源文件的加载,最后将加载完成的属性源对象保存在PropertySourcesLoader的集合中。在加载完成后,调用addConfigurationProperties方法,将属性源设置到环境中去。

private void addConfigurationProperties(MutablePropertySources sources) {
List<PropertySource<?>> reorderedSources = new ArrayList<PropertySource<?>>();
for (PropertySource<?> item : sources) {
reorderedSources.add(item);
}
// 使用ConfigurationPropertySources将所有的属性源进行包装
addConfigurationProperties(
new ConfigurationPropertySources(reorderedSources));
}

private void addConfigurationProperties(
ConfigurationPropertySources configurationSources) {
MutablePropertySources existingSources = this.environment
.getPropertySources();
// 判断环境中是否存在DEFAULT_PROPERTIES这个属性源,如果存在添加到之前
if (existingSources.contains(DEFAULT_PROPERTIES)) {
existingSources.addBefore(DEFAULT_PROPERTIES, configurationSources);
}
else {
// 如果不存在添加到最后
existingSources.addLast(configurationSources);
}
}

但是属性源的配置还并没有结束,ConfigurationPropertySources统一包装多个属性源只是一个中间过程。后面的处理还需要分析监听上下文准备完毕事件。在上面的处理中只是将ConfigurationPropertySources和DEFAULT_PROPERTIES属性源的位置关系进行处理。

监听上下文准备完毕事件

在ConfigFileApplicationListener中会通过ApplicationPreparedEvent事件来触发onApplicationPreparedEvent(event)方法。方法内容如下:

private void onApplicationPreparedEvent(ApplicationEvent event) {
this.logger.replayTo(ConfigFileApplicationListener.class);
// 添加BeanFactoryPostProcessor
addPostProcessors(((ApplicationPreparedEvent) event).getApplicationContext());
}
protected void addPostProcessors(ConfigurableApplicationContext context) {
// 将后置处理器添加到上下文中去
context.addBeanFactoryPostProcessor(
new PropertySourceOrderingPostProcessor(context));
}

我们在分析Spring Boot BeanFactoryPostProcessor时提到过ConfigFileApplicationListener$PropertySourceOrderingPostProcessor这个类,就是在监听上下文准备完毕后添加进去的。在Spring启动注册BeanFacatoryPostProcessor时会调用postProcessBeanFactory方法。

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
reorderSources(this.context.getEnvironment());
}

private void reorderSources(ConfigurableEnvironment environment) {
// 完成和重新分配属性配置
ConfigurationPropertySources
.finishAndRelocate(environment.getPropertySources());
// 从环境中删除DEFAULT_PROPERTIES
// 如果存在,则放到最后,也就是说默认的属性优先级最低
PropertySource<?> defaultProperties = environment.getPropertySources()
.remove(DEFAULT_PROPERTIES);
if (defaultProperties != null) {
environment.getPropertySources().addLast(defaultProperties);
}
}

关于重新分配属性我们需要细看一下,看配置资源是如何重新分配到环境中去的。下面是ConfigurationPropertySources类中的finishAndRelocate方法。

public static void finishAndRelocate(MutablePropertySources propertySources) {
// 很显然这里获取的ConfigurationPropertySources就是上面添加到环境中去的
String name = APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME;
ConfigurationPropertySources removed = (ConfigurationPropertySources) propertySources
.get(name);
// 如果存在,需要做如下处理
if (removed != null) {
// 将ConfigurationPropertySources中的属性源集合逐一存放到环境中来
for (PropertySource<?> propertySource : removed.sources) {
// 这里处理嵌入的情况,主要是设置name值
if (propertySource instanceof EnumerableCompositePropertySource) {
EnumerableCompositePropertySource composite = (EnumerableCompositePropertySource) propertySource;
for (PropertySource<?> nested : composite.getSource()) {
propertySources.addAfter(name, nested);
name = nested.getName();
}
}
else {
propertySources.addAfter(name, propertySource);
}
}
// 最后从环境中删除
propertySources.remove(APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME);
}
}

至此,所有的属性配置资源已经加载并存放到环境中来了。

转载于:https://my.oschina.net/xiaoqiyiye/blog/1624285

  • 点赞
  • 收藏
  • 分享
  • 文章举报
chenwei7858 发布了0 篇原创文章 · 获赞 0 · 访问量 169 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: