Environment是Spring Framework中提供的一个接口,旨在为属性获取提供一个统一的接口。不管是在Spring Framework,还是在Spring Boot,甚至在Spring Cloud中,很多地方都能见到该接口及其实现类,所以熟悉该接口的内部实现还是很有必要的。
属性源PropertySource
既然Environment是属性获取的抽象,那么必然需要知道从哪里获取属性,PropertySource类是属性属性源的抽象实现,所以先来介绍该类。
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;
}
@SuppressWarnings("unchecked")
public PropertySource(String name) {
// 默认以一个空的对象作为属性源
this(name, (T) new Object());
}
source这个字段就是属性源目标对象,属性获取就是基于该对象。
MutablePropertySources
在AbstractEnvironment的默认实现中,使用的是MutablePropertySources这个特殊的属性源。
// 属性源集合
private final MutablePropertySources propertySources;
// 属性解析器
private final ConfigurablePropertyResolver propertyResolver;
public AbstractEnvironment() {
// 创建一个默认的属性源集合对象
this(new MutablePropertySources());
}
// 这里也可以看出environment中是通过MutablePropertySources来封装了所有的属性源
protected AbstractEnvironment(MutablePropertySources propertySources) {
this.propertySources = propertySources;
// 创建属性解析器
this.propertyResolver = createPropertyResolver(propertySources);
// 自定义属性源集合,比如往里面添加属性源
customizePropertySources(propertySources);
}
之所以说特殊,是因为该类是对其他多个属性源的封装,其本身不封装真正的属性。
// 用于保存Environment中的所有属性源
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
public MutablePropertySources() {
}
public MutablePropertySources(PropertySources propertySources) {
this();
for (PropertySource<?> propertySource : propertySources) {
addLast(propertySource);
}
}
@Override
public Iterator<PropertySource<?>> iterator() {
// 直接返回集合列表的迭代器
return this.propertySourceList.iterator();
}
既然是对多个属性源的封装,自然就包含了管理属性源的方法,在Spring Boot启动流程一文中已经看到过太多次了,比如添加属性源、删除属性源,这类方法比较多,就不一一介绍了。 在Spring Boot中会使用到很多类型的属性源,都是通过MutablePropertySources来管理的。
自定义属性源
在Environment的直接子类AbstractEnvironment中,定义了扩展属性源的模板方法。在子类StandardEnvironment类中扩展了两个属性源。
/** System environment property source name: {@value}. */
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
/** JVM system properties property source name: {@value}. */
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
/*
* 添加两个属性源:
* * PropertiesPropertySource保存所有通过System.getProperties()获取的属性;
* * SystemEnvironmentPropertySource保存所有通过System.getenv()获取的属性;
*/
// 系统属性
propertySources.addLast(
new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
// 环境变量
propertySources.addLast(
new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public Map<String, Object> getSystemProperties() {
try {
// 获取系统属性
return (Map) System.getProperties();
}
}
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public Map<String, Object> getSystemEnvironment() {
if (suppressGetenvAccess()) {
return Collections.emptyMap();
}
try {
// 获取环境变量
return (Map) System.getenv();
}
}
这里分别往MutablePropertySources中添加了系统属性和系统环境变量这两个属性源,所以在Spring框架中可以获取到系统属性和环境变量信息。
属性解析器PropertyResolver
PropertyResolver是Environment的直接父接口,用于提供属性获取的功能。Spring是支持在属性中使用占位符的,那么在获取属性的时候,肯定需要进行解析。
PropertySource和PropertyResolver两者都提供了获取属性的功能,那么两者有什么区别? 一般Spring在获取属性的时候,是直接从Environment来获取的,也就是PropertyResolver的这套接口。而PropertySource定义的属性获取接口是会被PropertyResolver来调用的,所以两者是这样一种关系。
在AbstractEnvironment的构造方法中,会创建属性解析器。
// 这里也可以看出environment中是通过MutablePropertySources来封装了所有的属性源
protected AbstractEnvironment(MutablePropertySources propertySources) {
this.propertySources = propertySources;
// 创建属性解析器
this.propertyResolver = createPropertyResolver(propertySources);
// 自定义属性源集合,比如往里面添加属性源
customizePropertySources(propertySources);
}
protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) {
// 根据属性源创建属性解析器
return new PropertySourcesPropertyResolver(propertySources);
}
可以看到,这里创建的是PropertySourcesPropertyResolver类型的对象。
AbstractEnvironment中还定义了多个操作属性的门面方法,无一例外都是通过PropertyResolver来完成的。
@Override
public boolean containsProperty(String key) {
return this.propertyResolver.containsProperty(key);
}
// 下面是6个获取属性的方法
@Override
@Nullable
public String getProperty(String key) {
return this.propertyResolver.getProperty(key);
}
@Override
public String getProperty(String key, String defaultValue) {
return this.propertyResolver.getProperty(key, defaultValue);
}
@Override
@Nullable
public <T> T getProperty(String key, Class<T> targetType) {
return this.propertyResolver.getProperty(key, targetType);
}
@Override
public <T> T getProperty(String key, Class<T> targetType, T defaultValue) {
return this.propertyResolver.getProperty(key, targetType, defaultValue);
}
@Override
public String getRequiredProperty(String key) throws IllegalStateException {
return this.propertyResolver.getRequiredProperty(key);
}
@Override
public <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException {
return this.propertyResolver.getRequiredProperty(key, targetType);
}
// 下面是两个解析占位符的方法
@Override
public String resolvePlaceholders(String text) {
return this.propertyResolver.resolvePlaceholders(text);
}
@Override
public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
return this.propertyResolver.resolveRequiredPlaceholders(text);
}
下面以属性获取为切入口,看看属性解析器是怎么工作的。
获取属性
核心实现在PropertySourcesPropertyResolver的getProperty方法中。
@Nullable
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
if (this.propertySources != null) {
// 遍历每个属性源
for (PropertySource<?> propertySource : this.propertySources) {
if (logger.isTraceEnabled()) {
logger.trace("Searching for key '" + key + "' in PropertySource '" +
propertySource.getName() + "'");
}
// 尝试获取属性
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);
}
}
}
if (logger.isTraceEnabled()) {
logger.trace("Could not find key '" + key + "' in any property source");
}
return null;
}
这里在遍历每个属性源,依次从中获取属性,然后解析其内部可能存在的占位符,并对值转换到目标类型。这些功能都在其父类AbstractPropertyResolver中实现的。
属性占位符解析
protected String resolveNestedPlaceholders(String value) {
if (value.isEmpty()) {
return value;
}
/*
* ignoreUnresolvableNestedPlaceholders表示是否忽略嵌套的占位符,默认值是false。
* 不管要不要忽略,最终都会调用到doResolvePlaceholders方法,只不过helper参数不一样而已。
*/
return (this.ignoreUnresolvableNestedPlaceholders ?
// 忽略嵌套的占位符
resolvePlaceholders(value) :
// 不忽略嵌套的占位符
resolveRequiredPlaceholders(value));
}
@Override
public String resolvePlaceholders(String text) {
if (this.nonStrictHelper == null) {
// 创建helper
this.nonStrictHelper = createPlaceholderHelper(true);
}
// 解析占位符
return doResolvePlaceholders(text, this.nonStrictHelper);
}
@Override
public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
if (this.strictHelper == null) {
// 创建helper
this.strictHelper = createPlaceholderHelper(false);
}
// 解析占位符
return doResolvePlaceholders(text, this.strictHelper);
}
在上面第二和第三个方法中,其实逻辑都差不多,只是创建helper对象的参数不一样而已,下面来看看helper对象是怎么创建的。
/*
* 下面这三个属性的默认值分别是: '${'、 '}'、 ':'
*/
private String placeholderPrefix = SystemPropertyUtils.PLACEHOLDER_PREFIX;
private String placeholderSuffix = SystemPropertyUtils.PLACEHOLDER_SUFFIX;
@Nullable
private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR;
private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {
// 创建属性占位符helper,用于帮助解析属性
return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix,
this.valueSeparator, ignoreUnresolvablePlaceholders);
}
/** Prefix for system property placeholders: "${". */
public static final String PLACEHOLDER_PREFIX = "${";
/** Suffix for system property placeholders: "}". */
public static final String PLACEHOLDER_SUFFIX = "}";
/** Value separator for system property placeholders: ":". */
public static final String VALUE_SEPARATOR = ":";
看到SystemPropertyUtils中的三个属性,是不是非常熟悉。一般我们在Spring使用属性的时候都是通过:${name: defaultValue}的格式。
在进入属性解析之前,先理解好helper对象的创建过程和内部的一些属性。
private static final Map<String, String> wellKnownSimplePrefixes = new HashMap<>(4);
static {
wellKnownSimplePrefixes.put("}", "{");
wellKnownSimplePrefixes.put("]", "[");
wellKnownSimplePrefixes.put(")", "(");
}
private final String placeholderPrefix;
private final String placeholderSuffix;
private final String simplePrefix;
// 占位符名称和属性默认值之间的分隔符
@Nullable
private final String valueSeparator;
// 是否忽略不能够解析的占位符
private final boolean ignoreUnresolvablePlaceholders;
public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix) {
this(placeholderPrefix, placeholderSuffix, null, true);
}
public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix,
@Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders) {
Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null");
Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null");
this.placeholderPrefix = placeholderPrefix;
this.placeholderSuffix = placeholderSuffix;
// 默认的占位符后缀是'}',所以这里获取到的是:'{'
String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.placeholderSuffix);
/*
* 默认的占位符前缀是'${',所以这里的endsWith是满足的。
* 另外在下面的findPlaceholderEndIndex函数中,使用simplePrefix确实可以比使用整个前缀要提高性能。
*
* 这里不用担心前缀中没有使用预配置好的后缀对应的简化前缀,因为这里使用了endsWith来判断了。
*/
if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) {
// 所以这里的simplePrefix是'{'
this.simplePrefix = simplePrefixForSuffix;
}
else {
this.simplePrefix = this.placeholderPrefix;
}
this.valueSeparator = valueSeparator;
this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
}
接下来两个方法都会调用到相同的方法:
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
// 通过helper解析占位符,第二个值是用来获取占位符的值的函数
return helper.replacePlaceholders(text, this::getPropertyAsRawString);
}
下面进入helper对象,来看看到底是怎么解析属性的。
public String replacePlaceholders(String value, final Properties properties) {
Assert.notNull(properties, "'properties' must not be null");
return replacePlaceholders(value, properties::getProperty);
}
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "'value' must not be null");
// 解析属性
return parseStringValue(value, placeholderResolver, null);
}
protected String parseStringValue(
String value, PlaceholderResolver placeholderResolver, @Nullable Set<String> visitedPlaceholders) {
// 查找占位符前缀的索引
int startIndex = value.indexOf(this.placeholderPrefix);
if (startIndex == -1) { // 不存在占位符
return value;
}
StringBuilder result = new StringBuilder(value);
while (startIndex != -1) {
// 查找占位符的结束索引
int endIndex = findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
// 获取占位符(不包含前后缀)
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (visitedPlaceholders == null) {
visitedPlaceholders = new HashSet<>(4);
}
/*
* 判断属性定义中是否存在环形引用,比如:
* key: value
* aaa: ${bbb}
* bbb: ${aaa}
* 这个例子中就出现了属性间的环形依赖,Spring会在这里直接报错。
* 解析路径:bbb -> ${aaa} -> aaa -> ${bbb} -> bbb,
* 这样在第5次(递归)执行该方法时,visitedPlaceholders中已经存在bbb,所以这里添加会失败。
*/
if (!visitedPlaceholders.add(originalPlaceholder)) {
throw new IllegalArgumentException(
"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
}
// Recursive invocation, parsing placeholders contained in the placeholder key.
// 递归解析内嵌的占位符
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
// Now obtain the value for the fully resolved key...
// 解析占位符的值,执行到这里,placeholder是纯粹的属性名称,不包含任何属性占位符标记
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
// 获取到值,如果值分割标记不为null,则处理默认值,例如:${port: 8080},port是属性名称,8080是该属性的默认值
if (propVal == null && this.valueSeparator != null) {
// 获取值分割标记的下标
int separatorIndex = placeholder.indexOf(this.valueSeparator);
// 存在默认值
if (separatorIndex != -1) {
// 解析实际的属性名称
String actualPlaceholder = placeholder.substring(0, separatorIndex);
// 解析默认值
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
/*
* 如果是存在默认值的情况,那么上面resolvePlaceholder是获取不到的值,毕竟不会设置一个叫作'port: 8080'的属性,
* 所以这里去掉默认值后,以纯粹的属性名称来解析占位符的值。
*/
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
// 如果目标属性还是不存在,则采用默认值
if (propVal == null) {
propVal = defaultValue;
}
}
}
if (propVal != null) {
// Recursive invocation, parsing placeholders contained in the
// previously resolved placeholder value.
// 如果属性值中仍然包含占位符,那么继续递归解析
propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
// 将整个属性占位符替换为实际的属性值
result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
if (logger.isTraceEnabled()) {
logger.trace("Resolved placeholder '" + placeholder + "'");
}
/*
* 从替换后的位置开始往后查找第一个占位符前缀的下标,这里确保了在调用findPlaceholderEndIndex函数之前时,
* buf参数中是包含占位符前缀的。
* 这样就可以避免${outer-${inner}-12}3}(后缀数量大于前缀数量的情况)情况的出现,
* 那么在findPlaceholderEndIndex函数中就只需要考虑前缀数量是否大于后缀数量的情况。
*/
startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
}
else if (this.ignoreUnresolvablePlaceholders) { // 忽略不能够解析的占位符
// Proceed with unprocessed value.
// 和上一个if分支中的最后一条语句一样
startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
}
else { // 不能忽略不能够解析的占位符
throw new IllegalArgumentException("Could not resolve placeholder '" +
placeholder + "'" + " in value \"" + value + "\"");
}
visitedPlaceholders.remove(originalPlaceholder);
}
else {
// 停止解析,结束循环
startIndex = -1;
}
}
return result.toString();
}
/*
* 该函数不仅解析了占位符后缀的索引,而且还判断了内嵌的占位符是否合法(也就是判断前缀和后缀的数量是否相等)。
* 注意在该函数中没有考虑后缀数量大于前缀数量的情况,比如${outer-${inner}-12}3},第二个内嵌占位符后缀没有对应的前缀,
* 但该函数是会正常返回后缀下标的,但由于在上面parseStringValue方法中是确保了buf中包含占位符前缀才会调用该方法,所以对于这个例子,
* 解析第二个后缀占位符时是根本不会进入到该方法中的。
*/
private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
// 从占位符前缀的下一个字符开始遍历
int index = startIndex + this.placeholderPrefix.length();
// 如果占位符是合法的,也就是前缀和后缀数量相等,那么解析到最后,该变量也应该是0
int withinNestedPlaceholder = 0;
// 往后一个个字符地匹配
while (index < buf.length()) {
// 匹配上占位符后缀
if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
// 后缀数量不等于前缀数量
if (withinNestedPlaceholder > 0) {
// 利用当前解析到的后缀抵消掉一个前缀
withinNestedPlaceholder--;
// 跳过当前解析到的后缀,以便继续往后遍历
index = index + this.placeholderSuffix.length();
}
else { // 正常情况:即当前后缀是和之前解析到的前缀是一一对应的
return index;
}
}
/*
* 解析到了前缀
* 为什么这里用simplePrefix(默认值为'{'),而不是placeholderPrefix(默认值为'${')?
* 仅仅是在匹配时只用匹配一个字符(而不是两个)来提高性能?(大概率是了)
* 因为前后缀都是可以自定义的,比如定义一个复杂的前缀,例如: '$$$$$$$$$$${',使用'{'来简化匹配确实可以提高性能。
*/
else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
// 标记匹配上了一个前缀
withinNestedPlaceholder++;
// 跳过前缀,以便继续往后遍历
index = index + this.simplePrefix.length();
}
// 跳到下一个字符进行匹配
else {
index++;
}
}
return -1;
}
上面的parseStringValue方法是属性解析的核心实现,代码较长,但注释中都已给出详细说明,所以不再赘述。
属性类型转换
获取到属性后,可能不能直接将其赋值给目标对象,所以还要对属性的类型进行转换一下。
@SuppressWarnings("unchecked")
@Nullable
protected <T> T convertValueIfNecessary(Object value, @Nullable Class<T> targetType) {
if (targetType == null) {
return (T) value;
}
ConversionService conversionServiceToUse = this.conversionService;
// 如果没有设置转换服务
if (conversionServiceToUse == null) {
// Avoid initialization of shared DefaultConversionService if
// no standard type conversion is needed in the first place...
// 如果是可以直接赋值的,则不用转换,直接返回
if (ClassUtils.isAssignableValue(targetType, value)) {
return (T) value;
}
// 则使用默认的转换服务
conversionServiceToUse = DefaultConversionService.getSharedInstance();
}
// 利用转换服务将值转换为目标类型
return conversionServiceToUse.convert(value, targetType);
}
可以看到,这里是通过ConversionService来进行类型转换的,如果没有设置该属性的话会使用默认的DefaultConversionService类型的实例。 如果要使用自定义的转换服务,应该怎么设置呢?实际上在AbstractEnvironment中定义了相关接口。
@Override
public void setConversionService(ConfigurableConversionService conversionService) {
// 为属性解析器设置转换服务
this.propertyResolver.setConversionService(conversionService);
}
@Nullable
private volatile ConfigurableConversionService conversionService;
@Override
public void setConversionService(ConfigurableConversionService conversionService) {
Assert.notNull(conversionService, "ConversionService must not be null");
// 设置转换服务,多次设置会覆盖之前的
this.conversionService = conversionService;
}
类似地,AbstractPropertyResolver中定义的其他属性,AbstractEnvironment也提供了相应的方法来设置,这里不再赘述。
总结
本文分析了Spring中的Environment的实现,其实该接口及其实现类只是一个门面类,主要的属性获取还是要通过PropertyResolver和PropertySource来实现。另外本文以获取属性为例分析了属性解析器的属性获取过程及占位符解析过程。