在我们的开发工作总是离不了配置项相关的配置工作,SpringBoot也为我们提供了@ConfigurationProperties注解来进行配置项信息的配置工作,同时也提供了几个配置文件的默认加载位置,如:classpath:application.properties、classpath:application.yml、classpath:application.yaml、classpath:/config/application.properties、classpath:/config/application.yml、classpath:/config/application.yaml等。另外我们还可以在命令行中、系统属性中、虚拟机参数中、Servlet上下文中进行配置项的配置,既然有这么多的配置位置,程序在加载配置项的时候总得有一个先后顺序吧,要不然系统不就乱套了。在SpringBoot中大概有这样的一个先后加载顺序(优先级高的会覆盖优先级低的配置):
- 命令行参数。
- Servlet初始参数
- ServletContext初始化参数
- JVM系统属性
- 操作系统环境变量
- 随机生成的带random.*前缀的属性
- 应用程序以外的application.yml或者appliaction.properties文件
- classpath:/config/application.yml或者classpath:/config/application.properties
- 通过@PropertySource标注的属性源
- 默认属性
那么SpringBoot是怎么创建这样的一个优先顺序的呢?默认的application.properties是怎么被加载的呢?在本章中我将把其中奥秘慢慢道出:
我在之前的SpringBoot启动流程简析的文章中说过,SpringBoot在启动的过程中会从spring.factories中加载一些ApplicationListener,在这些ApplicationListener中其中就有一个我们今天要说的ConfigFileApplicationListener;我们之前也说过在启动过程中会创建ConfigurableEnvironment,也会进行命令行参数的解析工作。在org.springframework.boot.SpringApplication#prepareEnvironment这个方法中有这样的一段代码:
先创建应用可配置的环境变量,为命令行进行环境变量配置工作:
protected void configureEnvironment(ConfigurableEnvironment environment,
String[] args) {
//将命令行参数转换为org.springframework.core.env.PropertySource
configurePropertySources(environment, args);
//Profile的配置,这里先不说明
configureProfiles(environment, args);
}
protected void configurePropertySources(ConfigurableEnvironment environment,
String[] args) {
//从上面创建的ConfigurableEnvironment实例中获取MutablePropertySources实例
MutablePropertySources sources = environment.getPropertySources();
//如果有defaultProperties属性的话,则把默认属性添加为最后一个元素
if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
sources.addLast(
new MapPropertySource("defaultProperties", this.defaultProperties));
}
//这里addCommandLineProperties默认为true 如果有命令行参数的数
if (this.addCommandLineProperties && args.length > 0) {
//name为:commandLineArgs
String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
//如果之前的MutablePropertySources中有name为commandLineArgs的PropertySource的话,则把当前命令行参数转换为CompositePropertySource类型,和原来的PropertySource进行合并,替换原来的PropertySource
if (sources.contains(name)) {
PropertySource<?> source = sources.get(name);
CompositePropertySource composite = new CompositePropertySource(name);
composite.addPropertySource(new SimpleCommandLinePropertySource(
name + "-" + args.hashCode(), args));
composite.addPropertySource(source);
sources.replace(name, composite);
}
else {
//如果之前没有name为commandLineArgs的PropertySource的话,则将其添加为MutablePropertySources中的第一个元素,注意了这里讲命令行参数添加为ConfigurableEnvironment中MutablePropertySources实例的第一个元素,且永远是第一个元素
sources.addFirst(new SimpleCommandLinePropertySource(args));
}
}
}
从上面的代码中我们可以看到,SpringBoot把命令行参数转换为PropertySource,并添加为环境变量中的第一个元素!这里简单的提一下MutablePropertySources 这个类,它的UML如下所示:
从上面的UML中我们可以看到,MutablePropertySources实现了Iterable接口,是一个可迭代的类,在这个类中有这样的一个属性:
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<PropertySource<?>>();
一个类型为PropertySource的CopyOnWriteArrayList,这里用的是CopyOnWriteArrayList,而不是ArrayList、LinkedList,大家可以想一下这里为什么用了CopyOnWriteArrayList。
MutablePropertySources中的这些方法都是通过CopyOnWriteArrayList中的方法来实现的。我们在之前的文章中说明,SpringBoot创建的ConfigurableEnvironment实例是StandardServletEnvironment,其UML类图如下:
,其在实例化的过程中,会调用父类的构造函数先实例化父类,其父类StandardEnvironment为默认无参构造函数,AbstractEnvironment中的无参构造函数如下:
public AbstractEnvironment() {
//调用customizePropertySources方法进行定制PropertySource
customizePropertySources(this.propertySources);
}
在StandardServletEnvironment和StandardEnvironment分别重写了这个方法,其调用为StandardServletEnvironment中的customizePropertySources方法,其源码如下:
protected void customizePropertySources(MutablePropertySources propertySources) {
//SERVLET_CONFIG_PROPERTY_SOURCE_NAME 为 servletConfigInitParams 添加servletConfigInitParams 的PropertySource
propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
//SERVLET_CONTEXT_PROPERTY_SOURCE_NAME为servletContextInitParams 添加servletContextInitParams 的PropertySource
propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
//如果有JNDI
if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
//JNDI_PROPERTY_SOURCE_NAME 为 jndiProperties 添加jndiProperties 的PropertySource
propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
}
//调用父类的StandardEnvironment中的customizePropertySources
super.customizePropertySources(propertySources);
}
protected void customizePropertySources(MutablePropertySources propertySources) {
//SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME 为 systemProperties 添加systemProperties 的PropertySource
propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
//SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME 为 systemEnvironment 添加systemEnvironment的PropertySource
propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
从上面的分析中我们可以看到在创建StandardServletEnvironment的实例的时候,会向org.springframework.core.env.AbstractEnvironment#propertySources中按顺序添加:name分别为:servletConfigInitParams、servletContextInitParams、jndiProperties 、systemProperties、systemEnvironment 的PropertySource,再按照我们前面的分析将name为commandLineArgs的PropertySource放到第一位,则org.springframework.core.env.AbstractEnvironment#propertySources的顺序到现在为:commandLineArgs、servletConfigInitParams 、servletContextInitParams 、jndiProperties 、systemProperties 、systemEnvironment,是不是和我们前面说的对照起来了?我们一直在说PropertySource,也一直在说PropertySource中的name,对于PropertySource我们可以理解为带name的、存放 name/value 的property pairs;那么其中的name我们应该如何理解呢?在org.springframework.core.env.MutablePropertySources中有这样一个方法:addAfter,其作用是将某个PropertySource的实例添加到某个name的PropertySource的后面,其源码如下所示:
public void addAfter(String relativePropertySourceName, PropertySource<?> propertySource) {
//确定所传入的relativePropertySourceName和所传入的propertySource的name不相同
assertLegalRelativeAddition(relativePropertySourceName, propertySource);
//如果之前添加过此PropertySource 则移除
removeIfPresent(propertySource);
//获取所传入的relativePropertySourceName的位置
int index = assertPresentAndGetIndex(relativePropertySourceName);
//将传入的propertySource添加到相应的位置
addAtIndex(index + 1, propertySource);
}
在上面的代码中有assertPresentAndGetIndex这样的一段代码比较重要:
int index = assertPresentAndGetIndex(relativePropertySourceName);
private int assertPresentAndGetIndex(String name) {
//获取name为某个值的PropertySource的位置,
int index = this.propertySourceList.indexOf(PropertySource.named(name));
if (index == -1) {
throw new IllegalArgumentException("PropertySource named '" + name + "' does not exist");
}
return index;
}
在上面的代码中获取PropertySource的元素的位置的时候,是调用List中的indexOf方法来进行查找的,但是其参数为PropertySource.named(name)产生的对象,我们之前往propertySourceList中放入的明明是PropertySource类型的对象,这里在查找的时候为什么要用PropertySource.named(name)产生的对象来进行索引位置的查找呢?PropertySource.named(name)产生的对象又是什么呢?
public static PropertySource<?> named(String name) {
return new ComparisonPropertySource(name);
}
PropertySource.named(name)产生的对象是ComparisonPropertySource的实例,它也是PropertySource的一个子类,那么为什么用它也能查找到之前放入到propertySourceList中的元素的位置呢?通过翻开indexOf这个方法的源码我们知道,它是通过调用元素的equals方法来判断是否是同一个元素的,而凑巧的是在PropertySource中重写了equals这个方法:
public boolean equals(Object obj) {
return (this == obj || (obj instanceof PropertySource &&
ObjectUtils.nullSafeEquals(this.name, ((PropertySource<?>) obj).name)));
}
到这里就很明显了,PropertySource中的name属性是用来判断是否是同一个元素的,即是否是同一个PropertySource的实例!我们在创建PropertySource类型的子类的时候都会传入一个name,直接用我们创建的PropertySource来进行位置的查找不就可以了吗?为什么还要创建出来一个ComparisonPropertySource类呢?通过翻看ComparisonPropertySource这个类的源码我们可以发现,在这个类中调用getSource、containsProperty、getProperty方法都会抛出异常,并且除了这三个方法之外没有多余的方法,如果直接用我们创建的PropertySource的话,保不齐你会重写它的equals方法,是不是?用ComparisonPropertySource的话,即使你在别的PropertySource实现类重写了PropertySource方法,在查找其顺序是也要按照Spring定义的规则来,并且ComparisonPropertySource只能做干查找元素位置这一件事,其他的事它什么也干不了,这又是不是设计模式中的某一个原则的体现呢?