文章目录
- Springboot的外部化配置
- 外部化配置概念理解
- 外部化配置实际应用
- 外部化配置扩展
- 定义外部化属性源
- PropertySources属性源使用时机
- 扩展外部化配置属性源
- 基于 SpringApplicationRunListener#environmentPrepared 扩展外部化配置属性源
- 基于 SpringApplicationRunListener#contextPrepared 扩展外部化配置属性源
- 基于 SpringApplicationRunListener#contextLoaded 扩展外部化配置属性源
- 基于 ApplicationEnvironmentPreparedEvent 扩展外部化配置属性源
- 基于 EnvironmentPostProcessor 扩展外部化配置属性源
- 基于 ApplicationContextInitializer 扩展外部化配置属性源
- 各自定义PropertySource优先级(从高到低)
Springboot的外部化配置
外部化配置概念理解
什么是外部化配置
这个名词来源于Springboot官方文档的某一个章节名称,官方并没有对其下过准确的定义。一般研发人员,运维人员之间沟通时,经常会提及它。
有外部化配置也就有内部化配置,一般我们把在代码中枚举类,或硬性编码的部分称之为内部化配置。内部化配置缺少灵活性。
一个很熟悉的场景:一般公司的系统都会划分为开发(dev),测试(test),生产(prod)三个环境,每个环境的数据库、参数配置肯定是不一样的。一般公司都会借助spring的profile结合maven或gradle构建软件实现灵活的构建,部署好的软件系统自动对应到相应的环境,不用进行源码的修改。
还有springcloud的微服务实践中,通常也会引入总线配置方式,实现在系统不重启的情况下,实现修改某些参数或配置的目的。这些业务场景下配置其实就是外部化配置思想。抽象下概念外部化配置可以理解为:对于可扩展性应用系统,其内部组件是可配置化的,比如:认证信息、端口范围、线程池属性等。
- 官方链接:https://docs.spring.io/spring-boot/docs/2.0.2.RELEASE/reference/htmlsingle/#boot-features-external-config
springboot中外部化配置
springboot官方提供了三种外部化配置应用方式:
- Bean的@Value注入
- Spring Eviroment读取
- @ConfigurationProperties綁定到结构化对象
外部化配置实际应用
XML Bean的属性占位符
- 比如在spring的xml配置文件中添加如下配置:
<bean id="user" class="com.hzqiuxm.configuration.domain.User">
<property name="id" value="${user.id}"/>
<property name="name" value="${user.name}"/>
</bean>
- 另外一个xml配置文件内容:
<!-- 属性占位符配置-->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<!-- Properties 文件 classpath 路径 -->
<property name="location" value="classpath:/config/application.properties"/>
<!-- 文件字符编码 -->
<property name="fileEncoding" value="UTF-8"/>
</bean>
- 在属性配置文件(application.properties)中添加
# 用户配置属性
user.id = 10
user.name =临江仙2018
创建对应的实体类user,添加对应字段的get/set方法后,启动Spirng启动到类获取到实体类User的Bean,可以看到其id和name字段的值为配置文件中配置的值
public class SpringXmlConfigPlaceholderBootstrap {
public static void main(String[] args) {
String[] locations = {"META-INF/spring/spring-context.xml", "META-INF/spring/user-context.xml"};
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext(locations);
User user = applicationContext.getBean("user", User.class);
System.err.println("用户对象 : " + user);
// 关闭上下文
applicationContext.close();
}
}
输出结果:id=10, name='临江仙 2018',符合预期和我们配置文件中的值一致。这种方式是SpringFrame中普遍使用的方式,springboot出现后使用频率已经越来越少了。
接下来我们来搞点事情,换成springboot的方式启动
@ImportResource("META-INF/spring/user-context.xml") // 加载 Spring 上下文 XML 文件
@EnableAutoConfiguration
public class XmlPlaceholderExternalizedConfigurationBootstrap {
public static void main(String[] args) {
ConfigurableApplicationContext context =
new SpringApplicationBuilder(XmlPlaceholderExternalizedConfigurationBootstrap.class)
.web(WebApplicationType.NONE) // 非 Web 应用
.run(args);
User user = context.getBean("user", User.class);
System.err.println("用户对象 : " + user);
// 关闭上下文
context.close();
}
}
输出结果:id=10, name='hzqiuxm',为什么不符合预期?id是能对应上的,name的值确不对了?其实是 PropertySources顺序问题捣的鬼,这也是本文需要介绍的内容之一,相信看完文章后你就恍然大悟了。
@Value的注解方式
这种应用在平时开发中非常常见,很多开发人员都采用这种方式注入自定义的一些配置属性值。主要有三种注入方式:
- 字段注入
- 构造器注入
- 方法注入
主要的用法举例:
@Value("${user.id}") //普通属性注入
private Long userId;
@Value("${user.age:${my.user.age:32}}") //嵌套属性注入,非常适合新老API兼容的设计,user.age代表老的,my.user.age代表新的,32代表默认的
private int age;
@Value("#{'${list}'.split(',')}") //list注入
private List<String> list;
@Value("#{${maps}}") //map注入
private Map<String,String> maps;
--------------------------------------
对应配置文件:
user.id:1
my.user.age:32
list: topic1,topic2,topic3
maps: "{key1: 'value1', key2: 'value2'}"
Eviroment方式读取
- 方法/构造器依赖注入
@Override
public void setEnvironment(Environment environment) {
if (this.environment != environment) {
throw new IllegalStateException();
}
}
- @Autowired依赖注入
@Autowired
@Qualifier(ENVIRONMENT_BEAN_NAME)
private Environment environment;
- EviromentAware 接口回调
实现 EnvironmentAwarek接口 -
BeanFactory 依赖查找Environment
实现 BeanFactoryAware接口
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (this.environment != beanFactory.getBean(ENVIRONMENT_BEAN_NAME, Environment.class)) {
throw new IllegalStateException();
}
}
三者的执行顺序:1 @Autowired; 2 BeanFactoryAware ;3 EviromentAware
@ConfigurationProperties Bean绑定
- 类级别注入
-
@Bean方法声明
-
嵌套类型绑定
外部化配置扩展
定义外部化属性源
- PropertySources的顺序问题:官方提供的参考如下(Springboot版本需要1.5以上,低版本会缺少部分)
- 什么是PropertySource
带有名称的属性源,Properties文件、Map、YAML 文件等都可以称之为PropertySource。
- 什么是Eviroment抽象
Environment与PropertySources可以看成是一一对应的关系;PropertySource与PropertySources从单词的单数和复数关系也可以看的出是 1 对 多的关系;ConfigurableEnvironment与MutablePropertySources相对应。
PropertySources属性源使用时机
- Spring Framework 中,尽量在org.springframework.context.support.AbstractApplicationContext#prepareBeanFactory方法前初始化。
- Spring Boot 中,尽量在org.springframework.boot.SpringApplication#refreshContext(context)方法前初始化。
扩展外部化配置属性源
基于 SpringApplicationRunListener#environmentPrepared 扩展外部化配置属性源
- 实现两个接口:SpringApplicationRunListener, Ordered
- META-INF下新建spring.factories文件,添加如下配置
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
com.imooc.diveinspringboot.externalized.configuration.configuration.ExtendPropertySourcesRunListener
- 重写SpringApplicationRunListener#environmentPrepared 和 getOrder
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
MutablePropertySources propertySources = environment.getPropertySources();
Map<String, Object> source = new HashMap<>();
source.put("user.id", "0"); //设置编号为0
MapPropertySource propertySource = new MapPropertySource("from-environmentPrepared", source);
propertySources.addFirst(propertySource);
}
@Override
public int getOrder() {
return new EventPublishingRunListener(application,args).getOrder() + 1;//返回排在默认的后面
}
各个文件中的配置如下:
自定义environmentPrepared中: 0
application.properties : 10
META-INF/default.properties : 11
- 定义引导类ExtendPropertySourcesBootstrap,并模拟一个Command line arguments(88) 和Default properties(99) 配置方式
@EnableAutoConfiguration
@Configuration
@PropertySource(name = "from default.properties", value = "classpath:META-INF/spring/default.properties")
public class ExtendPropertySourcesBootstrap {
public static void main(String[] args) {
ConfigurableApplicationContext context =
new SpringApplicationBuilder(ExtendPropertySourcesBootstrap.class)
.web(WebApplicationType.NONE) // 非 Web 应用
.properties("user.id=99") // Default properties
.run(of("--user.id=88")); // Command line arguments.
// 获取 Environment 对象
ConfigurableEnvironment environment = context.getEnvironment();
System.err.printf("用户id : %d\n", environment.getProperty("user.id", Long.class));
environment.getPropertySources().forEach(propertySource -> {
System.err.printf("PropertySource[名称:%s] : %s\n", propertySource.getName(), propertySource);
});
// 关闭上下文
context.close();
}
private static <T> T[] of(T... args) {
return args;
}
}
根据上一节提到的PropertySources的顺序问题,我们可以猜测 我们自定义的优先级应该最高,所以结果应该是0
输出结果如下,符合预期:
用户id : 0
PropertySource[名称:configurationProperties] : ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertySource[名称:from-environmentPrepared] : MapPropertySource {name='from-environmentPrepared'}
PropertySource[名称:commandLineArgs] : SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertySource[名称:systemProperties] : MapPropertySource {name='systemProperties'}
PropertySource[名称:systemEnvironment] : OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
PropertySource[名称:random] : RandomValuePropertySource {name='random'}
PropertySource[名称:applicationConfig: [classpath:/config/application.properties]] : OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/config/application.properties]'}
PropertySource[名称:from default.properties] : ResourcePropertySource {name='from default.properties'}
PropertySource[名称:defaultProperties] : MapPropertySource {name='defaultProperties'}
类似的我们还可以重载:contextPrepared和contextLoaded方式来实现自定义的配置。三个方法的执行顺序为:
1 environmentPrepared;2 contextPrepared;3 contextLoaded;因为我们采用的是addFirst方法,先执行会被后执行的覆盖,三者优先级是倒过来的,这点需要特别注意
执行顺序在SpringApplication#run中可以看到,可以翻看之前的一篇文章获得具体详情,这里给标注下源码和相应位置
基于 SpringApplicationRunListener#contextPrepared 扩展外部化配置属性源
- 参考SpringApplicationRunListener#environmentPrepared
基于 SpringApplicationRunListener#contextLoaded 扩展外部化配置属性源
- 参考SpringApplicationRunListener#environmentPrepared
基于 ApplicationEnvironmentPreparedEvent 扩展外部化配置属性源
- 实现ApplicationListener
接口,并重载onApplicationEvent方法
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
ConfigurableEnvironment environment = event.getEnvironment();
MutablePropertySources propertySources = environment.getPropertySources();
Map<String, Object> source = new HashMap<>();
source.put("user.id", "9");//设置程9
MapPropertySource propertySource = new MapPropertySource("from-ApplicationEnvironmentPreparedEvent", source);
propertySources.addFirst(propertySource);
}
- META-INF下新建spring.factories文件,添加如下配置
# Event Listeners
org.springframework.context.ApplicationListener=\
com.imooc.diveinspringboot.externalized.configuration.configuration.ExtendPropertySourcesEventListener
- 启动之前引导类ExtendPropertySourcesBootstrap
由于ApplicationListener是在SpringApplication构造的时候调用的,执行顺序肯定在SpringApplicationRunListener相关方法之前执行,根据执行被覆盖的原则,输出的值应该是0 -
输出结果如下:
用户id : 0
PropertySource[名称:configurationProperties] : ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertySource[名称:from-environmentPrepared] : MapPropertySource {name='from-environmentPrepared'}
PropertySource[名称:from-ApplicationEnvironmentPreparedEvent] : MapPropertySource {name='from-ApplicationEnvironmentPreparedEvent'}
PropertySource[名称:commandLineArgs] : SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertySource[名称:systemProperties] : MapPropertySource {name='systemProperties'}
PropertySource[名称:systemEnvironment] : OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
PropertySource[名称:random] : RandomValuePropertySource {name='random'}
PropertySource[名称:applicationConfig: [classpath:/config/application.properties]] : OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/config/application.properties]'}
PropertySource[名称:from default.properties] : ResourcePropertySource {name='from default.properties'}
PropertySource[名称:defaultProperties] : MapPropertySource {name='defaultProperties'}
符合我们分析的结果
基于 EnvironmentPostProcessor 扩展外部化配置属性源
- 实现 EnvironmentPostProcessor接口和Ordered接口,分别重载它们的postProcessEnvironment和getOrder方法
- 在ApplicationEnvironmentPreparedEvent之前执行,所以如果实现了其它的自定义方式,它就会被覆盖
- META-INF下新建spring.factories文件,添加如下配置
# EnvironmentPostProcessor
org.springframework.boot.env.EnvironmentPostProcessor=\
com.imooc.diveinspringboot.externalized.configuration.processor.ExtendPropertySourcesEnvironmentPostProcessor
- 具体代码SpringApplicationRunListener#environmentPrepared
基于 ApplicationContextInitializer 扩展外部化配置属性源
- 在ApplicationContextInitializer上下文初始化的时候进行配置,在SpringApplicationRunListener#environmentPrepared之后执行,所以会覆盖environmentPrepared
- 不会覆盖contextPrepared和contextLoaded;
- META-INF下新建spring.factories文件,添加如下配置
# ApplicationContextInitializer
org.springframework.context.ApplicationContextInitializer=\
com.imooc.diveinspringboot.externalized.configuration.initializer.ExtendPropertySourcesApplicationContextInitializer
- 具体代码参考SpringApplicationRunListener#environmentPrepared
各自定义PropertySource优先级(从高到低)
PropertySource[名称:from-contextLoaded] : MapPropertySource {name='from-contextLoaded'}
PropertySource[名称:from-contextPrepared] : MapPropertySource {name='from-contextPrepared'}
PropertySource[名称:from-ApplicationContextInitializer] : MapPropertySource {name='from-ApplicationContextInitializer'}
PropertySource[名称:configurationProperties] : ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertySource[名称:from-environmentPrepared] : MapPropertySource {name='from-environmentPrepared'}
PropertySource[名称:from-ApplicationEnvironmentPreparedEvent] : MapPropertySource {name='from-ApplicationEnvironmentPreparedEvent'}
PropertySource[名称:from-EnvironmentPostProcessor] : MapPropertySource {name='from-EnvironmentPostProcessor'}
PropertySource[名称:commandLineArgs] : SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertySource[名称:systemProperties] : MapPropertySource {name='systemProperties'}
PropertySource[名称:systemEnvironment] : OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
PropertySource[名称:random] : RandomValuePropertySource {name='random'}
PropertySource[名称:applicationConfig: [classpath:/config/application.properties]] : OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/config/application.properties]'}
PropertySource[名称:from classpath:META-INF/default.properties] : ResourcePropertySource {name='from classpath:META-INF/default.properties'}
PropertySource[名称:defaultProperties] : MapPropertySource {name='defaultProperties'}
实时扩展外部化配置属性源:Eviroment支持,@Value和@ConfigurationProperties 不支持
理解清楚了各种自定义外部化配置的优先级,可以在自己设计框架的时候控制不想被开发人员影响到的配置。