【源码分析】Spring Boot中Relaxed Binding机制的不同实现
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);}
...
}
以上的代码其逻辑可以描述如下:
- 借助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 |
- 构造PropertySourcesPropertyValues对象从配置文件(如properties文件)中查找匹配的值,
- 使用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类中实现了一个比较复杂的递归调用。
- ConfigurationPropertiesBinder 调用Binder类的bind函数时,参数通过层层转换,来到bindObject函数中。
- bindObject函数中,通过bindAggregate,bindProperty与bindBean等私有方法逐步推导绑定,bindBean是最后一步。
- 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机制的不同实现相关推荐
- 从源码分析 Spring 基于注解的事务
从源码分析 Spring 基于注解的事务 在spring引入基于注解的事务(@Transactional)之前,我们一般都是如下这样进行拦截事务的配置: <!-- 拦截器方式配置事务 --> ...
- dubbo源码分析系列(1)扩展机制的实现
1 系列目录 dubbo源码分析系列(1)扩展机制的实现 dubbo源码分析系列(2)服务的发布 dubbo源码分析系列(3)服务的引用 dubbo源码分析系列(4)dubbo通信设计 2 SPI扩展 ...
- 【Spring源码】Spring Transactional事务:传播机制(Propagation) 介绍 和 源码剖析
[Spring源码]Spring Transactional事务:传播机制(Propagation) 源码剖析 关键词 AMethod调用BMethod,转载BMethod的角度来考虑:站在被调用者的 ...
- kube-controller-manager源码分析(三)之 Informer机制
本文个人博客地址:https://www.huweihuang.com/kubernetes-notes/code-analysis/kube-controller-manager/sharedInd ...
- 源码解读 Spring Boot Profiles
点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 前言 上文<一文掌握 Spring Boot Profiles> 是对 Spri ...
- apollo源码分析 感知_Kitty中的动态线程池支持Nacos,Apollo多配置中心了
目录 回顾昨日 nacos 集成 Spring Cloud Alibaba 方式 Nacos Spring Boot 方式 Apollo 集成 自研配置中心对接 无配置中心对接 实现源码分析 兼容 A ...
- springboot启动过程_spring5/springboot2源码学习 -- spring boot 应用的启动过程
推荐阅读: Spring全家桶笔记:Spring+Spring Boot+Spring Cloud+Spring MVC 疫情期间"闭关修炼",吃透这本Java核心知识,跳槽面试不 ...
- 源码分析 - Spring Security OAuth2 生成 token 的执行流程
说明 本文内容全部基于 Spring Security OAuth2(2.3.5.RELEASE). OAuth2.0 有四种授权模式, 本文会以 密码模式 来举例讲解源码. 阅读前, 需要对 OAu ...
- lodash源码分析之compact中的遍历
小时候, 乡愁是一枚小小的邮票, 我在这头, 母亲在那头. 长大后,乡愁是一张窄窄的船票, 我在这头, 新娘在那头. 后来啊, 乡愁是一方矮矮的坟墓, 我在外头, 母亲在里头. 而现在, 乡愁是一湾浅 ...
- 「源码分析」CopyOnWriteArrayList 中的隐藏知识,你Get了吗?
前言 本觉 CopyOnWriteArrayList 过于简单,寻思看名字就能知道内部的实现逻辑,所以没有写这篇文章的想法,最近又仔细看了下 CopyOnWriteArrayList 的源码实现,大体 ...
最新文章
- Chroot’ing users with openssh[强文推荐]
- Cissp-【第5章 身份与访问管理】-2021-3-14(601页-660页)
- mysql Decimal(M,D)解释
- 基于FPGA的HDB3编译码器设计
- Android自定义View之仿QQ侧滑菜单实现
- MongoDB的地理位置索引
- 【MM模块】Vendor Consignment 供应商寄售
- 【LeetCode】Minimum Depth of Binary Tree 二叉树的最小深度 java
- iOS项目功能模块封装SDK使用总结
- FusionInsight怎么帮「宇宙行」建一个好的「云数据平台」?
- JavaScript面向对象和原型函数
- 我是怎么找电子书的?
- 苹果计算机系统是什么,苹果电脑系统和Win电脑系统有什么不同
- VHDL三段式状态机
- Python 实现自动刷抖音,解放双手了
- dw网页制作的基本步骤_dreamweaver制作网页详细步骤(设计网站首页)
- word2010学习
- 怎么给电脑里面的文件加密?这个软件轻松帮忙搞定
- cesium 获取当前屏幕视角的三维参数,x、y、z、heading、pitch、roll
- 图形学学习笔记2——点阵图形光栅化