Spring Boot @ConfigurationProperties(原理三)

用法:https://blog.csdn.net/qq_32868023/article/details/123390627
原理一:https://blog.csdn.net/qq_32868023/article/details/123515652
原理二:https://blog.csdn.net/qq_32868023/article/details/123603241
原理三:https://blog.csdn.net/qq_32868023/article/details/123604439
原理四:https://blog.csdn.net/qq_32868023/article/details/123606236
原理五:https://blog.csdn.net/qq_32868023/article/details/123616110

Binder负责对一个Bindable进行绑定,一个Bindable的属性可能是一个java对象、数组、集合、Map等各种情况,需要递归的去绑定java对象的属性,数组、集合、Map的元素,其流程简单来说就是:从配置属性源中获取到对应的值,将这个值转化为所需要的类型。本节主要分析下属性绑定过程中配置值获取的过程,主要涉及ConfigurationPropertyName、ConfigurationProperty、ConfigurationPropertySource、值获取的过程

1、ConfigurationPropertyName

ConfigurationPropertyName代表的是配置属性的名称,在Binder里的作用可以简单描述为:为Bindable的每个属性构造一个ConfigurationPropertyName,然后为.properties里的kv的每个key生成一个ConfigurationPropertyName,如果这俩equals,就把.properties里的值绑定到Bindable的这个属性上。所以ConfigurationPropertyName就有三个重要功能:

  • 将一串字符串构造成ConfigurationPropertyName
  • 判断不同ConfigurationPropertyName实例是否equal
  • toString

ConfigurationPropertyName这个类一千多行慢慢分析

a、Elements

ConfigurationPropertyName最重要的属性是elements,Elements封装了配置属性名称的层级化结构,看下Elements的数据结构。

	private static class Elements {

		private final CharSequence source;

		private final int size;

		private final int[] start;

		private final int[] end;

		private final ElementType[] type;
    }

Elements使用数组保存配置属性的每个element的开始index、结束index、类型(是否有横杠、是否有[]等),size保存有多少个element、source保存原始属性名称字符串。例如aa$$.b-_-B.c[1]这样一个属性,会被分割成4个element,Elements结构如下
image.png
ElementType表示每个element的类型,是一个枚举,indexed属性表示是否数组或map下标类型

  • EMPTY:空的
  • UNIFORM:只有a-z、0-9,没有’-’,只有小写
  • DASHED:跟UNIFORM一样,只是包含’-’
  • NON_UNIFORM:可能存在大写,或者有特殊字符
  • INDEXED:存在非数字下标
  • NUMERICALLY_INDEXED:存在数字下标
	private enum ElementType {
		EMPTY(false),
		UNIFORM(false),
		DASHED(false),
		NON_UNIFORM(false),
		INDEXED(true),
		NUMERICALLY_INDEXED(true);

		private final boolean indexed;
    }

ElementsParser负责将一串如aa$$.b-_-B.c[1]这样的字符串解析成Elements,逻辑比较复杂,不必深入去理解
ConfigurationPropertyName提供的第一个重要功能就是将字符串转换为ConfigurationPropertyName,有adapt、of、append三类,分别来看

b、字符串转ConfigurationPropertyName

adapt简单调用了ElementsParser去解析Elements,能够容忍无效字符

	static ConfigurationPropertyName adapt(CharSequence name, char separator,
			Function<CharSequence, CharSequence> elementValueProcessor) {
		Assert.notNull(name, "Name must not be null");
		if (name.length() == 0) {
			return EMPTY;
		}
		Elements elements = new ElementsParser(name, separator).parse(elementValueProcessor);
		if (elements.getSize() == 0) {
			return EMPTY;
		}
		return new ConfigurationPropertyName(elements);
	}

of调用了elementsOf方法,ElementsParser也是通过ElementsParser去解析,但如果存在NON_UNIFORM的话则抛异常,即名称只能有小写、数字、’-’。

	private static Elements elementsOf(CharSequence name, boolean returnNullIfInvalid, int parserCapacity) {
		if (name == null) {
			Assert.isTrue(returnNullIfInvalid, "Name must not be null");
			return null;
		}
		if (name.length() == 0) {
			return Elements.EMPTY;
		}
		if (name.charAt(0) == '.' || name.charAt(name.length() - 1) == '.') {
			if (returnNullIfInvalid) {
				return null;
			}
			throw new InvalidConfigurationPropertyNameException(name, Collections.singletonList('.'));
		}
		Elements elements = new ElementsParser(name, '.', parserCapacity).parse();
		for (int i = 0; i < elements.getSize(); i++) {
			if (elements.getType(i) == ElementType.NON_UNIFORM) {
				if (returnNullIfInvalid) {
					return null;
				}
				throw new InvalidConfigurationPropertyNameException(name, getInvalidChars(elements, i));
			}
		}
		return elements;
	}

apend在当前ConfigurationPropertyName的基础上追加字符串或另一个ConfigurationPropertyName,追加字符串的重载也是调用了elementsOf,也会对名称有限制

	public ConfigurationPropertyName append(String suffix) {
		if (!StringUtils.hasLength(suffix)) {
			return this;
		}
		Elements additionalElements = probablySingleElementOf(suffix);
		return new ConfigurationPropertyName(this.elements.append(additionalElements));
	}

	public ConfigurationPropertyName append(ConfigurationPropertyName suffix) {
		if (suffix == null) {
			return this;
		}
		return new ConfigurationPropertyName(this.elements.append(suffix.elements));
	}

c、判断equals

ConfigurationPropertyName提供的第二个重要功能就是判断两个ConfigurationPropertyName是否equal,判断两个ConfigurationPropertyName是否equal会判断每个element是否equal,判断过程中还会判断能否走捷径

	public boolean equals(Object obj) {
		if (obj == this) {
			return true;
		}
		if (obj == null || obj.getClass() != getClass()) {
			return false;
		}
		ConfigurationPropertyName other = (ConfigurationPropertyName) obj;
		if (getNumberOfElements() != other.getNumberOfElements()) {
			return false;
		}
		if (this.elements.canShortcutWithSource(ElementType.UNIFORM)
				&& other.elements.canShortcutWithSource(ElementType.UNIFORM)) {
			return toString().equals(other.toString());
		}
		return elementsEqual(other);
	}

如果两个ConfigurationPropertyName的每个element都是UNIFORM的,可以直接toString判断equal,否则需要判断每个element是否equal

	private boolean elementsEqual(ConfigurationPropertyName name) {
		for (int i = this.elements.getSize() - 1; i >= 0; i--) {
			if (elementDiffers(this.elements, name.elements, i)) {
				return false;
			}
		}
		return true;
	}

	private boolean elementDiffers(Elements e1, Elements e2, int i) {
		ElementType type1 = e1.getType(i);
		ElementType type2 = e2.getType(i);
		if (type1.allowsFastEqualityCheck() && type2.allowsFastEqualityCheck()) {
			return !fastElementEquals(e1, e2, i);
		}
		if (type1.allowsDashIgnoringEqualityCheck() && type2.allowsDashIgnoringEqualityCheck()) {
			return !dashIgnoringElementEquals(e1, e2, i);
		}
		return !defaultElementEquals(e1, e2, i);
	}
  • 如果两个element都是UNIFORM、NUMERICALLY_INDEXED中的一个,就能走fastElementEquals逻辑
  • 如果两个element都是UNIFORM、NUMERICALLY_INDEXED、DASHED中的一个,就能走dashIgnoringElementEquals逻辑
  • 否则只能适用defaultElementEquals逻辑,defaultElementEquals会将字母转小写,忽略无效字符

如果使用ConfigurationPropertyName.adapt方法将字符串转成ConfigurationPropertyName,那么以下几个都是equal的

  • demo.appName.c[0]
  • demo.app-name.c.0
  • DEMO.APP-NAME.C.0
  • DEMO.APP—$$NAME.C.0

defaultElementEquals的逻辑会导致equals存在一些问题,我也没有想明白是设计如此还是bug ,已经提交了issue,确认是bug,将在2.5.x版本改正,issue:https://github.com/spring-projects/spring-boot/issues/30317。如下

boolean c1 = adapt("demo", '.').equals(adapt("demo$$", '.'));  // true
boolean c2 = adapt("demo$$", '.').equals(adapt("demo", '.'));  // false

d、toString

toString方法也是绑定过程中会用到的一个重要方法。Form枚举指定了格式

  • ORIGNIAL:保留原本格式
  • DASHED:转换成小写,去掉无效字符,保留’-’
  • UNIFORM:转换成小写,去掉无效字符,去掉’-’
	public enum Form {
		ORIGINAL,
		DASHED,
		UNIFORM
	}

toString方法调用了buildToString,buildToString遍历elements,如果是indexed的话,保留原格式,否则采用DASHED格式

	public String toString() {
		if (this.string == null) {
			this.string = buildToString();
		}
		return this.string;
	}

	private String buildToString() {
		if (this.elements.canShortcutWithSource(ElementType.UNIFORM, ElementType.DASHED)) {
			return this.elements.getSource().toString();
		}
		int elements = getNumberOfElements();
		StringBuilder result = new StringBuilder(elements * 8);
		for (int i = 0; i < elements; i++) {
			boolean indexed = isIndexed(i);
			if (result.length() > 0 && !indexed) {
				result.append('.');
			}
			if (indexed) {
				result.append('[');
				result.append(getElement(i, Form.ORIGINAL));
				result.append(']');
			}
			else {
				result.append(getElement(i, Form.DASHED));
			}
		}
		return result.toString();
	}

2、ConfigurationProperty

ConfigurationProperty代表ConfigurationPropertySource中的kv对,有四个属性

  • name,用来查找该属性值的属性名称
  • value,值
  • source,这个kv所在的ConfigurationPropertySource
  • orgin:这个kv的来源标记
public final class ConfigurationProperty implements OriginProvider, Comparable<ConfigurationProperty> {

	private final ConfigurationPropertyName name;

	private final Object value;

	private final ConfigurationPropertySource source;

	private final Origin origin;
}

3、ConfigurationPropertySource

ConfigurationPropertySource是一个配置源,背后可以是一个.properties文件、命令行参数甚至是一个map,接口定义如下

@FunctionalInterface
public interface ConfigurationPropertySource {

	ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name);

	default ConfigurationPropertySource filter(Predicate<ConfigurationPropertyName> filter) {
		return new FilteredConfigurationPropertiesSource(this, filter);
	}

	default ConfigurationPropertySource withAliases(ConfigurationPropertyNameAliases aliases) {
		return new AliasedConfigurationPropertySource(this, aliases);
	}

	default ConfigurationPropertySource withPrefix(String prefix) {
		return (StringUtils.hasText(prefix)) ? new PrefixedConfigurationPropertySource(this, prefix) : this;
	}

	static ConfigurationPropertySource from(PropertySource<?> source) {
		if (source instanceof ConfigurationPropertySourcesPropertySource) {
			return null;
		}
		return SpringConfigurationPropertySource.from(source);
	}
}

分别看下一下这几个方法

a、getConfigurationProperty

这个ConfigurationPropertySource接口需要实现的方法,就是根据ConfigurationPropertyName查找出一个ConfigurationProperty,有两个重要的实现类:SpringConfigurationPropertySource和SpringIterableConfigurationPropertySource,在下一节专门介绍该方法的实现

b、filter

返回FilteredConfigurationPropertiesSource实例,getConfigurationProperty方法会看ConfigurationPropertyName是否满足filter条件,不满足条件的会忽略

class FilteredConfigurationPropertiesSource implements ConfigurationPropertySource {

	private final ConfigurationPropertySource source;

	private final Predicate<ConfigurationPropertyName> filter;

	FilteredConfigurationPropertiesSource(ConfigurationPropertySource source,
			Predicate<ConfigurationPropertyName> filter) {
		Assert.notNull(source, "Source must not be null");
		Assert.notNull(filter, "Filter must not be null");
		this.source = source;
		this.filter = filter;
	}

	@Override
	public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) {
		boolean filtered = getFilter().test(name);
		return filtered ? getSource().getConfigurationProperty(name) : null;
	}
}

c、withAliases

返回AliasedConfigurationPropertySource实例,getConfigurationProperty方法会首先按ConfigurationPropertyName查找,查不到再按aliasedName查找

class AliasedConfigurationPropertySource implements ConfigurationPropertySource {

	private final ConfigurationPropertySource source;

	private final ConfigurationPropertyNameAliases aliases;

	AliasedConfigurationPropertySource(ConfigurationPropertySource source, ConfigurationPropertyNameAliases aliases) {
		Assert.notNull(source, "Source must not be null");
		Assert.notNull(aliases, "Aliases must not be null");
		this.source = source;
		this.aliases = aliases;
	}

	@Override
	public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) {
		Assert.notNull(name, "Name must not be null");
		ConfigurationProperty result = getSource().getConfigurationProperty(name);
		if (result == null) {
			ConfigurationPropertyName aliasedName = getAliases().getNameForAlias(name);
			result = getSource().getConfigurationProperty(aliasedName);
		}
		return result;
	}
}

d、withPrefix

返回PrefixedConfigurationPropertySource实例,getConfigurationProperty方法会在ConfigurationPropertyName前追加prefix

class PrefixedConfigurationPropertySource implements ConfigurationPropertySource {

	private final ConfigurationPropertySource source;

	private final ConfigurationPropertyName prefix;

	PrefixedConfigurationPropertySource(ConfigurationPropertySource source, String prefix) {
		Assert.notNull(source, "Source must not be null");
		Assert.hasText(prefix, "Prefix must not be empty");
		this.source = source;
		this.prefix = ConfigurationPropertyName.of(prefix);
	}

    private ConfigurationPropertyName getPrefixedName(ConfigurationPropertyName name) {
		return this.prefix.append(name);
	}


	@Override
	public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) {
		ConfigurationProperty configurationProperty = this.source.getConfigurationProperty(getPrefixedName(name));
		if (configurationProperty == null) {
			return null;
		}
		return ConfigurationProperty.of(configurationProperty.getSource(), name, configurationProperty.getValue(),
				configurationProperty.getOrigin());
	}
}

e、from

通过SpringConfigurationPropertySource#from将一个propertySource包装成ConfigurationPropertySource,其实就是返回了SpringConfigurationPropertySource或SpringIterableConfigurationPropertySource的实例

	static SpringConfigurationPropertySource from(PropertySource<?> source) {
		Assert.notNull(source, "Source must not be null");
		PropertyMapper[] mappers = getPropertyMappers(source);
		if (isFullEnumerable(source)) {
			return new SpringIterableConfigurationPropertySource((EnumerablePropertySource<?>) source, mappers);
		}
		return new SpringConfigurationPropertySource(source, mappers);
	}

为什么需要将propertySource包装成ConfigurationPropertySource?主要是因为从将属性映射到java field是存在复杂的名称映射规则的(propertySource里配置的host[0]、host.0、HOST_0、h-o-s-t$$.0都能映射到host数组上),不能简单地用字符串到propertySource里查询

4、值获取的过程

Binder里的findProperty方法就是通过ConfigurationPropertyName查找出ConfigurationProperty,实际上遍历所有的ConfigurationPropertySource,从ConfigurationPropertySource里查找

	private <T> ConfigurationProperty findProperty(ConfigurationPropertyName name, Bindable<T> target,
			Context context) {
		if (name.isEmpty() || target.hasBindRestriction(BindRestriction.NO_DIRECT_PROPERTY)) {
			return null;
		}
		for (ConfigurationPropertySource source : context.getSources()) {
			ConfigurationProperty property = source.getConfigurationProperty(name);
			if (property != null) {
				return property;
			}
		}
		return null;
	}

ConfigurationPropertySource有两个重要实现:SpringConfigurationPropertySource和SpringIterableConfigurationPropertySource

a、SpringConfigurationPropertySource

SpringConfigurationPropertySource#getConfigurationProperty方法中把mappers映射出来的String都到propertySource里查一遍,查到了就返回了

class SpringConfigurationPropertySource implements ConfigurationPropertySource {
	private final PropertySource<?> propertySource;

	private final PropertyMapper[] mappers;

	@Override
	public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) {
		if (name == null) {
			return null;
		}
		for (PropertyMapper mapper : this.mappers) {
			try {
				for (String candidate : mapper.map(name)) {
					Object value = getPropertySource().getProperty(candidate);
					if (value != null) {
						Origin origin = PropertySourceOrigin.get(this.propertySource, candidate);
						return ConfigurationProperty.of(this, name, value, origin);
					}
				}
			}
			catch (Exception ex) {
			}
		}
		return null;
	}
}

mapper是什么呢?就是PropertyMapper,能实现ConfigurationPropertyName到String相互映射

interface PropertyMapper {

	List<String> map(ConfigurationPropertyName configurationPropertyName);

	ConfigurationPropertyName map(String propertySourceName);
}

PropertyMapper有两个实现,DefaultPropertyMapper和SystemEnvironmentPropertyMapper

  • DefaultPropertyMapper简单调用了ConfigurationPropertyName#toString和ConfigurationPropertyName#adapt方法进行相互映射
  • SystemEnvironmentPropertyMapper处理了大小写转换、’-‘和’‘的转换,因为系统环境变量往往是全大写、’'分隔的形式

mappers具体都有哪些呢?就看PropertySource是不是系统环境的PropertySource

	private static final PropertyMapper[] DEFAULT_MAPPERS = { DefaultPropertyMapper.INSTANCE };
	private static final PropertyMapper[] SYSTEM_ENVIRONMENT_MAPPERS = { SystemEnvironmentPropertyMapper.INSTANCE,
			DefaultPropertyMapper.INSTANCE };

    private static PropertyMapper[] getPropertyMappers(PropertySource<?> source) {
		if (source instanceof SystemEnvironmentPropertySource && hasSystemEnvironmentName(source)) {
			return SYSTEM_ENVIRONMENT_MAPPERS;
		}
		return DEFAULT_MAPPERS;
	}

b、SpringIterableConfigurationPropertySource

SpringIterableConfigurationPropertySource继承了上面的SpringConfigurationPropertySource类,getConfigurationProperty方法首先调用了父类的getConfigurationProperty方法(到这里java属性按照驼峰命名法、.properties按照’-'分隔的写法都能查到了),如果查不到再从getMappings().getMapped(name)里查一遍

	@Override
	public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) {
		if (name == null) {
			return null;
		}
		ConfigurationProperty configurationProperty = super.getConfigurationProperty(name);
		if (configurationProperty != null) {
			return configurationProperty;
		}
		for (String candidate : getMappings().getMapped(name)) {
			Object value = getPropertySource().getProperty(candidate);
			if (value != null) {
				Origin origin = PropertySourceOrigin.get(getPropertySource(), candidate);
				return ConfigurationProperty.of(this, name, value, origin);
			}
		}
		return null;
	}

Mapping是什么呢?既然正常的映射规则查不到,那就将propertySource里所有的key都转成ConfigurationPropertyName,通过ConfigurationPropertyName的equals方法来进行查找,Mapping包含两种映射:一种是propertySource里的key到ConfigurationPropertyName的映射,称为reverseMappings;一种是propertySource里的key转成的ConfigurationPropertyName到key的映射,称为mappings。由于可能存在多个key映射到的ConfigurationPropertyName都是equals的,所有mappings映射的key是Set,如下

	private static class Mappings {

		private volatile Map<ConfigurationPropertyName, Set<String>> mappings;

		private volatile Map<String, ConfigurationPropertyName> reverseMappings;
    }

有了以上理解,如何构造出这个Mappings逻辑也好理解了

	private Mappings getMappings() {
		return this.cache.get(this::createMappings, this::updateMappings);
	}

getMappings包含了缓存逻辑,总之就是createMappings和updateMappings都会被调用

	private Mappings createMappings() {
		return new Mappings(getMappers(), isImmutablePropertySource(),
				this.ancestorOfCheck == PropertyMapper.DEFAULT_ANCESTOR_OF_CHECK);
	}

createMappings方法new了一个Mappings,并把当前SpringIterableConfigurationPropertySource拥有的mappers传给了Mappings

	private Mappings updateMappings(Mappings mappings) {
		mappings.updateMappings(getPropertySource()::getPropertyNames);
		return mappings;
	}

updateMappings方法调用了mappings的updateMappings方法,并把propertyNames传了进去。


		void updateMappings(Supplier<String[]> propertyNames) {
			if (this.mappings == null || !this.immutable) {
				int count = 0;
				while (true) {
					try {
						updateMappings(propertyNames.get());
						return;
					}
					catch (ConcurrentModificationException ex) {
						if (count++ > 10) {
							throw ex;
						}
					}
				}
			}
		}

然后又调用了updateMapping重载


		private void updateMappings(String[] propertyNames) {
			String[] lastUpdated = this.lastUpdated;
			if (lastUpdated != null && Arrays.equals(lastUpdated, propertyNames)) {
				return;
			}
			int size = propertyNames.length;
			Map<ConfigurationPropertyName, Set<String>> mappings = cloneOrCreate(this.mappings, size);
			Map<String, ConfigurationPropertyName> reverseMappings = cloneOrCreate(this.reverseMappings, size);
			Map<ConfigurationPropertyName, Set<ConfigurationPropertyName>> descendants = cloneOrCreate(this.descendants,
					size);
			for (PropertyMapper propertyMapper : this.mappers) {
				for (String propertyName : propertyNames) {
					if (!reverseMappings.containsKey(propertyName)) {
						ConfigurationPropertyName configurationPropertyName = propertyMapper.map(propertyName);
						if (configurationPropertyName != null && !configurationPropertyName.isEmpty()) {
							add(mappings, configurationPropertyName, propertyName);
							reverseMappings.put(propertyName, configurationPropertyName);
							if (this.trackDescendants) {
								addParents(descendants, configurationPropertyName);
							}
						}
					}
				}
			}
			this.mappings = mappings;
			this.reverseMappings = reverseMappings;
			this.descendants = descendants;
			this.lastUpdated = this.immutable ? null : propertyNames;
			this.configurationPropertyNames = this.immutable
					? reverseMappings.values().toArray(new ConfigurationPropertyName[0]) : null;
		}

		private <K, T> void add(Map<K, Set<T>> map, K key, T value) {
			map.computeIfAbsent(key, (k) -> new HashSet<>()).add(value);
		}

这里是构造mappings和reverseMappings的核心逻辑。两个for循环遍历了所有mapper和propertySource的key,用mapper将key转成ConfigurationPropertyName,然后将propertyName和ConfigurationPropertyName的映射关系加入到mappings和reverseMappings
Mappings构造完毕,getMapped方法返回mappings里configurationPropertyName对应的key集合

		Set<String> getMapped(ConfigurationPropertyName configurationPropertyName) {
			return this.mappings.getOrDefault(configurationPropertyName, Collections.emptySet());
		}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值