Relaxed Binding 是 Spring Boot 中一個有趣的機制,它可以讓開發(fā)人員更靈活地進行配置。但偶然的機會,我發(fā)現這個機制在 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 | 大寫+下劃線分隔 | 推薦用于環(huán)境變量或命令行參數 |
源碼分析
出于好奇,我研究了一下SpringBoot的源碼,發(fā)現在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 {
...
@Override
public 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函數則在執(zhí)行Bean的初始化調用(如afterPropertiesSet)前被調用。
postProcessBeforeInitialization函數執(zhí)行時,屬性值綁定的工作被委派給了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;
}
}
如上代碼,將字符串拆為單個字符進行比較,使用了雙層循環(huán)匹配。
Spring Boot 2.0版本的實現(以2.0.4.RELEASE版本為例)
與1.5.14.RELEASE有很大區(qū)別。
屬性與配置值的綁定邏輯依舊始于
ConfigurationPropertiesBindingPostProcessor 類的
postProcessBeforeInitialization 函數。
但 ConfigurationPropertiesBindingPostProcessor 類的定義與實現均發(fā)生了變化。先看代碼。
public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,
PriorityOrdered, ApplicationContextAware, InitializingBean {
...
private ConfigurationPropertiesBinder configurationPropertiesBinder;
...
@Override
public 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的一個內部靜態(tài)類,表面上負責處理@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 bean
Object 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表達式的執(zhí)行結果,properties配置文件的配置項對應的是SpringIterableConfigurationPropertySource類型,因此調用的是其getConfigurationProperty函數,關鍵代碼如下。
class SpringIterableConfigurationPropertySource extends SpringConfigurationPropertySource
implements IterableConfigurationPropertySource {
...
@Override
public 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 ConfigurationPropertyName
implements Comparable<ConfigurationPropertyName> {
@Override
public 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 |
| 查找方式 | 將屬性拼接為字符串,窮舉字符串的可能性 | 遞歸查找,分級匹配 |
| 字符串匹配方式 | 雙層循環(huán)匹配 | 單層循環(huán)匹配 |
| 代碼風格 | 傳統(tǒng)java代碼 | 大量使用lambda表達式 |
| 優(yōu)點 | 由于使用傳統(tǒng)代碼風格,代碼層次相對簡單,可讀性較強 | 不采用窮舉的方式,匹配更精準,查找更有效 |
| 缺點 | 由于使用拼接窮舉,當屬性數量大且有“-”或“_”分隔單詞時,組合會變多,影響查找效率 | 使用lambda表達式,產生各種延遲綁定,代碼可讀性差,不易調試 |
題外話:為什么我發(fā)現了這兩個版本實現的不一致?
前一兩個月,我寫了一個自動識別消息的框架,在消息中模糊查找可能存在的關鍵信息,用了1.5版本的RelaxedNames類,雖然這個類在官方文檔上沒有提到,但是挺好用的。
然而最近,我想把框架遷移到2.0版本上,結果遇到了編譯錯誤。因此特別研究了一下,發(fā)現這些類都不見了。
幸好只是小問題,我把RelaxedNames源碼拷貝過來一份,就解決了問題,但這也給我提了個醒,大版本升級還是要仔細閱讀下代碼,避免留下一些坑。
另外也從一個側面反映出,Spring Boot的2.0版本相比于1.5確實做了不小的更改,有空再好好琢磨下。