1.背景
之前对于公司统一架构的组件在安装后,有些配置项不会使用classpath下面的application.properties中的值,而是去相应组件的config/config.properties去加载,这样的好处是,本地搭建的测试环境和线程环境对于配置文件是不冲突的,极大方便的开发效率,基于这样的场景,产生了一些关于配置文件加载顺序的一系列问题,在此进行记录和学习。
2.配置文件的加载顺序
其实关于SpringBoot配置文件加载顺序的文章有很多,这里就不详细说明了,贴出两篇大佬总结的文章:
【小家Spring】一篇文章彻底搞懂Spring Boot配置文件的加载顺序(项目内部配置和外部配置)
整体的加载顺序如下图:
在项目路径下面配置文件的加载顺序
3.深入理解
在Spring项目中,每一个配置文件被加载后,都会对应一个JVM中的实例对象,这个实例对象会被Spring容器所管理,以上图举例config/application.properties、application.properties、classpath:application.properties、classpath:config/application.properties,都有对应的实例对象表示这个配置文件。
所以说这些配置文件都会被Spring容器所管理,具体哪一个生效就要看前面的加载顺序了,如果我们指定了具体哪一个配置文件生效,那么他的优先级是比较高的,其他的会排在他后面生效。
Spring会使用Environment接口的实现类管理所有的配置文件的对象,这个Environment在Spring中也是一个很重要的概念,如果你阅读Spring、SpringBoot的源码会发现,会有一步是初始化并配置Environment的代码,也就是在这里完成了对于配置文件的解析,即将配置文件对应成Java的对象,并交给Spring进行管理。
那么此时的重点,就应该去分析在从Environment中获取配置项的value时,是获取哪一个配置文件中值就可以了,跟着environment.getProperty进行查看:
1.实现EnvironmentAware接口,将setEnvironment方法将在Spring容器启动过程中,自动调用,然后查看environment.getProperty(“component.id”)的调用流程
2.查看getProperty方法,会进入PropertyResolver接口中,因为Environment继承了PropertyResolver接口
3.PropertyResolver接口中的String getProperty(String key),只有PropertySourcesPropertyResolver进行了实现,那么我们只需要查看相应的方法即可。
org.springframework.core.env.PropertySourcesPropertyResolver类的getProperty方法
@Override
@Nullable
public String getProperty(String key) {
return getProperty(key, String.class, true);
}
@Nullable
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
if (this.propertySources != null) {
//1.获取到所有的配置资源,也就是配置对象列表
for (PropertySource<?> propertySource : this.propertySources) {
//2.从配置对象中获取配置项value
Object value = propertySource.getProperty(key);
//3. 如果value为空,则进行遍历下一个配置对象
// 如果value不为空,则进行处理,然后直接返回当前配置对象的值,不再继续遍历
if (value != null) {
//3.1 解析嵌套占位符,用于解析是否用占位符,并且进行转换
if (resolveNestedPlaceholders && value instanceof String) {
value = resolveNestedPlaceholders((String) value);
}
logKeyFound(key, propertySource, value);
//3.2 如果有必要,则进行类型转换,然后进行返回
return convertValueIfNecessary(value, targetValueType);
}
}
}
return null;
}
到此步我们发现,environment.getProperty(“component.id”)就是依次遍历所有的配置对象,如果能从这个配置对象中获取到值,那么就进行一些解析和转换后,直接进行返回了。这样的话,猜测一下也知道,肯定是this.propertySources是按照一定规则排序的,才能实现SpringBoot配置文件加载的顺序性。他的顺序性一定也是按照config/application.properties、application.properties、classpath:application.properties、classpath:config/application.properties中的顺序进行的。
有上图和代码进行分析,我们知道如果在config/application.properties、application.properties、classpath:application.properties、classpath:config/application.properties中有配置项是重复的,那么就会找到配置对象的先后顺序进行生效和查找,只要其中一个配置对象不为空,则直接返回了。
如果我们指定其中一个文件生效的,那么他的顺序是怎么样的呢?假设我们指定application-dev1.properties文件生效
总结:
1.所有的配置文件都会有对应的配置对象与之关联,并且会放到一个集合中进行管理
2.默认情况下,如果有多个配置文件的配置项是重复的,那么就会按照SpringBoot的配置文件的加载顺序进行生效,不是说这些配置文件只有一个有用,其他的配置文件中的配置项是无用的,他们都是会生效的,只是在有配置项重复时,谁的优先级高用谁的。
4.自定义加载顺序
其关键就是修改this.propertySources中配置对象的顺序,这样就可以实现他的顺序了,或者说我们想要让我们自定义目录的优先级最高,此时也只需要将这个配置对象加到集合的头部就行,关于获取这个propertySources对象可以实现ConfigurableEnvironment这个接口就行,ConfigurableEnvironment是Environment的子接口,一般我们实现EnvironmentAware接口,传递过来的也是ConfigurableEnvironment的实现类,所有我们可以将Environment强转为ConfigurableEnvironment然后进行设置。
如下是实现的例子:
public class LoadConfigBeanFactoryPostProcessor implements BeanFactoryPostProcessor, EnvironmentAware, Ordered {
private static final Logger logger = LoggerFactory.getLogger(com.xxx.ocdvs.common.config.LoadConfigBeanFactoryPostProcessor.class);
private static final String CONFIG_PROPERTIES = "config.properties";
private ConfigurableEnvironment environment;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
String configPath = getConfigPropertiesPath();
logger.info("- Config path is {}.", configPath);
Properties properties = new Properties();
try {
properties.load(new FileInputStream(configPath));
} catch (IOException e) {
logger.info("- Fail to load config properties because the path is error.");
}
environment.getPropertySources().addFirst(new PropertiesPropertySource(CONFIG_PROPERTIES, properties));
}
@Override
public void setEnvironment(Environment environment) {
this.environment = (ConfigurableEnvironment) environment;
}
@Override
public int getOrder() {
return -1;
}
/**
* 拿到config.properties文件的目录
*/
public String getConfigPropertiesPath(){
return getConfPropertiesPath(CONFIG_PROPERTIES);
}
/**
* 拿到/conf下properties路径,例如拿到config.properties的路径
* @param file /conf文件夹下的.properties文件
*/
protected String getConfPropertiesPath(String file){
//这个路径只是举例,可以是你想指定的任何路径,我们统一架构是先安装组件,然后会根据动态的生成服务器、数据库、注册中心的地址
//使用一个配置文件进行保存,这个文件的地址不是在tomcat目录下面
String propertiesPath = "/config/"+file;
return propertiesPath;
}
}
关键点:
1.需要在Spring Bean生命周期实例化之前的阶段执行,也就是需要在BeanFactoryPostProcessor接口的postProcessBeanFactory方法中进行实现,关于Spring的扩展点你可能知道几个,但是对于此步骤不能在BeanPostProcessor生效之前使用。具体原因还是要自定义的加载顺序要优于Spring Bean生命周期之前,因为在Spring Bean生命周期中,会进行属性赋值,此时会从environment中获取值。
2.将Environment强转为ConfigurableEnvironment,这样才能调用ConfigurableEnvironment.getPropertySources()获取配置对象集合进行处理。
3.实现Ordered接口,其实不实现也行,但是实现了Ordered 接口,以后如果有两个自定义的路径,你先指定谁的优先级高,可以使用Ordered接口轻松实现。