Relaxed Binding 是 Spring Boot 中一个有趣的机制,它可以让开发人员更灵活地进行配置。但偶然的机会,我发现这个机制在 Spring Boot 1.5 与 Spring Boot 2.0 版本中的实现是不一样的。

Relaxed Binding机制

关于Relaxed Binding机制在Spring Boot的官方文档中有描述,链接如下:
24.7 Type-safe Configuration Properties
24.7.2 Relaxed Binding
这个Relaxed Binding机制的主要作用是,当配置文件(properties或yml)中的配置项值与带@ConfigurationProperties注解的配置类属性进行绑定时,可以进行相对宽松的绑定。
本文并不是针对@ConfigurationProperties与类型安全的属性配置进行解释,相关的说明请参考上面的链接。

Relax Binding示例

下面的例子有助于理解Relaxed Binding。
假设有一个带@ConfigurationProperties注解的属性类

@ConfigurationProperties(prefix="my")
public class MyProperties {private String firstName;public String getFirstName() {return this.firstName;}public void setFirstName(String firstName) {this.firstName = firstName;}
}

在配置文件(properties文件)中,以下的写法都能将值正确注入到firstName这个属性中。

property 写法 说明 推荐场景
my.firstName 标准驼峰
my.first-name 减号分隔 推荐在properties或yml文件中使用
my.first_name 下划线分隔
MY_FIRST_NAME 大写+下划线分隔 推荐用于环境变量或命令行参数

源码分析

出于好奇,我研究了一下SpringBoot的源码,发现在1.5版本与2.0版本中,Relaxed Binding 的具体实现是不一样的。
为了解释两个版本的不同之处,我首先在properties配置文件中做如下的配置。

my.first_Name=fonoisrev

Spring Boot 1.5版本的实现(以1.5.14.RELEASE版本为例)

属性与配置值的绑定逻辑始于ConfigurationPropertiesBindingPostProcessor类的postProcessBeforeInitialization函数。

public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,BeanFactoryAware, EnvironmentAware, ApplicationContextAware, InitializingBean,DisposableBean, ApplicationListener<ContextRefreshedEvent>, PriorityOrdered {
...@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName)throws BeansException {...ConfigurationProperties annotation = AnnotationUtils.findAnnotation(bean.getClass(), ConfigurationProperties.class);if (annotation != null) {postProcessBeforeInitialization(bean, beanName, annotation);}...}private void postProcessBeforeInitialization(Object bean, String beanName,ConfigurationProperties annotation) {Object target = bean;PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>(target);factory.setPropertySources(this.propertySources);...try {factory.bindPropertiesToTarget();}...}
...
}

从以上代码可以看出,该类实现了BeanPostProcessor接口(BeanPostProcessor官方文档请点我)。BeanPostProcessor接口主要作用是允许自行实现Bean装配的逻辑,而postProcessBeforeInitialization函数则在执行Bean的初始化调用(如afterPropertiesSet)前被调用。

postProcessBeforeInitialization函数执行时,属性值绑定的工作被委派给了PropertiesConfigurationFactory<T>类。

public class PropertiesConfigurationFactory<T> implements FactoryBean<T>,ApplicationContextAware, MessageSourceAware, InitializingBean {
...public void bindPropertiesToTarget() throws BindException {...doBindPropertiesToTarget();...}private void doBindPropertiesToTarget() throws BindException {RelaxedDataBinder dataBinder = (this.targetName != null? new RelaxedDataBinder(this.target, this.targetName): new RelaxedDataBinder(this.target));...Iterable<String> relaxedTargetNames = getRelaxedTargetNames();Set<String> names = getNames(relaxedTargetNames);PropertyValues propertyValues = getPropertySourcesPropertyValues(names,relaxedTargetNames);dataBinder.bind(propertyValues);...}private PropertyValues getPropertySourcesPropertyValues(Set<String> names,Iterable<String> relaxedTargetNames) {PropertyNamePatternsMatcher includes = getPropertyNamePatternsMatcher(names,relaxedTargetNames);return new PropertySourcesPropertyValues(this.propertySources, names, includes,this.resolvePlaceholders);}
...
}

以上的代码其逻辑可以描述如下:

  1. 借助RelaxedNames类,将注解@ConfigurationProperties(prefix="my")中的前缀“my”与MyProperties类的属性firstName可能存在的情况进行穷举(共28种情况,如下表)
序号 RelaxedNames
0 my.first-name
1 my_first-name
2 my.first_name
3 my_first_name
4 my.firstName
5 my_firstName
6 my.firstname
7 my_firstname
8 my.FIRST-NAME
9 my_FIRST-NAME
10 my.FIRST_NAME
11 my_FIRST_NAME
12 my.FIRSTNAME
13 my_FIRSTNAME
14 MY.first-name
15 MY_first-name
16 MY.first_name
17 MY_first_name
18 MY.firstName
19 MY_firstName
20 MY.firstname
21 MY_firstname
22 MY.FIRST-NAME
23 MY_FIRST-NAME
24 MY.FIRST_NAME
25 MY_FIRST_NAME
26 MY.FIRSTNAME
27 MY_FIRSTNAME
  1. 构造PropertySourcesPropertyValues对象从配置文件(如properties文件)中查找匹配的值,
  2. 使用DataBinder操作PropertySourcesPropertyValues实现MyProperties类的setFirstName方法调用,完成绑定。
public class PropertySourcesPropertyValues implements PropertyValues {
...PropertySourcesPropertyValues(PropertySources propertySources,Collection<String> nonEnumerableFallbackNames,PropertyNamePatternsMatcher includes, boolean resolvePlaceholders) {...PropertySourcesPropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);for (PropertySource<?> source : propertySources) {processPropertySource(source, resolver);}}private void processPropertySource(PropertySource<?> source,PropertySourcesPropertyResolver resolver) {...processEnumerablePropertySource((EnumerablePropertySource<?>) source,resolver, this.includes);...}private void processEnumerablePropertySource(EnumerablePropertySource<?> source,PropertySourcesPropertyResolver resolver,PropertyNamePatternsMatcher includes) {if (source.getPropertyNames().length > 0) {for (String propertyName : source.getPropertyNames()) {if (includes.matches(propertyName)) {Object value = getEnumerableProperty(source, resolver, propertyName);putIfAbsent(propertyName, value, source);}}}}
...
}

从以上代码来看,整个匹配的过程其实是在PropertySourcesPropertyValues对象构造的过程中完成的。
更具体地说,是在DefaultPropertyNamePatternsMatcher的matches函数中完成字符串匹配的,如下代码。

class DefaultPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher {
...public boolean matches(String propertyName) {char[] propertyNameChars = propertyName.toCharArray();boolean[] match = new boolean[this.names.length];boolean noneMatched = true;for (int i = 0; i < this.names.length; i++) {if (this.names[i].length() <= propertyNameChars.length) {match[i] = true;noneMatched = false;}}if (noneMatched) {return false;}for (int charIndex = 0; charIndex < propertyNameChars.length; charIndex++) {for (int nameIndex = 0; nameIndex < this.names.length; nameIndex++) {if (match[nameIndex]) {match[nameIndex] = false;if (charIndex < this.names[nameIndex].length()) {if (isCharMatch(this.names[nameIndex].charAt(charIndex),propertyNameChars[charIndex])) {match[nameIndex] = true;noneMatched = false;}}else {char charAfter = propertyNameChars[this.names[nameIndex].length()];if (isDelimiter(charAfter)) {match[nameIndex] = true;noneMatched = false;}}}}if (noneMatched) {return false;}}for (int i = 0; i < match.length; i++) {if (match[i]) {return true;}}return false;}private boolean isCharMatch(char c1, char c2) {if (this.ignoreCase) {return Character.toLowerCase(c1) == Character.toLowerCase(c2);}return c1 == c2;}private boolean isDelimiter(char c) {for (char delimiter : this.delimiters) {if (c == delimiter) {return true;}}return false;}
}

如上代码,将字符串拆为单个字符进行比较,使用了双层循环匹配。

Spring Boot 2.0版本的实现(以2.0.4.RELEASE版本为例)

与1.5.14.RELEASE有很大区别。
属性与配置值的绑定逻辑依旧始于
ConfigurationPropertiesBindingPostProcessor 类的
postProcessBeforeInitialization 函数。
但 ConfigurationPropertiesBindingPostProcessor 类的定义与实现均发生了变化。先看代码。

public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,PriorityOrdered, ApplicationContextAware, InitializingBean {
...private ConfigurationPropertiesBinder configurationPropertiesBinder;
...@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName)throws BeansException {ConfigurationProperties annotation = getAnnotation(bean, beanName,ConfigurationProperties.class);if (annotation != null) {bind(bean, beanName, annotation);}return bean;}private void bind(Object bean, String beanName, ConfigurationProperties annotation) {ResolvableType type = getBeanType(bean, beanName);Validated validated = getAnnotation(bean, beanName, Validated.class);Annotation[] annotations = (validated != null)? new Annotation[] { annotation, validated }: new Annotation[] { annotation };Bindable<?> target = Bindable.of(type).withExistingValue(bean).withAnnotations(annotations);try {this.configurationPropertiesBinder.bind(target);}...}
...
}

从以上代码可以看出,该类依旧实现了BeanPostProcessor接口,但调用postProcessBeforeInitialization函数,属性值绑定的工作则被委派给了ConfigurationPropertiesBinder类,调用了bind函数。

class ConfigurationPropertiesBinder {
...public void bind(Bindable<?> target) {ConfigurationProperties annotation = target.getAnnotation(ConfigurationProperties.class);Assert.state(annotation != null,() -> "Missing @ConfigurationProperties on " + target);List<Validator> validators = getValidators(target);BindHandler bindHandler = getBindHandler(annotation, validators);getBinder().bind(annotation.prefix(), target, bindHandler);}private Binder getBinder() {if (this.binder == null) {this.binder = new Binder(getConfigurationPropertySources(),getPropertySourcesPlaceholdersResolver(), getConversionService(),getPropertyEditorInitializer());}return this.binder;}
...
}

ConfigurationPropertiesBinder类并不是一个public的类。实际上这相当于ConfigurationPropertiesBindingPostProcessor的一个内部静态类,表面上负责处理@ConfigurationProperties注解的绑定任务。
但从代码中可以看出,具体的工作委派给另一个Binder类的对象。
Binder类是SpringBoot 2.0版本后加入的类,其负责处理对象与多个ConfigurationPropertySource之间的绑定。

public class Binder {
...private static final List<BeanBinder> BEAN_BINDERS;static {List<BeanBinder> binders = new ArrayList<>();binders.add(new JavaBeanBinder());BEAN_BINDERS = Collections.unmodifiableList(binders);}
...public <T> BindResult<T> bind(String name, Bindable<T> target, BindHandler handler) {return bind(ConfigurationPropertyName.of(name), target, handler);}public <T> BindResult<T> bind(ConfigurationPropertyName name, Bindable<T> target,BindHandler handler) {...Context context = new Context();T bound = bind(name, target, handler, context, false);return BindResult.of(bound);}protected final <T> T bind(ConfigurationPropertyName name, Bindable<T> target,BindHandler handler, Context context, boolean allowRecursiveBinding) {...try {...Object bound = bindObject(name, target, handler, context,allowRecursiveBinding);return handleBindResult(name, target, handler, context, bound);}...}private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> target,BindHandler handler, Context context, boolean allowRecursiveBinding) {ConfigurationProperty property = findProperty(name, context);if (property == null && containsNoDescendantOf(context.streamSources(), name)) {return null;}AggregateBinder<?> aggregateBinder = getAggregateBinder(target, context);if (aggregateBinder != null) {return bindAggregate(name, target, handler, context, aggregateBinder);}if (property != null) {try {return bindProperty(target, context, property);}catch (ConverterNotFoundException ex) {// We might still be able to bind it as a beanObject bean = bindBean(name, target, handler, context,allowRecursiveBinding);if (bean != null) {return bean;}throw ex;}}return bindBean(name, target, handler, context, allowRecursiveBinding);}private Object bindBean(ConfigurationPropertyName name, Bindable<?> target,BindHandler handler, Context context, boolean allowRecursiveBinding) {...BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName), propertyTarget, handler, context, false);Class<?> type = target.getType().resolve(Object.class);...return context.withBean(type, () -> {Stream<?> boundBeans = BEAN_BINDERS.stream().map((b) -> b.bind(name, target, context, propertyBinder));return boundBeans.filter(Objects::nonNull).findFirst().orElse(null);});}
...private ConfigurationProperty findProperty(ConfigurationPropertyName name,Context context) {...return context.streamSources().map((source) -> source.getConfigurationProperty(name)).filter(Objects::nonNull).findFirst().orElse(null);}
}

如下代码,Binder类中实现了一个比较复杂的递归调用。

  1. ConfigurationPropertiesBinder 调用Binder类的bind函数时,参数通过层层转换,来到bindObject函数中。
  2. bindObject函数中,通过bindAggregate,bindProperty与bindBean等私有方法逐步推导绑定,bindBean是最后一步。
  3. bindBean函数通过定义BeanPropertyBinder的lambda表达式,允许bean绑定过程递归调用bindObject函数。

实际上,bindObject函数中findProperty函数调用是从properties文件中查找匹配项的关键,根据lambda表达式的执行结果,properties配置文件的配置项对应的是SpringIterableConfigurationPropertySource类型,因此调用的是其getConfigurationProperty函数,关键代码如下。

class SpringIterableConfigurationPropertySource extends SpringConfigurationPropertySourceimplements IterableConfigurationPropertySource {
...@Overridepublic ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) {ConfigurationProperty configurationProperty = super.getConfigurationProperty(name);if (configurationProperty == null) {configurationProperty = find(getPropertyMappings(getCache()), name);}return configurationProperty;}protected final ConfigurationProperty find(PropertyMapping[] mappings,ConfigurationPropertyName name) {for (PropertyMapping candidate : mappings) {if (candidate.isApplicable(name)) {ConfigurationProperty result = find(candidate);if (result != null) {return result;}}}return null;}
...
}

最终,isApplicable函数中判断properties文件中的配置项(my.first_Name)与MyProperties类中的属性(firstName)是否匹配,其字符串比较过程使用的是ConfigurationPropertyName类重写的equals方法。

public final class ConfigurationPropertyNameimplements Comparable<ConfigurationPropertyName> {@Overridepublic boolean equals(Object obj) {...ConfigurationPropertyName other = (ConfigurationPropertyName) obj;...for (int i = 0; i < this.elements.length; i++) {if (!elementEquals(this.elements[i], other.elements[i])) {return false;}}return true;}private boolean elementEquals(CharSequence e1, CharSequence e2) {int l1 = e1.length();int l2 = e2.length();boolean indexed1 = isIndexed(e1);int offset1 = indexed1 ? 1 : 0;boolean indexed2 = isIndexed(e2);int offset2 = indexed2 ? 1 : 0;int i1 = offset1;int i2 = offset2;while (i1 < l1 - offset1) {if (i2 >= l2 - offset2) {return false;}char ch1 = indexed1 ? e1.charAt(i1) : Character.toLowerCase(e1.charAt(i1));char ch2 = indexed2 ? e2.charAt(i2) : Character.toLowerCase(e2.charAt(i2));if (ch1 == '-' || ch1 == '_') {i1++;}else if (ch2 == '-' || ch2 == '_') {i2++;}else if (ch1 != ch2) {return false;}else {i1++;i2++;}}while (i2 < l2 - offset2) {char ch = e2.charAt(i2++);if (ch != '-' && ch != '_') {return false;}}return true;}
}

两者的异同

总结一下两个版本实现的不同。

对比科目 Spring Boot-1.5.14.RELEASE版本 Spring Boot-2.0.4.RELEASE版本
java版本 jdk1.7以上 jdk1.8以上
关键类 ConfigurationPropertiesBindingPostProcessor,PropertiesConfigurationFactory<T>,PropertySourcesPropertyValues,DefaultPropertyNamePatternsMatcher ConfigurationPropertiesBindingPostProcessor,ConfigurationPropertiesBinder,SpringIterableConfigurationPropertySource,ConfigurationPropertyName
查找方式 将属性拼接为字符串,穷举字符串的可能性 递归查找,分级匹配
字符串匹配方式 双层循环匹配 单层循环匹配
代码风格 传统java代码 大量使用lambda表达式
优点 由于使用传统代码风格,代码层次相对简单,可读性较强 不采用穷举的方式,匹配更精准,查找更有效
缺点 由于使用拼接穷举,当属性数量大且有“-”或“_”分隔单词时,组合会变多,影响查找效率 使用lambda表达式,产生各种延迟绑定,代码可读性差,不易调试

题外话:为什么我发现了这两个版本实现的不一致?

前一两个月,我写了一个自动识别消息的框架,在消息中模糊查找可能存在的关键信息,用了1.5版本的RelaxedNames类,虽然这个类在官方文档上没有提到,但是挺好用的。
然而最近,我想把框架迁移到2.0版本上,结果遇到了编译错误。因此特别研究了一下,发现这些类都不见了。
幸好只是小问题,我把RelaxedNames源码拷贝过来一份,就解决了问题,但这也给我提了个醒,大版本升级还是要仔细阅读下代码,避免留下一些坑。
另外也从一个侧面反映出,Spring Boot的2.0版本相比于1.5确实做了不小的更改,有空再好好琢磨下。

【源码分析】Spring Boot中Relaxed Binding机制的不同实现相关推荐

  1. 从源码分析 Spring 基于注解的事务

    从源码分析 Spring 基于注解的事务 在spring引入基于注解的事务(@Transactional)之前,我们一般都是如下这样进行拦截事务的配置: <!-- 拦截器方式配置事务 --> ...

  2. dubbo源码分析系列(1)扩展机制的实现

    1 系列目录 dubbo源码分析系列(1)扩展机制的实现 dubbo源码分析系列(2)服务的发布 dubbo源码分析系列(3)服务的引用 dubbo源码分析系列(4)dubbo通信设计 2 SPI扩展 ...

  3. 【Spring源码】Spring Transactional事务:传播机制(Propagation) 介绍 和 源码剖析

    [Spring源码]Spring Transactional事务:传播机制(Propagation) 源码剖析 关键词 AMethod调用BMethod,转载BMethod的角度来考虑:站在被调用者的 ...

  4. kube-controller-manager源码分析(三)之 Informer机制

    本文个人博客地址:https://www.huweihuang.com/kubernetes-notes/code-analysis/kube-controller-manager/sharedInd ...

  5. 源码解读 Spring Boot Profiles

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 前言 上文<一文掌握 Spring Boot Profiles> 是对 Spri ...

  6. apollo源码分析 感知_Kitty中的动态线程池支持Nacos,Apollo多配置中心了

    目录 回顾昨日 nacos 集成 Spring Cloud Alibaba 方式 Nacos Spring Boot 方式 Apollo 集成 自研配置中心对接 无配置中心对接 实现源码分析 兼容 A ...

  7. springboot启动过程_spring5/springboot2源码学习 -- spring boot 应用的启动过程

    推荐阅读: Spring全家桶笔记:Spring+Spring Boot+Spring Cloud+Spring MVC 疫情期间"闭关修炼",吃透这本Java核心知识,跳槽面试不 ...

  8. 源码分析 - Spring Security OAuth2 生成 token 的执行流程

    说明 本文内容全部基于 Spring Security OAuth2(2.3.5.RELEASE). OAuth2.0 有四种授权模式, 本文会以 密码模式 来举例讲解源码. 阅读前, 需要对 OAu ...

  9. lodash源码分析之compact中的遍历

    小时候, 乡愁是一枚小小的邮票, 我在这头, 母亲在那头. 长大后,乡愁是一张窄窄的船票, 我在这头, 新娘在那头. 后来啊, 乡愁是一方矮矮的坟墓, 我在外头, 母亲在里头. 而现在, 乡愁是一湾浅 ...

  10. 「源码分析」CopyOnWriteArrayList 中的隐藏知识,你Get了吗?

    前言 本觉 CopyOnWriteArrayList 过于简单,寻思看名字就能知道内部的实现逻辑,所以没有写这篇文章的想法,最近又仔细看了下 CopyOnWriteArrayList 的源码实现,大体 ...

最新文章

  1. Chroot’ing users with openssh[强文推荐]
  2. Cissp-【第5章 身份与访问管理】-2021-3-14(601页-660页)
  3. mysql Decimal(M,D)解释
  4. 基于FPGA的HDB3编译码器设计
  5. Android自定义View之仿QQ侧滑菜单实现
  6. MongoDB的地理位置索引
  7. 【MM模块】Vendor Consignment 供应商寄售
  8. 【LeetCode】Minimum Depth of Binary Tree 二叉树的最小深度 java
  9. iOS项目功能模块封装SDK使用总结
  10. FusionInsight怎么帮「宇宙行」建一个好的「云数据平台」?
  11. JavaScript面向对象和原型函数
  12. 我是怎么找电子书的?
  13. 苹果计算机系统是什么,苹果电脑系统和Win电脑系统有什么不同
  14. VHDL三段式状态机
  15. Python 实现自动刷抖音,解放双手了
  16. dw网页制作的基本步骤_dreamweaver制作网页详细步骤(设计网站首页)
  17. word2010学习
  18. 怎么给电脑里面的文件加密?这个软件轻松帮忙搞定
  19. cesium 获取当前屏幕视角的三维参数,x、y、z、heading、pitch、roll
  20. 图形学学习笔记2——点阵图形光栅化

热门文章

  1. 【Arcgis】Extract by Mask时出错,ERROR 999999
  2. 如何获取 iOS 设备 UDID?
  3. Substance Designer Dirt Ground
  4. Microsoft Recruit in Suzhou Branch (微软苏州招聘)
  5. 动态电路中的动态元件——电容和电感
  6. 蓝桥杯单片机头文件导入_CT107D蓝桥杯单片机编程笔记
  7. 大时代,小过客——《激荡三十年》优秀读后感范文4600字
  8. 20210108练习
  9. Word中批量进行中英文标点的转换
  10. 虚拟机VMware官网下载教程,中文详细步骤(图文)