说说Spring中PropertySource属性源配置文件的加载流程
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
这个抽象类告诉我们,PropertySource的name非常的重要。接下来重点就是它的实现们,它的继承树如下:
JndiPropertySource
显然它和Jndi有关。JNDI:Java Naming and Directory Interface Java命名和目录接口。
// @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获取属性值的原理:属性源最终都被加入进Environment持有的属性:MutablePropertySources保存着。所以,我们使用@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>> optionArgs和List<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是一个解析命令行选项参数的第三方库。它能够自定义格式、从文件中解析等高级操作。
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取值。
下面以@PropertySource注解导入自定义属性源文件为例做个介绍。
@PropertySource属性源的加载流程
为了节约篇幅,这里直接从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);
}
}
从中可以看出一些小细节:
- @PropertySource被解析的时机还是非常早的(次于内部类)
- 它允许同名的PropertySource存在,并且两个最终都会添加进来不会覆盖
- 通过注解@PropertySource导入进来的属性源的优先级是最低的~~~
- location是支持占位符的,但是properties件里面其实也是支持占位符的(文件内的${xxx}这种占位符依旧可以用来引用本文件的内容、环境变量内容等等。它的解析实际是在给java属性赋值时~)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· Qt个人项目总结 —— MySQL数据库查询与断言