Spring中PropertySource属性源配置文件的优先级、顺序问题大解析(加载流程)

2022-06-20 09:15:45

每篇一句

当没有外敌的时候,我们是对手。当家被侵犯的时候,我们是兄弟

前言

关于Spring的配置文件的优先级、加载顺序一直是个老生常谈的问题。但即使经常被提起,却还是经常被忘记或者弄混。有一种听了很多道理但仍过不好这一生的赶脚有木有。

如果你度娘上去搜索:Spring Boot 配置顺序关键字,会发现相关的文章非常之多,足以可见这个问题的热度。但我们普遍遇到的一个问题是:看的时候都知道,但用的时候又有很多的不确定~

怎么破?我个人经验是若想把这块变成永久记忆,一种方式是理解作者的设计意图,掌握其设计思想**(推荐,因为设计思想都是相通的)**。另外一种就是把它落实在源码上,毕竟查源码找顺序比在度娘看别人的二手信息来得踏实。

另外,我把这篇文章定位为:Spring Boot配置文件加载顺序先行篇。因为只有SpringBoot才会自动的加载对应的属性源,而Spring Framework是没有这么自动化的(都是手动的)。

若想要快速知道结论,你也可以直接阅读:
【小家Spring】一篇文章彻底搞懂Spring Boot配置文件的加载顺序(项目内部配置和外部配置)

PropertySource

此处指的是org.springframework.core.env.PropertySource,而不是注解org.springframework.context.annotation.PropertySource,注意区分

PropertySource是抽象类,表示一个键值对,代表着属性源。Spring内部是通过它来承载来自不同地方都的属性源的。

Spring认为每个属性源都应该是有名称的,也就是作为属性源的key~

// @since 3.1publicabstractclassPropertySource<T>{protectedfinal String name;// 该属性源的名字protectedfinal T source;publicPropertySource(String name, T source){
		Assert.hasText(name,"Property source name must contain at least one character");
		Assert.notNull(source,"Property source must not be null");this.name= name;this.source= source;}// 若没有指定source 默认就是object  而不是nullpublicPropertySource(String name){this(name,(T)newObject());}// getProperty是个抽象方法  子类去实现~~~// 小细节:若对应的key存在但是值为null,此处也是返回false的  表示不包含~publicbooleancontainsProperty(String name){return(getProperty(name)!= null);}@Nullablepublicabstract ObjectgetProperty(String name);// 此处特别特别注意重写的这两个方法,我们发现它只和name有关,只要name相等  就代表着是同一个对象~~~~ 这点特别重要~@Overridepublicbooleanequals(Object other){return(this== other||(otherinstanceofPropertySource&&
				ObjectUtils.nullSafeEquals(this.name,((PropertySource<?>) other).name)));}@OverridepublicinthashCode(){return ObjectUtils.nullSafeHashCode(this.name);}// 静态方法:根据name就创建一个属性源~  ComparisonPropertySource是StubPropertySource的子类~publicstatic PropertySource<?>named(String name){returnnewComparisonPropertySource(name);}}

该类重写了equals()hashCode()方法,所以对于List的remove、indexOf方法都是有影响的~~~后续会看到

PropertySource提供了一个named(String name)方法用于构造基于name的PropertySource的空实现,从而便于PropertySource 集合中查找指定属性命的PropertySource(毕竟上面说了它只和name有关~)。

这个抽象类告诉我们,PropertySource的name非常的重要。接下来重点就是它的实现们,它的继承树如下:
在这里插入图片描述

JndiPropertySource

显然它和Jndi有关。JNDI:Java Naming and Directory Interface Java命名和目录接口
题外话:我认为它是IoC的鼻祖,在Tomcat中有大量的应用,Spring或许都是抄它的~

// @since 3.1  它的source源是JndiLocatorDelegatepublicclassJndiPropertySourceextendsPropertySource<JndiLocatorDelegate>{public ObjectgetProperty(String name){...
		Object value=this.source.lookup(name);...}}

它的lookup方法就是依赖查找的精髓。由于现在是Spring的天下,Jndi确实使用太少了,此处一笔带过。

备注:上篇文章我们已经知道了,web环境默认情况下的StandardServletEnvironment初始化的时候是会把JndiPropertySource放进环境里去的,name为:jndiProperties
JndiTemplate是Spring提供的对JNDI的访问模版

EnumerablePropertySource

这是PropertySource的一个最重要分支,绝大部分配置源都继承于它。Enumerable:可枚举的

publicabstractclassEnumerablePropertySource<T>extendsPropertySource<T>{publicEnumerablePropertySource(String name, T source){super(name, source);}protectedEnumerablePropertySource(String name){super(name);}@OverridepublicbooleancontainsProperty(String name){return ObjectUtils.containsElement(getPropertyNames(), name);}// 返回所有Property的names(keys)publicabstract String[]getPropertyNames();}

该抽象类主要提供抽象方法getPropertyNames()表示每个key都应该是可以枚举的

ServletContextPropertySource

它的属性源是ServletContext,此属于源头用于暴露和访问Servlet上下文的一些InitParameters

publicclassServletContextPropertySourceextendsEnumerablePropertySource<ServletContext>{publicServletContextPropertySource(String name, ServletContext servletContext){super(name, servletContext);}@Overridepublic String[]getPropertyNames(){return StringUtils.toStringArray(this.source.getInitParameterNames());}@Override@Nullablepublic StringgetProperty(String name){returnthis.source.getInitParameter(name);}}

ServletConfigPropertySource

source源为ServletConfig,源码逻辑同上。

ConfigurationPropertySource

需要注意:这个不是Spring提供的,你导入了commons-configuration2这个jar时才会有这个类。source源为:org.apache.commons.configuration2.Configuration

MapPropertySource

这是一个较为常用的属性源,一般我们自己new往里添加时,会使用它。
它的source源为:Map<String, Object>,还是非常的通用的~

publicclassMapPropertySourceextendsEnumerablePropertySource<Map<String, Object>>{publicMapPropertySource(String name, Map<String, Object> source){super(name, source);}@Override@Nullablepublic ObjectgetProperty(String name){returnthis.source.get(name);}@OverridepublicbooleancontainsProperty(String name){returnthis.source.containsKey(name);}// map里所有的key就行~@Overridepublic String[]getPropertyNames(){return StringUtils.toStringArray(this.source.keySet());}}
PropertiesPropertySource

继承自MapPropertySource,不解释~

ResourcePropertySource:我们注解导入使用的是它

ResourcePropertySource它继承自PropertiesPropertySource。它处理用org.springframework.core.io.Resource装载的Properties文件

// @since 3.1  若你的Properties资源使用的Resource装机进来的  直接使用它即可publicclassResourcePropertySourceextendsPropertiesPropertySource{@Nullableprivatefinal String resourceName;publicResourcePropertySource(String name, EncodedResource resource)throws IOException{// 注意此处加载的最好是EncodedResource,因为Properties文件是需要处理乱码的~super(name, PropertiesLoaderUtils.loadProperties(resource));this.resourceName=getNameForResource(resource.getResource());}publicResourcePropertySource(EncodedResource resource)throws IOException{super(getNameForResource(resource.getResource()), PropertiesLoaderUtils.loadProperties(resource));this.resourceName= null;}publicResourcePropertySource(String name, Resource resource)throws IOException{super(name, PropertiesLoaderUtils.loadProperties(newEncodedResource(resource)));this.resourceName=getNameForResource(resource);}publicResourcePropertySource(String name, String location, ClassLoader classLoader)throws IOException{this(name,newDefaultResourceLoader(classLoader).getResource(location));}publicResourcePropertySource(String location, ClassLoader classLoader)throws IOException{this(newDefaultResourceLoader(classLoader).getResource(location));}publicResourcePropertySource(String name, String location)throws IOException{this(name,newDefaultResourceLoader().getResource(location));}...}

它有非常多的重载构造函数,这是Spring设计中最为常用的模式之一~~~目的权是为了让使用者越简单、越方便越好

CommandLinePropertySource

顾名思义,它表示命令行属性源。它这个泛型T可能最简单的String[],也可以是OptionSet(依赖joptsimple这个jar)

在传统的Spring应用中,命令行参数一般存在于main方法的入参里就够了,但是在某些特殊的情况下,它需要被注入到Spring Bean中。

如下案例:我们手动把命令行参数放进Spring容器内:

publicstaticvoidmain(String[] args)throws Exception{
        CommandLinePropertySource clps=newSimpleCommandLinePropertySource(args);// 启动容器
        AnnotationConfigApplicationContext ctx=newAnnotationConfigApplicationContext();
        ConfigurableEnvironment environment= ctx.getEnvironment();
        environment.getPropertySources().addFirst(clps);
        ctx.register(RootConfig.class);
        ctx.refresh();

        System.out.println(ArrayUtils.toString(args));//{--server.port=8080}
        System.out.println(environment.getProperty("server.port"));//8080}

此处:Spring命令行参数为--server.port=8080,有的时候你会看到-D参数,这里用一个示例注意区分一下这两者的区别。
在这里插入图片描述
运行结果如下:

@Slf4jpublicclassMain{publicstaticvoidmain(String[] args)throws Exception{// vm参数里(其实就是java -Xmx512m -Dmyname=fsx)  的-D参数最终都会到System系统属性里面去
        System.out.println(System.getProperty("myname"));//fsx// --开头的命令行参数  是可以被spring应用识别的特定格式
        System.out.println(ArrayUtils.toString(args));// {--server.port=8080,fsx}}}

Enviroment环境内容值截图如下:
在这里插入图片描述
经过我这一番处理(放进容器)后,Environment被注入到Spring Bean内,就会含有这些命令行属性值,然后就直接可以在Spring Bean中使用了

使用Environment获取属性值的原理上篇博文有解释:属性源最终都被加入进Environment持有的属性:MutablePropertySources保存着。So,我们使用@Value也可以从它里面取值的~

// @since 3.1publicabstractclassCommandLinePropertySource<T>extendsEnumerablePropertySource<T>{// 命令行选项参数publicstaticfinal String COMMAND_LINE_PROPERTY_SOURCE_NAME="commandLineArgs";// 非选项参数 的名称publicstaticfinal String DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME="nonOptionArgs";private String nonOptionArgsPropertyName= DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME;// 可以看到若调用者没有指定  会使用这个默认值的~publicCommandLinePropertySource(T source){super(COMMAND_LINE_PROPERTY_SOURCE_NAME, source);}...// containsOption和getNonOptionArgs都是抽象方法@OverridepublicfinalbooleancontainsProperty(String name){// 若你是的name是`nonOptionArgs`  那就是非选项参数中不为空 就是trueif(this.nonOptionArgsPropertyName.equals(name)){return!this.getNonOptionArgs().isEmpty();}returnthis.containsOption(name);}@Override@Nullablepublicfinal StringgetProperty(String name){if(this.nonOptionArgsPropertyName.equals(name)){
			Collection<String> nonOptionArguments=this.getNonOptionArgs();if(nonOptionArguments.isEmpty()){return null;}else{// 显然非选项参数是多个 最终逗号分隔后再返回return StringUtils.collectionToCommaDelimitedString(nonOptionArguments);}}// 选项参数使用getOptionValues 若它是一个集合,那就用逗号分隔后再返回
		Collection<String> optionValues=this.getOptionValues(name);if(optionValues== null){return null;}else{return StringUtils.collectionToCommaDelimitedString(optionValues);}}protectedabstractbooleancontainsOption(String name);@Nullableprotectedabstract List<String>getOptionValues(String name);protectedabstract List<String>getNonOptionArgs();}

选项参数:能够通过如上--server.port=9090这种方式传入的,可以传入多个值或者列表等
非选项参数:我们在命令行传递除了vm参数的所以其它参数。比如我上面写成--server.port=9090 fsx最终的结果如下(非选项参数是个List装载的)
在这里插入图片描述

SimpleCommandLinePropertySource

它是我们最为常用的一个属性源之一,source类型为CommandLineArgs:Spring内部使用的一个类。CommandLineArgs内部维护着Map<String, List<String>> optionArgsList<String> nonOptionArgs来表示整个命令行消息

我们构造使用它只需要把命令行的String[]数组扔进来即可,非常的方便。

publicclassSimpleCommandLinePropertySourceextendsCommandLinePropertySource<CommandLineArgs>{// SimpleCommandLineArgsParser解析这个数组。// 注意:它识别的是--而不是-DpublicSimpleCommandLinePropertySource(String... args){super(newSimpleCommandLineArgsParser().parse(args));}publicSimpleCommandLinePropertySource(String name, String[] args){super(name,newSimpleCommandLineArgsParser().parse(args));}// 可见最终的source类型是CommandLineArgs类型~~~// 下面实现最终都委托给CommandLineArgs去处理~@Overridepublic String[]getPropertyNames(){return StringUtils.toStringArray(this.source.getOptionNames());}@OverrideprotectedbooleancontainsOption(String name){returnthis.source.containsOption(name);}@Override@Nullableprotected List<String>getOptionValues(String name){returnthis.source.getOptionValues(name);}@Overrideprotected List<String>getNonOptionArgs(){returnthis.source.getNonOptionArgs();}}
JOptCommandLinePropertySource

基于JOpt Simple的属性源实现,JOpt Simple是一个解析命令行选项参数的第三方库。它能够自定义格式、从文件中解析等高级操作,处略~



SpringBoot扩展的PropertySource

此处讲到了PropertySource,所以把SpringBoot对它的扩展也一并说说。SpringBoot主要扩展了两个属性源RandomValuePropertySourceAnnotationsPropertySource

RandomValuePropertySource

随机属性源,系统中用到随机数的地方使用它配置非常非常好使,如下:

my.secret=${random.value}
my.number=${random.int}
my.bignumber=${random.long}
my.number.less.than.ten=${random.int(10)}
my.number.in.range=${random.int[1024,65536]}

书写配置文件的时候也能得到如下提示:
在这里插入图片描述

publicclassRandomValuePropertySourceextendsPropertySource<Random>{...privatestaticfinal String PREFIX="random.";// 必须以random.打头...private ObjectgetRandomValue(String type){if(type.equals("int")){returngetSource().nextInt();}if(type.equals("long")){returngetSource().nextLong();}
		String range=getRange(type,"int");if(range!= null){returngetNextIntInRange(range);}
		range=getRange(type,"long");if(range!= null){returngetNextLongInRange(range);}if(type.equals("uuid")){return UUID.randomUUID().toString();}returngetRandomBytes();}...}

给出一个示例如下:

@Slf4jpublicclassMain{publicstaticvoidmain(String[] args){// 自定义的一个随机值属性源,起名叫做 myRandom
        RandomValuePropertySource random=newRandomValuePropertySource
  • 作者:方向盘(YourBatman)
  • 原文链接:https://fangshixiang.blog.csdn.net/article/details/91399789
    更新时间:2022-06-20 09:15:45