用法: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结构如下
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());
}