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

前言

关于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.1 
public abstract class PropertySource<T> {

	protected final String name; // 该属性源的名字
	protected final T source;

	public PropertySource(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  而不是null
	public PropertySource(String name) {
		this(name, (T) new Object());
	}

	// getProperty是个抽象方法  子类去实现~~~
	// 小细节:若对应的key存在但是值为null,此处也是返回false的  表示不包含~
	public boolean containsProperty(String name) {
		return (getProperty(name) != null);
	}
	@Nullable
	public abstract Object getProperty(String name);


	// 此处特别特别注意重写的这两个方法,我们发现它只和name有关,只要name相等  就代表着是同一个对象~~~~ 这点特别重要~
	@Override
	public boolean equals(Object other) {
		return (this == other || (other instanceof PropertySource &&
				ObjectUtils.nullSafeEquals(this.name, ((PropertySource<?>) other).name)));
	}
	@Override
	public int hashCode() {
		return ObjectUtils.nullSafeHashCode(this.name);
	}


	// 静态方法:根据name就创建一个属性源~  ComparisonPropertySource是StubPropertySource的子类~
	public static PropertySource<?> named(String name) {
		return new ComparisonPropertySource(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源是JndiLocatorDelegate
public class JndiPropertySource extends PropertySource<JndiLocatorDelegate> {
	public Object getProperty(String name) {
		...
		Object value = this.source.lookup(name);
		...
	}
}

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

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

EnumerablePropertySource

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

public abstract class EnumerablePropertySource<T> extends PropertySource<T> {
	public EnumerablePropertySource(String name, T source) {
		super(name, source);
	}
	protected EnumerablePropertySource(String name) {
		super(name);
	}
	@Override
	public boolean containsProperty(String name) {
		return ObjectUtils.containsElement(getPropertyNames(), name);
	}
	
	// 返回所有Property的names(keys)
	public abstract String[] getPropertyNames();
}

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

ServletContextPropertySource

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

public class ServletContextPropertySource extends EnumerablePropertySource<ServletContext> {
	public ServletContextPropertySource(String name, ServletContext servletContext) {
		super(name, servletContext);
	}
	@Override
	public String[] getPropertyNames() {
		return StringUtils.toStringArray(this.source.getInitParameterNames());
	}
	@Override
	@Nullable
	public String getProperty(String name) {
		return this.source.getInitParameter(name);
	}
}

ServletConfigPropertySource

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

ConfigurationPropertySource

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

MapPropertySource

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

public class MapPropertySource extends EnumerablePropertySource<Map<String, Object>> {

	public MapPropertySource(String name, Map<String, Object> source) {
		super(name, source);
	}
	@Override
	@Nullable
	public Object getProperty(String name) {
		return this.source.get(name);
	}
	@Override
	public boolean containsProperty(String name) {
		return this.source.containsKey(name);
	}
	// map里所有的key就行~
	@Override
	public String[] getPropertyNames() {
		return StringUtils.toStringArray(this.source.keySet());
	}

}
PropertiesPropertySource

继承自MapPropertySource,不解释~

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

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

 // @since 3.1  若你的Properties资源使用的Resource装机进来的  直接使用它即可
 public class ResourcePropertySource extends PropertiesPropertySource {

	@Nullable
	private final String resourceName;

 	public ResourcePropertySource(String name, EncodedResource resource) throws IOException {
 		// 注意此处加载的最好是EncodedResource,因为Properties文件是需要处理乱码的~
		super(name, PropertiesLoaderUtils.loadProperties(resource));
		this.resourceName = getNameForResource(resource.getResource());
	}
	public ResourcePropertySource(EncodedResource resource) throws IOException {
		super(getNameForResource(resource.getResource()), PropertiesLoaderUtils.loadProperties(resource));
		this.resourceName = null;
	}
	public ResourcePropertySource(String name, Resource resource) throws IOException {
		super(name, PropertiesLoaderUtils.loadProperties(new EncodedResource(resource)));
		this.resourceName = getNameForResource(resource);
	}
	public ResourcePropertySource(String name, String location, ClassLoader classLoader) throws IOException {
		this(name, new DefaultResourceLoader(classLoader).getResource(location));
	}
	public ResourcePropertySource(String location, ClassLoader classLoader) throws IOException {
		this(new DefaultResourceLoader(classLoader).getResource(location));
	}
	public ResourcePropertySource(String name, String location) throws IOException {
		this(name, new DefaultResourceLoader().getResource(location));
	}
	...
 }

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

CommandLinePropertySource

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

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

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

    public static void main(String[] args) throws Exception {
        CommandLinePropertySource clps = new SimpleCommandLinePropertySource(args);

        // 启动容器
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        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参数,这里用一个示例注意区分一下这两者的区别。

运行结果如下:

@Slf4j
public class Main {

    public static void main(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.1
public abstract class CommandLinePropertySource<T> extends EnumerablePropertySource<T> {

	// 命令行选项参数
	public static final String COMMAND_LINE_PROPERTY_SOURCE_NAME = "commandLineArgs";
	// 非选项参数 的名称
	public static final String DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME = "nonOptionArgs";
	private String nonOptionArgsPropertyName = DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME;


	// 可以看到若调用者没有指定  会使用这个默认值的~
	public CommandLinePropertySource(T source) {
		super(COMMAND_LINE_PROPERTY_SOURCE_NAME, source);
	}
	...

	// containsOption和getNonOptionArgs都是抽象方法  
	@Override
	public final boolean containsProperty(String name) {
		// 若你是的name是`nonOptionArgs`  那就是非选项参数中不为空 就是true
		if (this.nonOptionArgsPropertyName.equals(name)) {
			return !this.getNonOptionArgs().isEmpty();
		}
		return this.containsOption(name);
	}

	@Override
	@Nullable
	public final String getProperty(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);
		}
	}

	protected abstract boolean containsOption(String name);

	@Nullable
	protected abstract List<String> getOptionValues(String name);
	protected abstract 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[]数组扔进来即可,非常的方便。

public class SimpleCommandLinePropertySource extends CommandLinePropertySource<CommandLineArgs> {

	// SimpleCommandLineArgsParser解析这个数组。
	// 注意:它识别的是--而不是-D
	public SimpleCommandLinePropertySource(String... args) {
		super(new SimpleCommandLineArgsParser().parse(args));
	}
	public SimpleCommandLinePropertySource(String name, String[] args) {
		super(name, new SimpleCommandLineArgsParser().parse(args));
	}
	// 可见最终的source类型是CommandLineArgs类型~~~
	// 下面实现最终都委托给CommandLineArgs去处理~
	@Override
	public String[] getPropertyNames() {
		return StringUtils.toStringArray(this.source.getOptionNames());
	}
	@Override
	protected boolean containsOption(String name) {
		return this.source.containsOption(name);
	}
	@Override
	@Nullable
	protected List<String> getOptionValues(String name) {
		return this.source.getOptionValues(name);
	}
	@Override
	protected List<String> getNonOptionArgs() {
		return this.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]}

书写配置文件的时候也能得到如下提示:

public class RandomValuePropertySource extends PropertySource<Random> {
	...
	private static final String PREFIX = "random."; // 必须以random.打头
	...
	private Object getRandomValue(String type) {
		if (type.equals("int")) {
			return getSource().nextInt();
		}
		if (type.equals("long")) {
			return getSource().nextLong();
		}
		String range = getRange(type, "int");
		if (range != null) {
			return getNextIntInRange(range);
		}
		range = getRange(type, "long");
		if (range != null) {
			return getNextLongInRange(range);
		}
		if (type.equals("uuid")) {
			return UUID.randomUUID().toString();
		}
		return getRandomBytes();
	}
	...
}

给出一个示例如下:

@Slf4j
public class Main {
    public static void main(String[] args) {
        // 自定义的一个随机值属性源,起名叫做 myRandom
        RandomValuePropertySource random = new RandomValuePropertySource("myRandom");
        // 随机生成一个整数
        log.info("random int:{}", random.getProperty("random.int"));
        // 随机生成一个整数,指定上边界,不大于等于1
        log.info("random int(1):{}", random.getProperty("random.int(1)"));
        // 随机生成一个整数,指定上边界,不大于等于5
        log.info("random int(5):{}", random.getProperty("random.int(5)"));

        // 随机生成一个整数,使用区间[0,1),前闭后开=>只能是1
        // 注意区间的表示法:使用()包围,2个字符
        log.info("random int(0,1):{}", random.getProperty("random.int(0,1)"));
        // 随机生成一个整数,使用区间[1,3),前闭后开=>只能是1或者2
        // 注意区间的表示法:使用空格包围,2个字符,前后各一个空格
        log.info("random int(1,3):{}", random.getProperty("random.int 1,3 "));
        // 随机生成一个整数,使用区间[3,4),前闭后开=>只能是3
        // 注意区间的表示法:使用汉字包围,2个字符,前后各一个汉字自负
        log.info("random int(3,4):{}", random.getProperty("random.int底3,4顶"));
        // 随机生成一个整数,使用区间[5,6),前闭后开=>只能是5
        // 注意区间的表示法:使用英文字母包围,2个字符,前后各一个英文字母
        log.info("random int(5,6):{}", random.getProperty("random.intL5,6U"));
        // 随机生成一个整数,使用区间[5,6),前闭后开=>只能是5
        // 注意区间的表示法:使用数字包围,2个字符,前一个数字5,后一个数字6
        log.info("random int(5,6):{}", random.getProperty("random.int55,66"));

        // 随机生成一个长整数
        log.info("random long:{}", random.getProperty("random.long"));
        // 随机生成一个整数,使用区间[100,101),前闭后开=>只能是100
        log.info("random long(100,101):{}", random.getProperty("random.long(100,101)"));

        // 随机生成一个 uuid
        log.info("random uuid:{}", random.getProperty("random.uuid"));
    }
}

打印结果:

17:31:56.629 [main] INFO com.fsx.boot1demo1.maintest.Main - random int:-1019461798
17:31:56.648 [main] INFO com.fsx.boot1demo1.maintest.Main - random int(1):0
17:31:56.648 [main] INFO com.fsx.boot1demo1.maintest.Main - random int(5):0
17:31:56.651 [main] INFO com.fsx.boot1demo1.maintest.Main - random int(0,1):0
17:31:56.651 [main] INFO com.fsx.boot1demo1.maintest.Main - random int(1,3):1
17:31:56.651 [main] INFO com.fsx.boot1demo1.maintest.Main - random int(3,4):3
17:31:56.651 [main] INFO com.fsx.boot1demo1.maintest.Main - random int(5,6):5
17:31:56.651 [main] INFO com.fsx.boot1demo1.maintest.Main - random int(5,6):5
17:31:56.652 [main] INFO com.fsx.boot1demo1.maintest.Main - random long:-7444646397347920224
17:31:56.653 [main] INFO com.fsx.boot1demo1.maintest.Main - random long(100,101):100
17:31:57.035 [main] INFO com.fsx.boot1demo1.maintest.Main - random uuid:77d0c3e1-ae05-41e3-ba8d-6c168c1b9a3c
AnnotationsPropertySource

关于这个注解属性源,它需要结合@PropertyMapping一起使用,稍微稍微复杂点,这个我把它放在SpringBoot章节中进行详细讲解~



PropertySources

从命名中就可以看出,它是PropertySource的一个复数形式,但是它是接口而不是抽象类。 它如同一个容器可以包含一个或者多个PropertySource(姑且可粗暴的把它理解成一个Collection)

public interface PropertySources extends Iterable<PropertySource<?>> {

	// @since 5.1   注意这个default方法是5.1后才有的  方便遍历和流式操作
	default Stream<PropertySource<?>> stream() {
		return StreamSupport.stream(spliterator(), false);
	}

	// 注意这个name指的是PropertySource的name属性~
	boolean contains(String name);
	// 根据name找到一个PropertySource~~~没找到返回null
	@Nullable
	PropertySource<?> get(String name);

}

Spring仅为我们提供一个实现类:MutablePropertySources

MutablePropertySources

Mutable:可变的 它包含有多个数据源,并且提供对他们操作的方法~

public class MutablePropertySources implements PropertySources {

	// 持有多个PropertySource,并且它是个CopyOnWriteArrayList  放置了并发问题
	private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();

	public MutablePropertySources() {
	}
	// 注意:此处是循环调用的addLast方法~~~~~~~~~~~
	public MutablePropertySources(PropertySources propertySources) {
		this();
		for (PropertySource<?> propertySource : propertySources) {
			addLast(propertySource);
		}
	}

	@Override
	public Iterator<PropertySource<?>> iterator() {
		return this.propertySourceList.iterator();
	}
	@Override
	public Spliterator<PropertySource<?>> spliterator() {
		return Spliterators.spliterator(this.propertySourceList, 0);
	}
	// 复写了父类的Default方法~~~直接使用List的流~
	@Override
	public Stream<PropertySource<?>> stream() {
		return this.propertySourceList.stream();
	}


	// 此处注意:使用的是index,并且使用的是named静态方法~~~  因为这里是根据name来查找
	// 而上面我们说了,关于PropertySource的相等只和name有关而已~
	@Override
	@Nullable
	public PropertySource<?> get(String name) {
		int index = this.propertySourceList.indexOf(PropertySource.named(name));
		return (index != -1 ? this.propertySourceList.get(index) : null);
	}

	// 放在List的顶部=======注意:都先remove了,避免重复出现多个============
	public void addFirst(PropertySource<?> propertySource) {
		removeIfPresent(propertySource);
		this.propertySourceList.add(0, propertySource);
	}
	public void addLast(PropertySource<?> propertySource) {
		removeIfPresent(propertySource);
		this.propertySourceList.add(propertySource);
	}
	// 把propertySource放在指定名字的relativePropertySourceName的前面
	public void addBefore(String relativePropertySourceName, PropertySource<?> propertySource) {
		// 若relativePropertySourceName和propertySource同名,抛出异常~
		assertLegalRelativeAddition(relativePropertySourceName, propertySource);
		removeIfPresent(propertySource);
		// 若relativePropertySourceName里不存在  这里也会抛出异常~
		int index = assertPresentAndGetIndex(relativePropertySourceName);
		
		// 放在指定index的位置~
		addAtIndex(index, propertySource);
	}
	
	public void addAfter(String relativePropertySourceName, PropertySource<?> propertySource) { ... }

	// 获取指定propertySource的优先权,实际就是index角标。
	// 显然角标正数情况下越小越优先。0表示最优先,但是-1表示不存在~~~
	public int precedenceOf(PropertySource<?> propertySource) {
		return this.propertySourceList.indexOf(propertySource);
	}
	
	// 根据名称来移除
	@Nullable
	public PropertySource<?> remove(String name) {
		int index = this.propertySourceList.indexOf(PropertySource.named(name));
		return (index != -1 ? this.propertySourceList.remove(index) : null);
	}

	public void replace(String name, PropertySource<?> propertySource) {
		int index = assertPresentAndGetIndex(name);
		this.propertySourceList.set(index, propertySource);
	}
	public int size() {
		return this.propertySourceList.size();
	}
	@Override
	public String toString() {
		return this.propertySourceList.toString();
	}
	...
}

MutablePropertySources它更像是一个管理器,管理着所有的PropertySource们。然后调用者最终调用getProperty()的时候,就会按照优先级从所有的PropertySource取值,具体逻辑上篇博文有详细讲述,参考PropertySourcesPropertyResolver#getProperty()方法


好了,有这些基础知识后,我们怎么运用和控制呢?也就是所谓的自定义属性配置文件,这在我们开发中使用得还是极多的(特别是SpringBoot~),下面以@PropertySource注解导入自定义属性源文件为例做个介绍

自定义属性源

@PropertySource属性源的加载流程

其实关于@PropertySource的加载,在之前有篇博文里已经有比较详细的分析了: 【小家Spring】Spring中@PropertySource和@ImportResource的区别,以及各自的实现原理解析

为了节约篇幅这里直接从ConfigurationClassParser开始:

class ConfigurationClassParser {
	...
	@Nullable
	protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
			throws IOException {
			//1、解析嵌套内部类
			//2、解析@PropertySource  === 这是下面的内容 ====
		// 相当于拿到所有的PropertySource注解,注意PropertySources属于重复注解的范畴~~~
		for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
				sourceClass.getMetadata(), PropertySources.class,
				org.springframework.context.annotation.PropertySource.class)) {
			
			// 这个判断目前来说是个恒等式~~~  所以的内置实现都是子接口ConfigurableEnvironment的实现类~~~~
			// processPropertySource:这个方法只真正解析这个注解的地方~~~
			if (this.environment instanceof ConfigurableEnvironment) {
				processPropertySource(propertySource);
			} else {
				logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
						"]. Reason: Environment must implement ConfigurableEnvironment");
			}
		}
			//3、解析@ComponentScan
			//4、解析@Import
			//5、解析@ImportResource
			//6、解析@Bean
			//7、解析接口default方法~~~ 也可以用@Bean标注
			//8、解析super class父类
	}

	// 处理每一个属性源,最终加入到环境上下文里面去~
	private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
		// 属性源的name,大多数情况下我们并不指定~
		String name = propertySource.getString("name");
		if (!StringUtils.hasLength(name)) {
			name = null;
		}
		String encoding = propertySource.getString("encoding");
		if (!StringUtils.hasLength(encoding)) {
			encoding = null;
		}
		String[] locations = propertySource.getStringArray("value");
		Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
		boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");

		// 此处注意:若我们都没有指定Factory的话,就会使用Spring默认的工厂,最终都是生成一个ResourcePropertySource(是个PropertiesPropertySource~~)
		// 所以它默认是只能处理Properties文件的(当然指定的格式的xml也是可以的),yaml是不能被支持的~~~~~~~~~~~
		Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
		PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
				DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));


		for (String location : locations) {
			try {
				String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
				// 处理好占位符后,拿到这个资源~~~~
				Resource resource = this.resourceLoader.getResource(resolvedLocation);

				// 重点就在这个方法里~~~把这个属性源添加进来~~~
				addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
			}
		}
	}


	private void addPropertySource(PropertySource<?> propertySource) {
		String name = propertySource.getName();
		// 从环境里把MutablePropertySources拿出来,准备向里面添加~~~~
		MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();

		// 这里有个暖心的处理:若出现同名的配置文件,它会两个都保存着,联合形成一个CompositePropertySource  这样它哥俩就都会生效了
		// 否则MutablePropertySources 的Map里面的name是不能同名的,我觉得这个做法还是很暖心的~~~
		// 我觉得这个操作虽然小,但是足见Spring的小暖心~
		if (this.propertySourceNames.contains(name)) {
			// We've already added a version, we need to extend it
			PropertySource<?> existing = propertySources.get(name);
			if (existing != null) {
				PropertySource<?> newSource = (propertySource instanceof ResourcePropertySource ?
						((ResourcePropertySource) propertySource).withResourceName() : propertySource);
				if (existing instanceof CompositePropertySource) {
					((CompositePropertySource) existing).addFirstPropertySource(newSource);
				}
				else {
					if (existing instanceof ResourcePropertySource) {
						existing = ((ResourcePropertySource) existing).withResourceName();
					}
					CompositePropertySource composite = new CompositePropertySource(name);
					// 小细节:后添加的反而在最上面的~~~ 已经存在会被挤下来一个位置~
					composite.addPropertySource(newSource);
					composite.addPropertySource(existing);
	
					// 把已经存在的这个name替换成composite组合的~~~
					propertySources.replace(name, composite);
				}
				return;
			}
		}

		// 重要:手动导入进来的propertySource是放在最后面的(优先级最低)
		
		// 这段代码处理的意思是:若你是自己导入进来的第一个,那就放在最末尾
		// 若你不是第一个,那就把你放在已经导入过的最后一个的前一个里面~~~
		if (this.propertySourceNames.isEmpty()) {
			propertySources.addLast(propertySource);
		} else {
			String firstProcessed = this.propertySourceNames.get(this.propertySourceNames.size() - 1);
			propertySources.addBefore(firstProcessed, propertySource);
		}
		this.propertySourceNames.add(name);
	}
}

从中可以看出一些小细节: 1、@PropertySource被解析的时机还是非常早的(次于内部类) 2、它允许同名的PropertySource存在,并且两个最终都会添加进来不会覆盖 3、通过注解@PropertySource导入进来的属性源的优先级是最低的~~~ 4、location是支持占位符的,但是perperties文件里面其实也是支持占位符的(文件内的${xxx}这种占位符依旧可以用来引用本文件的内容、环境变量内容等等。它的解析实际是在给java属性赋值时~)

总结

我把这篇文章定位为SpringBoot自动化处理属性配置文件的先行文章,因为传统的Spring并不对自动处理,而都是我们手动去导入、添加属性配置文件~

但是我相信,在了解了这块内容后再去看SpringBoot的自动化方案实现,简直小巫见大巫,一种居高临下的赶脚。这又印证了我常说的那句话:你对Spring Framework有多了解决定了你对SpringBoot有多了解,你对SpringBoot有多了解决定了你对SpringCloud有多了解~

学无止境,keep moving

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java技术栈

Java 12 骚操作, String居然还能这样玩!

栈长之前在Java技术栈微信公众号分享过《Java 11 已发布,String 还能这样玩!》这篇文章,介绍了 Java 11 的 String 新玩法,让大家...

10330
来自专栏女程序员的日常_Lin

Iterator 、Generator(一)

调用Generator函数,返回一个遍历器对象,代表Generator函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和don...

7930
来自专栏AI科技大本营的专栏

如何用Python编写一个Lisp解释器

这篇文章有两个目的:一是展示如何实现一个计算机语言的解释器,二是演示如何使用 Python 3 构造 Lisp 的一种方言 Schema,作者把自己的这个语言解...

13940
来自专栏Java后端技术栈cwnait

再也不敢使用集合默认初始化值了

集合初始化通常进行分配容量、设定特定参数等相关工作。我们以使用频率相对较高的ArrayList和HashMap为例,简要说明初始化的相关工作,并解释为什么在任何...

11530
来自专栏技术从心

JAVAAPI中SortedMap解释

A Map进一步提供其键上的总排序 。地图根据其键的natural ordering或通过在分类地图创建时提供的Comparator进行排序。当迭代排序的地图的...

15720
来自专栏Java研发军团

Java性能优化的50个细节,我必须分享给你!

来源:blog.csdn.net/dongnan591172113/article/details/51790428

7520
来自专栏京程一灯

怎样成为全栈开发工程师[每日前端夜话0xAA]

在 LinkedIn 和 Facebook 上,有很多人将当前的工作标记为全栈工开发程师。在 Medium 上关于这个问题的文章也收到了很多读者的好评。一些人认...

7330
来自专栏Super 前端

JavaScript的工作原理:引擎,运行时和调用堆栈的概述

随着 JavaScript 变得越来越流行,各团队正在多个领域栈中使用它们,其中包括 — 前端,后端,混合应用,嵌入式等等。

8420
来自专栏大数据实战演练

Elasticsearch BulkProcessor 的具体实现

本文示例使用的是 Spring Boot 框架,由于该框架有默认的 Elasticsearch 版本,为了避免版本混乱或冲突,我在 pom.xml 文件内添加了...

78630
来自专栏美团技术团队

美团大规模微服务通信框架及治理体系OCTO核心组件开源

微服务通信框架及治理平台OCTO作为美团基础架构设施的重要组成部分,目前已广泛应用于公司技术线,稳定承载上万应用、日均支撑千亿级的调用。业务基于OCTO提供的标...

15110

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励