【Spring】Spring原理源码解析(五)-- Spring识别jvm、环境配置及通过@PropertySources加入配置原理

本文深入解析Spring如何识别JVM参数、环境变量以及通过@PropertySources加载配置文件的原理。通过实例分析,展示了配置优先级:JVM参数 > 环境变量 > @PropertySources。详细探讨了Spring初始化Environment的过程,以及配置文件的读取和解析过程。

系列文章主旨

从如何把自己的Bean注册到Spring容器开始,从该点出发,每篇只关注一个核心流程的原理、源码,再由该篇带出引申出来的其他Spring知识点,继续剖析,最终达到理解Spring核心原理的目的;

上篇内容

分析了spring如何通过内置的,bean工厂后置处理器,解析配置类,识别配置类注解@Bean后,存储在config属性上,后续解析为BD,最后注册到spring容器中;
详情:【Spring】Spring原理源码解析(四)-- Spring在解析配置类时,怎么识别并处理@Bean的

本篇内容

1、补充spring容器在初始化Environment环境组件时,是怎么识别我们jvm、系统运行环境配置的原理源码;
2、从上一篇分析的spring内部实现的ConfigurationClassPostProcessor继续入手,分析spring在解析配置类的时候,又是怎么识别@PropertySources,加入自定义配置到spring环境的原理源码;

文章图片均出自:
Spring识别jvm、环境配置及通过@PropertySources加入配置原理

核心原理

代码示例

配置文件

两个,分别是
test1.yml,内容为 > test: 1;
test2.yml,内容为 > test: 2;

test: 1
test:2
@PropertySource配置类

作用为:
读取(占位符)${activeEnv}.yml文件
然后把配置文件中的test设置到test属性中

@Data
@Configuration
@PropertySource(value = {"classpath:/${activeEnv}.yml"})
public class PropertySourceCls {
    @Value(value = "${test}")
    private String test;
}
JVM参数配置

activeEnv占位符配置为:test2
test:4

-DactiveEnv=test2 -Dtest=4
环境变量配置

test=3

test=3
启动spring容器,获取test属性,得到读取配置的默认优先级

通过变换配置打印test属性,总结默认优先级为:
JVM参数 > 环境变量 > @PropertySource加入配置

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PropertySourceCls.class);
PropertySourceCls propertySourceCls = context.getBean(PropertySourceCls.class);
System.out.println(propertySourceCls.getTest());

打印结果:

JVM配置: -DactiveEnv=test2,-Dtest=4
环境配置:test=3
配置文件:test1.yml,内容 test=1;test2.yml,内容test=2
打印 -> 4

JVM配置: -DactiveEnv=test2
环境配置:test=3
配置文件:test1.yml(内容test=1);test2.yml(内容test=2)
打印 -> 3

JVM配置: -DactiveEnv=test2
环境配置:
配置文件:test1.yml(内容test=1) ;test2.yml(内容test=2)
打印 -> 2

JVM配置: -DactiveEnv=test1
环境配置:
配置文件:test1.yml(内容test=1) ;test2.yml(内容test=2)
打印 -> 1

Spring识别jvm、环境配置

图解

在这里插入图片描述

核心处理逻辑

spring容器在进行初始化时,会初始化环境,对应的是Enviroment组件;该组件有属性propertySourceList,用来记录环境变量;

然后通过java自带的System获取得到jvm以及系统环境参数,解析后加入到propertySourceList存储;

进阶源码解析
实例化环境入口

跟踪到实例化容器,先会实例化reader,而在reader初始化时,就会顺便初始化环境Enviroment;

/**
* org.springframework.context.annotation.AnnotationConfigApplicationContext#AnnotationConfigApplicationContext()
*/
public AnnotationConfigApplicationContext(DefaultListableBeanFactory beanFactory) {
	this.reader = new AnnotatedBeanDefinitionReader(this);
	this.scanner = new ClassPathBeanDefinitionScanner(this);
}

getOrCreateEnvironment()方法就是初始化环境的入口

public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry) {
	this(registry, 
		// 创建环境的核心入口
		getOrCreateEnvironment(registry));
}

继续跟进

private static Environment getOrCreateEnvironment(BeanDefinitionRegistry registry) {
	Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
	if (registry instanceof EnvironmentCapable) {
		return ((EnvironmentCapable) registry).getEnvironment();
	}
	// 默认为基本环境
	return new StandardEnvironment();
}

StandardEnvironment是AbstractEnvironment的子类,在实例化StandardEnvironment时,就会走到父类构造方法,会发现其实调用到父类AbstractEnvironment的构造方法;
在父类的构造方法中,会把存储配置集合属性propertySources存入到方法customizePropertySources(),完成实例化;

/**
* 存储配置集合的属性
*/
private final MutablePropertySources propertySources = new MutablePropertySources();

// ...忽略非相关

public AbstractEnvironment() {
	// 实际又会调用到子类的customizePropertySources()方法
	customizePropertySources(this.propertySources);
}
MutablePropertySources可变属性对象略览

配置集合对象MutablePropertySources,实际上就是封装了可变的集合;

public class MutablePropertySources implements PropertySources {

	private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();

	// .. 维护属性的一些方法,addLast加入到集合最后,等等...
}
读取jvm、系统环境参数,维护到环境的属性中

这里就会发现,先读取jvm参数,然后加入到propertySources中;
接着读取系统环境变量,也加入到propertySources中;
这里很重要的可以发现,他们是有顺序加入的,因此可以从结果预测,同一个配置名,会从jvm优先获取,再到系统环境变量;

/**
* org.springframework.core.env.StandardEnvironment#customizePropertySources
*/
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
	propertySources.addLast(new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
	propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
获取jvm参数

通过java的System获取到解析好的jvm参数

@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public Map<String, Object> getSystemProperties() {
	try {
		// 通过java的System获取到jvm参数
		return (Map) System.getProperties();
	}
	catch (AccessControlException ex) {
		// 异常处理..
	}
}
获取系统环境参数

通过java的System获取到解析好的系统环境参数

@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public Map<String, Object> getSystemEnvironment() {
	if (suppressGetenvAccess()) {
		return Collections.emptyMap();
	}
	try {
		// 通过java的System获取到系统环境参数
		return (Map) System.getenv();
	}
	catch (AccessControlException ex) {
		// 异常处理..
	}
}

@PropertySources识别配置文件并维护到环境

图解

在这里插入图片描述

核心处理逻辑

同样是在解析配置类时,org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass,会解析@PropertySources/@PropertySource注解;

然后解析注解内容,得到通过配置的资源文件字符串,解析占位符,得到真正的位置,然后通过文件位置构建成PropertySource对象后,加入到环境的propertySources集合、sourceNames中;

后续获取属性值时,就可以通过这种方式获取到;

进阶源码解析
入口

老地方,解析配置类时,就可以看到解析@PropertySources/@PropertySource注解的方法;

@Nullable
protected final SourceClass doProcessConfigurationClass(
		ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
		throws IOException {

	// 忽略非相关..

	// Process any @PropertySource annotations
	for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
			sourceClass.getMetadata(), PropertySources.class,
			org.springframework.context.annotation.PropertySource.class)) {
		if (this.environment instanceof ConfigurableEnvironment) {
			processPropertySource(propertySource);
		}
		else {
			logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
					"]. Reason: Environment must implement ConfigurableEnvironment");
		}
	}

	// 忽略非相关..
}

继续跟进,处理单个注解
1、先是得到注解上的配置值,名称、解析资源文件的编码、资源文件路径
2、然后先是解析资源文件路径,把其中的占位符替换掉;
3、然后通过解析后的文件路径,读取得到资源文件;
4、最后通过把资源文件封装为PropertySource,加入到环境的propertySources集合中;

/**
* org.springframework.context.annotation.ConfigurationClassParser#processPropertySource
*/
private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
    // 获取注解的属性值,名称、编码、特别是拿到资源的位置 - value
	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");

	// 获得PropertySourceFactory工厂,后面就可以通过工厂来创建PropertySource
	// 然后加入到环境的propertySourceList配置集合中
	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);
			// 通过资源文件,工厂,创建PropertySource,然后加入到环境的propertySources配置集合中
			addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
		}
		catch (IllegalArgumentException | FileNotFoundException | UnknownHostException | SocketException ex) {
			// 异常捕获处理...
		}
	}
}

通过环境即可匹配读取到配置

示例
@Autowired
private Environment environment;

String propertyVal = environment.getProperty(propertyConfig);
图解

在这里插入图片描述

核心处理逻辑

实际上,就是通过propertySources集合,找到第一个名字相应的值,并且替换掉它的占位符后,返回出去;

进阶源码解析
入口

环境在初始化时,属性propertyResolver是会初始化为PropertySourcesPropertyResolver;
并且会传入环境的propertySources集合进去,因此我们加入进去的配置集会当做引用传入进去了;
getProperty()方法实际上是会调用PropertySourcesPropertyResolver的getProperty()


private final MutablePropertySources propertySources = new MutablePropertySources();

private final ConfigurablePropertyResolver propertyResolver = new PropertySourcesPropertyResolver(this.propertySources);

/**
* org.springframework.core.env.AbstractEnvironment#getProperty(java.lang.String)
*/
@Override
@Nullable
public String getProperty(String key) {
	return this.propertyResolver.getProperty(key);
}

继续跟进,org.springframework.core.env.PropertySourcesPropertyResolver#getProperty(java.lang.String)

/**
* org.springframework.core.env.PropertySourcesPropertyResolver#getProperty(java.lang.String)
*/
@Override
@Nullable
public String getProperty(String key) {
	return getProperty(key, String.class, true);
}

通过propertySources获取属性,拿到第一个并且替换掉占位符之后就返回出去

@Nullable
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
	if (this.propertySources != null) {
		// 遍历propertySources集合
		for (PropertySource<?> propertySource : this.propertySources) {
			// 日志相关...
			
			// 获取到对应的属性
			Object value = propertySource.getProperty(key);
			if (value != null) {
				if (resolveNestedPlaceholders && value instanceof String) {
					// 这里如果有对应的占位符,也会被替换掉
					value = resolveNestedPlaceholders((String) value);
				}
				logKeyFound(key, propertySource, value);
				return convertValueIfNecessary(value, targetValueType);
			}
		}
	}
	
	// 日志相关...
	
	return null;
}

总结

本篇主要介绍了spring如何通过内置的bean工厂后置处理器,解析配置类,识别配置类注解@PropertySources/@PropertySource;

实际上就是通过在环境enviroment中,先会存储封装好的资源对象集合propertySources,然后当从enviroment获取时,实际上就会遍历这个集合,拿到第一个匹配上key的属性配置,然后去解析它所有的占位符后,得到属性值返回;

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值