SpringBoot外部化配置使用Plus版的方法示例
PS:之前写过一篇关于SpringBoo中使用配置文件的一些姿势,不过嘛,有句话(我)说的好:曾见小桥流水,未睹观音坐莲!所以再写一篇增强版,以便记录。
序言
上一篇博客记录,主要集中在具体的配置内容,也就是使用@ConfigurationProperties这个注解来进行配置与结构化对象的绑定,虽然也顺带说了下@Value的使用以及其区别。
在这篇记录中,打算从总览,鸟瞰的俯视视角,来从整体上对SpringBoot,乃至SpringFramework对于外部化配置文件处理,以及配置参数的绑定操作,是如果处理的、怎么设计的。
这里其实主要说的是SpringBoot,虽然@Value属于SpringFramework的注解,不过在SpringBoot中也被频繁使用。
SpringBoot版本:2.2.6.RELEASE
SpringBoot启动流程简介
在SpringBoot的启动过程中,大体上分为三步
第一步:prepareEnvironment,准备SpringBoot执行时所有的配置。
第二步:prepareContext,根据启动时的传入的配置类,创建其BeanDefinition。
第三步:refreshContext,真正启动上下文。
在这上面三步中,第一步结束后,我们所需要的或者配置文件配置的内容,大部分已经被加载进来,然后在第三步中进行配置的注入或者绑定操作。
至于为什么是大部分,后面会有解释。
将配置从配置文件加载到Environment中,使用的是事件通知的方式。
本篇博客记录仅仅聚焦第一步中如何读取配置文件的分析,顺带介绍下第三步的注入和绑定。
受限于技术水平,仅能达到这个程度
外部化配置方式
如果有看到SpringBoot官网关于外部化配置的说明,就会惊讶的发现,原来SpringBoot有那么多的配置来源。
SpringBoot关于外部化配置特性的文档说明,直达地址。
而实际使用中,通常可能会使用的比较多的是通过以下这些方式
commandLine
通过在启动jar时,加上-DconfigKey=configValue或者--configKey=configValue的方式,来进行配置,多个配置项用空格分隔。
这种使用场景也多,只是一般用于一些配置内容很少且比较关键的配置,比如说可以决定运行环境的配置。
不易进行比较多的或者配置内容比较冗长的配置,容易出错,且不便于维护管理。
application
这种是SpringBoot提供的,用于简便配置的一种方式,只要我们将应用程序所用到的配置,直接写到application.properties中,并将文件放置于以下四个位置即可。
- 位于jar同目录的config目录下的application.properties
- 位于jar同目录的application.properties
- classpath下的config内application.properties
- classpath下的application.properties
以上配置文件类型也都可以使用yml
默认情况下,这种方式是SpringBoot约定好的一种方式,文件名必须为application,文件内容格式可以为Yaml或者Properties,也许支持XML,因为看源码是支持的,没有实践。
好处就是简单,省心省事,我们只需关注文件本身的内容就可,其他的无需关心,这也是SpringBoot要追求的结果。
缺点也很明显,如果配置内容比较冗长,为了便于管理维护,增加可读性,必须要对配置文件进行切分,通过功能等维度进行分类分组,使用多个配置文件来进行存放配置数据。
SpringBoot也想到了这些问题,因此提供了下面两个比较方便的使用方式,来应对这种情况
profiles
profiles本身是也是一个配置项,它提供一种方式将部分应用程序配置进行隔离,并且使得它仅在具体某一个环境中可用。
具体实践中常用的主要是针对不同的环境,有开发环境用到的特有配置值,有测试环境特有的配置,有生产环境特有的配置,包括有些Bean根据环境选择决定是否进行实例化,这些都是通过profiles来实现的。不过这里只关注配置这一块内容。
它的使用方式通常是spring.profiles.active=dev,dev1或者spring.profiles.include=db1,db2
这里可以看到有两种不同的用法,这两种方式是有区别的。
如果在application.properties中定义了一个spring.profiles.active=dev,而后在启动时通过命令行又写了个--spring.profiles.active=test,那么最终使用的是test,而不是dev。
如果同样的场景下,使用spring.profiles.include来替换spring.profiles.active,那么结果会是dev和test都会存在,而不是替换的行为。
这就是两个之间的差别,这种差别也使得他们使用的场景并不一样,active更适合那些需要互斥的环境,而include则是多个并存的配置。
仅仅配置了profiles是没有意义的,必须要有相应的配置文件配合一起使用,而且这些配置文件的命名要符合一定的规则,否则配置文件不会被加载进Environment的。
profiles文件的命名规则为application-*.properties,同样的,application.properties能放置的位置它也可以,不能的,它也不可以。
properysource
注解@PropertySource可以写在配置类上,并且指定要读取的配置文件路径,这个路径可以是绝对路径,也可以是相对路径。
它可以有以下几种配置
- @PropertySource("/config.properties")
- @PropertySource("config.properties")
- @PropertySource("file:/usr/local/config.properties")
- @PropertySource("file:./config.properties")
- @PropertySource("${pathPrefix}/config.properties")
其中1和2两种方式是一样的,都是从classpath去开始查找的
3和4是使用文件系统的绝对和相对路径的方式,这里绝对路径比较好理解,相对路径则是从项目的根目录作为相对目录的
5是结合SpEL的表达式来使用的,可以直接从环境中获取配置好的路径。
以上几种方式在实际开发中遇到和SpringBoot相关的配置,基本都能应付过来了。
不过对于上面配置的一些原理性的内容,还没有提到,下面会简单说一下SpringBoot关于配置更详细的处理,以及配置的优先级的问题。
原理浅入浅出
带着问题去找原因,比较有目的性和针对性,效果也相对好一些。
所以这里描述几个会引起疑问的现象
默认情况下自动加载的配置文件命名必须要是application
在使用application.properties时,可以同时在四个位置放置配置,配置的优先级就是上面罗列时显示的优先级。同样的配置,优先级高的生效,优先级低的忽略。
profiles引入的配置,也准守同样的优先级规则
命令行配置具有最高优先级
有些配置不能使用@PropertySource的方式进行注入,比如日志的配置。
如果一个配置类使用了@ConfigurationProperties,然后字段使用了@Value,@ConfigurationProperties先被处理,@Value后被处理。
源码简读
SpringBoot读取application.properties配置
查看org.springframework.boot.context.config.ConfigFileApplicationListener的源码
publicclassConfigFileApplicationListenerimplementsEnvironmentPostProcessor,SmartApplicationListener,Ordered{
//Notetheorderisfromleasttomostspecific(lastonewins)
//默认检索配置文件的路径,优先级越来越高,
//可以通过spring.config.location重新指定,要早于当前类执行时配置好
privatestaticfinalStringDEFAULT_SEARCH_LOCATIONS="classpath:/,classpath:/config/,file:./,file:./config/";
//默认的配置名,可以通过命令行配置--spring.config.name=xxx来重新指定
//不通过命令行也可以通过其他方式,环境变量这些。
privatestaticfinalStringDEFAULT_NAMES="application";
privateclassLoader{
//找到配置的路径
privateSetgetSearchLocations(){
if(this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)){
returngetSearchLocations(CONFIG_LOCATION_PROPERTY);
}
Setlocations=getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
locations.addAll(
asResolvedSet(ConfigFileApplicationListener.this.searchLocations,DEFAULT_SEARCH_LOCATIONS));
returnlocations;
}
//解析成Set
privateSetasResolvedSet(Stringvalue,Stringfallback){
Listlist=Arrays.asList(StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(
(value!=null)?this.environment.resolvePlaceholders(value):fallback)));
//这里会做一个反转,也就是配置的路径中,放在后面的优先级越高
Collections.reverse(list);
returnnewLinkedHashSet<>(list);
}
privateSetgetSearchNames(){
if(this.environment.containsProperty(CONFIG_NAME_PROPERTY)){
Stringproperty=this.environment.getProperty(CONFIG_NAME_PROPERTY);
returnasResolvedSet(property,null);
}
returnasResolvedSet(ConfigFileApplicationListener.this.names,DEFAULT_NAMES);
}
}
}     
命令行的配置具有最高优先级
protectedvoidconfigurePropertySources(ConfigurableEnvironmentenvironment,String[]args){
MutablePropertySourcessources=environment.getPropertySources();
if(this.defaultProperties!=null&&!this.defaultProperties.isEmpty()){
sources.addLast(newMapPropertySource("defaultProperties",this.defaultProperties));
}
//支持从命令行添加属性以及存在参数时
if(this.addCommandLineProperties&&args.length>0){
Stringname=CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
//这里是看下是不是存在同名的配置了
if(sources.contains(name)){
PropertySource>source=sources.get(name);
CompositePropertySourcecomposite=newCompositePropertySource(name);
composite.addPropertySource(
newSimpleCommandLinePropertySource("springApplicationCommandLineArgs",args));
composite.addPropertySource(source);
sources.replace(name,composite);
}
else{
//直接添加,并且是添加到第一个位置,具有最高优先级
sources.addFirst(newSimpleCommandLinePropertySource(args));
}
}
}
@PropertySource是在refreshContext阶段,执行BeanDefinitionRegistryPostProcessor时处理的
//org.springframework.context.annotation.ConfigurationClassParser#processPropertySource
privatevoidprocessPropertySource(AnnotationAttributespropertySource)throwsIOException{
Stringname=propertySource.getString("name");
if(!StringUtils.hasLength(name)){
name=null;
}
Stringencoding=propertySource.getString("encoding");
if(!StringUtils.hasLength(encoding)){
encoding=null;
}
//获取配置的文件路径
String[]locations=propertySource.getStringArray("value");
Assert.isTrue(locations.length>0,"Atleastone@PropertySource(value)locationisrequired");
booleanignoreResourceNotFound=propertySource.getBoolean("ignoreResourceNotFound");
//指定的读取配置文件的工厂
ClassfactoryClass=propertySource.getClass("factory");
//没有就用默认的
PropertySourceFactoryfactory=(factoryClass==PropertySourceFactory.class?
DEFAULT_PROPERTY_SOURCE_FACTORY:BeanUtils.instantiateClass(factoryClass));
//循环加载
for(Stringlocation:locations){
try{
//会解析存在占位符的情况
StringresolvedLocation=this.environment.resolveRequiredPlaceholders(location);
//使用DefaultResourceLoader来加载资源
Resourceresource=this.resourceLoader.getResource(resolvedLocation);
//创建PropertySource对象
addPropertySource(factory.createPropertySource(name,newEncodedResource(resource,encoding)));
}
catch(IllegalArgumentException|FileNotFoundException|UnknownHostExceptionex){
//Placeholdersnotresolvableorresourcenotfoundwhentryingtoopenit
if(ignoreResourceNotFound){
if(logger.isInfoEnabled()){
logger.info("Propertieslocation["+location+"]notresolvable:"+ex.getMessage());
}
}
else{
throwex;
}
}
}
}
因为执行时机的问题,有些配置不能使用@PropertySource,因为这个时候对有些配置来说,如果使用这种配置方式,黄花菜都凉了。同时这个注解要配合@Configuration注解一起使用才能生效,使用@Component是不行的。
处理@ConfigurationProperty的处理器是一个BeanPostProcessor,处理@Value的也是一个BeanPostProcessor,不过他俩的优先级并不一样,
//@ConfigurationProperty
publicclassConfigurationPropertiesBindingPostProcessor
implementsBeanPostProcessor,PriorityOrdered,ApplicationContextAware,InitializingBean{
@Override
publicintgetOrder(){
returnOrdered.HIGHEST_PRECEDENCE+1;
}
}
//@Value
publicclassAutowiredAnnotationBeanPostProcessorextendsInstantiationAwareBeanPostProcessorAdapter
implementsMergedBeanDefinitionPostProcessor,PriorityOrdered,BeanFactoryAware{
privateintorder=Ordered.LOWEST_PRECEDENCE-2;
@Override
publicintgetOrder(){
returnthis.order;
}
}
从上面可以看出处理@ConfigurationProperty的BeanPostProcessor优先级很高,而@Value的BeanPostProcessor优先级很低。
使用@Value注入时,要求配置的key必须存在于Environment中的,否则会终止启动,而@ConfigurationProperties则不会。
@Value可以支持SpEL表达式,也支持占位符的方式。
自定义配置读取
org.springframework.boot.context.config.ConfigFileApplicationListener是一个监听器,同时也是一个EnvironmentPostProcessor,在有ApplicationEnvironmentPreparedEvent事件触发时,会去处理所有的EnvironmentPostProcessor的实现类,同时这些个实现也是使用SpringFactoriesLoader的方式来加载的。
对于配置文件的读取,就是使用的这种方式。
@Override
publicvoidonApplicationEvent(ApplicationEventevent){
if(eventinstanceofApplicationEnvironmentPreparedEvent){
onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent)event);
}
if(eventinstanceofApplicationPreparedEvent){
onApplicationPreparedEvent(event);
}
}
privatevoidonApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEventevent){
ListpostProcessors=loadPostProcessors();
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
for(EnvironmentPostProcessorpostProcessor:postProcessors){
postProcessor.postProcessEnvironment(event.getEnvironment(),event.getSpringApplication());
}
}
 
有了这个扩展点后,我们就能自己定义读取任何配置,从任何地方。
只要实现了EnvironmentPostProcessor接口,并且在META-INF/spring.factories中配置一下
org.springframework.boot.env.EnvironmentPostProcessor=com.example.configuration.ConfigurationFileLoader
附一个自己写的例子
publicclassConfigurationFileLoaderimplementsEnvironmentPostProcessor{
privatestaticfinalStringDEFAULT_SEARCH_LOCATIONS="classpath:/,classpath:/config/,file:./,file:./config/";
privatestaticfinalStringDEFAULT_NAMES="download";
privatestaticfinalStringDEFAULT_FILE_EXTENSION=".yml";
@Override
publicvoidpostProcessEnvironment(ConfigurableEnvironmentenvironment,
SpringApplicationapplication){
Listlist=Arrays.asList(StringUtils.trimArrayElements(
StringUtils.commaDelimitedListToStringArray(DEFAULT_SEARCH_LOCATIONS)));
Collections.reverse(list);
SetreversedLocationSet=newLinkedHashSet(list);
ResourceLoaderdefaultResourceLoader=newDefaultResourceLoader();
YamlPropertiesFactoryBeanyamlPropertiesFactoryBean=newYamlPropertiesFactoryBean();
ListloadedProperties=newArrayList<>(2);
reversedLocationSet.forEach(location->{
Resourceresource=defaultResourceLoader.getResource(location+DEFAULT_NAMES+DEFAULT_FILE_EXTENSION);
if(resource==null||!resource.exists()){
return;
}
yamlPropertiesFactoryBean.setResources(resource);
Propertiesproperties=yamlPropertiesFactoryBean.getObject();
loadedProperties.add(properties);
});
PropertiesfilteredProperties=newProperties();
Set   
基本上都是参考ConfigFileApplicationListener写的,不过这里实现的功能,其实可以通过@PropertySource来解决,只是当时不知道。
使用@PropertySource的话,这么写@PropertySource("file:./download.properties")即可。
个人猜测SpringBoot从配置中心加载配置就是使用的这个方式,不过由于没有实际看过相关源码确认,不敢说一定是的,但是应该是八九不离十的。
总结
这篇记录写的有点乱,一个是涉及到东西感觉也不少,还有就是本身有些地方不怎么了解,花费的时间不够。
不过对SpringBoot的外部化配置来说,就是将各个途径加载进来的配置,统一收归Environment的MutablePropertySources字段,这个字段是一个ArrayList,保持添加进来时的顺序,因此查找也是按照这个顺序查找,查找时查到即返回,不会完全遍历所有的配置,除非遇到不存在的。
整个设计思想就是使用集中所有的配置,进行优先级排序,最后在有需要获取配置的地方,从Environment对象中查找配置项。
对一般使用来说,关注点就是配置文件的位置,配置文件的名,以及优先级,这三个方面比较关心。
这篇记录也基本能解答这几个疑问,完成了写这篇记录的初衷。
到此这篇关于SpringBoot外部化配置使用Plus版的方法示例的文章就介绍到这了,更多相关SpringBoot外部化配置Plus版内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
