Spring Boot 2.0源码解析-配置绑定

[TOC]

一. 前言

开发中, 时常会有获取某个属性资源文件的场景, 尤其是在多个Profile不同配置时

熟悉Spring的可能知道:

  • 来自spring-beans的@Value能够完成这一功能
  • 我们经常在xml中配置的PropertyPlaceholderConfigurer及其父类就是来负责解析属性的

想要了解它的解析过程, 可以从PropertyPlaceholderConfigurer#postProcessBeanFactory()入手

@Value用起来很方便, 而在Spring Boot中, 提供了一种更为优雅的配置绑定方式: @ConfigurationProperties

通过配合该注解来完成属性资源文件到Bean属性的绑定.

之前我很好奇为什么引入rabbit依赖后, 没有配置任何属性源而直接启动就能连接到, Spring Boot的Auto Configuration特性是怎么把连接的配置属性绑定进去的呢

spring-boot-autoconfigure源码中, 能看到大量@ConfigurationProperties案例的运用, 例如我们熟悉的:

  • RedisProperties
  • JpaProperties
  • RabbitProperties
  • etc…

这次源码分析我选用的版本是Spring Boot 2.0.8.RELEASE, 而网上的源码分析大都是基于1.x版本

二. 变化

迁移的变化

在Spring Boot 2.x中, 对配置属性绑定有了一些变化, 例如Relaxed Binding(宽松绑定)的约束, 以及新的Binder API

关于Relaxed Binding, 如果是1.x迁移2.x, 这部分内容的变化并不会造成很大麻烦

但如果你之前的配置使用了驼峰或过于”宽松”的写法, 那在初始化时校验就会收到报错的, 不信可以试试

具体变化可以参见wiki: Configuration Property Binding

源码的变化

除了功能方面, 相较1.x版本, 代码部分在2.x有了较大的重构, 重写了整个绑定过程. 个人感觉相比1.x好看了

接口和绑定阶段职责的变化后面的源码部分会体现

有意思的是代码风格也有很大变化:

  • 很多地方都可以看到用Stream流编程和链式的写法, 怪不得要以Java 8为基线
  • 不少地方都使用了Java 8中的函数接口

所以看Spring Boot 2.x/Spring Framework 5的源码需要对函数式编程, lambda, Stream流有一些了解

Tip: 需要注意的是在进行源码调试中, 可能会有些不便

比如Stream的惰性求值特性, 也就是中间操作会在终止条件调用时候才执行

后面源码中能看到return结果中的allMatch()findFirst()就是终止条件中的短路操作!

好在我们可以善用IDE的Evaluate Expression功能来查看

包括调试中, 如果我们想只关注自己配置的属性, 也可以使用IDE的debug condition功能, 很方便!

三. 基本使用

@ConfigurationProperties的使用极为简单, 以下三步

  • 属性配置文件

    1
    2
    3
    4
    5
    custom:
    name: thank
    age: 18
    address[0]: 上海
    address[1]: 北京
  • 属性配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Data
    @Component
    @ConfigurationProperties(prefix = "custom")
    public class CustomProperties {

    private String name;

    private Integer age;

    private List<String> address;
    }
  • 简单验证

    1
    2
    3
    4
    5
    6
    7
    @Test
    public void testBean() {
    ConfigurableApplicationContext applicationContext
    = SpringApplication.run(CacheApplication.class);
    CustomProperties bean = applicationContext.getBean(CustomProperties.class);
    System.out.println(bean.toString());
    }

四. 源码解析

源码重点关注的部分是spring-boot-xx模块的, 涉及到Spring Framework部分的源码则不做太多说明

@ConfigurationProperties

首先来看下@ConfigurationProperties的源码, 我去掉了注释部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationProperties {

@AliasFor("prefix")
String value() default "";

@AliasFor("value")
String prefix() default "";

boolean ignoreInvalidFields() default false;

boolean ignoreUnknownFields() default true;
}

简单说明:

  • value() & prefiex(): 前面示例已经使用过, 用来解析属性文件中的前缀

    可以看到上面打有@AliasFor, 所以你用哪个都没问题

  • ignoreInvalidFields(): 是否忽略无效的字段, 默认为false

    e.g. 有一个age=xxx的配置, 无法将该值绑定到Bean中的Integer age上, 是选择忽略(true)还是抛出异常(false)

  • ignoreUnknownFields(): 是否忽略未知的字段, 默认为true

    e.g. 有一个在Bean中不存在的属性, 是选择忽略(true)还是抛出异常(false)

使用方式可以说简单了, 那么会有两个疑问:

  • Spring是如何发现我们打有@ConfigurationProperties注解的bean的?
  • 是如何将属性资源文件中的值绑定到Bean中的?

ConfigurationPropertiesBindingPostProcessor

@ConfigurationProperties源码上可以看到@see中标明了以下两个类:

1
2
@see ConfigurationPropertiesBindingPostProcessor
@see EnableConfigurationProperties

一个处理类和一个Enable模块中的配置属性绑定模块注解

从字面意思看, 我们应该从这个处理类去寻找答案

进入该处理类ConfigurationPropertiesBindingPostProcessor来看看它的图:

ConfigurationPropertiesBindingPostProcessor.png

相比Spring Boot 1.x版本, 继承结构简化清晰了好多

看到了几个常见接口, 能大概挑一下非重点:

  • ApplicationContextAware: 方便获取上应用下文信息

  • Ordered: 方法getOrder()指定优先级, 可以理解为用来处理处理器的调用顺序

重点是看到了BeanPostProcessor和InitializingBean, 一定能猜到容器bean初始化实例时会调用一个初始方法, 和另外两个前后回调方法

所以重点关注以下三个Override方法:

  • BeanPostProcessor: postProcessBeforeInitialization()
  • InitializingBean: afterPropertiesSet()
  • BeanPostProcessor: postProcessAfterInitialization()

这里与Spring 4.x不同的是在5.x中BeanPostProcessor的两个回调方法声明都使用了default method,

所以在这里postProcessAfterInitialization()也就是后置并未做任何事情, 可以忽略

afterPropertiesSet()

源码如下:

1
2
3
4
5
6
7
8
9
@Override
public void afterPropertiesSet() throws Exception {
this.beanFactoryMetadata = this.applicationContext.getBean(
ConfigurationBeanFactoryMetadata.BEAN_NAME,
ConfigurationBeanFactoryMetadata.class
);
this.configurationPropertiesBinder =
new ConfigurationPropertiesBinder(this.applicationContext, VALIDATOR_BEAN_NAME);
}

可以看到这里做了两个初始化:

  • beanFactoryMetadata: 初始化了工厂Bean的元数据信息

  • configurationPropertiesBinder: 从字面翻译感觉它一定与配置属性绑定有关

    该类在1.x版本是没有的, 但原理类似

此时需要看一眼ConfigurationPropertiesBinder的构造函数

1
2
3
4
5
6
7
8
ConfigurationPropertiesBinder(ApplicationContext applicationContext, String validatorBeanName) {
this.applicationContext = applicationContext;
this.propertySources = new PropertySourcesDeducer(applicationContext).getPropertySources();
this.configurationPropertiesValidator =
getConfigurationPropertiesValidator(applicationContext, validatorBeanName);
this.jsr303Present =
ConfigurationPropertiesJsr303Validator.isJsr303Present(applicationContext);
}

可以看到只是将propertySources, validator封装在内

关于validator

这里看到一个jsr303XXX, 如果不清楚JSR-303, 但是你一定熟悉Hibernate-validator

不要跟ORM产生联系, 其是JSR-303规范的实现

所以也能大概明白它的作用: 即在绑定阶段对@ConfigurationPropertiesBean中的属性做校验

前面的简单示例中没有加valid, 其实用法与我们以前校验Form实体没什么两样, 在配置属性中标记valid注解即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@Component
@Validated
@ConfigurationProperties(value = "custom")
public class CustomProperties {

private String name;

@Min(0) @Max(30)
private Integer age;

@Email(message = "Not Email!")
private String email;
}

需要注意的是, 2.x的@Valid在hibernate-validator已经被标记为@Deprecated, 用javax.validation的吧!

校验部分与配置属性绑定并无太大联系, 在此时不用太关注, 所以后面Validation相关的源码就刻意跳过!

关于propertySources

看到propertySources是不是很眼熟? 以及Environment

这两个spring-core中提供的接口及其实现完成了系统属性, 环境配置, 属性资源配置的解析

可以在propertySources构造完成后打一个断点观察下

如图: 它正确的加载了默认的配置文件application.yml及我们定义的几个属性custom.xxx

propertySource.png

当然, 可以结合@PropertySources自己指定资源文件的位置. 这仍是spring-core的东西, 不说了

postProcessBeforeInitialization()

看完afterPropertiesSet(), 按照顺序, 自然来到了postProcessBeforeInitialization()

1
2
3
4
5
6
7
8
9
10
11
@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;
}

该方法很简单, 就是调用一个getAnnotation()方法, 找出有ConfigurationProperties声明的bean, 执行bind()操作

该部分处理一定是被循环调用的, 具体的调用时机在这里:

AbstractAutowireCapableBeanFactory#initializeBean –> applyBeanPostProcessorsBeforeInitialization()

回到getAnnotation()方法, 它的源码如下:

1
2
3
4
5
6
7
private <A extends Annotation> A getAnnotation(Object bean, String beanName, Class<A> type) {
A annotation = this.beanFactoryMetadata.findFactoryAnnotation(beanName, type);
if (annotation == null) {
annotation = AnnotationUtils.findAnnotation(bean.getClass(), type);
}
return annotation;
}

可以看到会先从工厂bean中开始寻找起, 看到beanFactoryMetadata了吗, 前面提到过

ConfigurationBeanFactoryMetadata中有一个这样的集合:

  • Map<String, FactoryMetadata> beansFactoryMetadata

在回调方法postProcessBeanFactory()中, 完成对配置元数据进行处理, 这里的处理仅是对那个Map做put操作

无论是否工厂方式创建的bean, 最终都会调用工具类AnnotationUtils, 反射得到注解ConfigurationProperties信息

Then return it

基于以上, Spring已经找到了标记@ConfigurationProperties的配置bean了

但是还没有发现属性绑定阶段相关的信息

bind()

接着往下看, 进入bind(), 猜想它一定负责绑定阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}
catch (Exception ex) {
throw new ConfigurationPropertiesBindException(beanName, bean, annotation, ex);
}
}

同样, 略过Validate的部分, 可以看到使用链式设置属性包装出一个Bindable对象, 包含了以下内容:

  • type: 使用Spring提供的泛型操作API获取到该bean的泛型信息

    后面的绑定阶段需要根据属性的类型来判断使用什么方式转换(e.g. String, Collections…)

  • bean: 该bean, 例如我们自己配置的customProperties

  • annotations: 包含注解@ConfigurationProperties@Validated的信息

进入ConfigurationPropertiesBinder#bind(Bindable<?> target)

1
2
3
4
5
6
7
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); // 看这里
}

这里的getBindHandler(), 字面意思是返回一个绑定的处理器

进入源码可以看到, 就是根据注解ConfigurationProperties里前面说过的那两个属性来选择的:

  • IgnoreErrorsBindHandler: 设置ignoreInvalidFields = true时: 忽略无效的属性
  • NoUnboundElementsBindHandler: ignoreUnknownFields = false时: 忽略不存在的的属性
  • IgnoreTopLevelConverterNotFoundBindHandler: 默认情况使用的

除了以上三个, 你可能还会看见ValidationBindHandler, 是对绑定对象执行Valid的处理器, 同样略过

查看这几个处理器的继承结构可以看出, 他们有共同的接口BindHandler

其中的不同主要体现在: onStart, onSuccess, onFailure, onFinish这几个阶段的回调方法上不同的覆盖行为上

getBinder()

接下来看到getBinder(), 还记得之前的afterPropertiesSet()中做了哪些内容的初始化吧

结合它的构造函数, 来看下Binder是怎么构造出来的

1
2
3
4
5
6
7
8
9
10
private Binder getBinder() {
if (this.binder == null) {
this.binder = new Binder(
getConfigurationPropertySources(),
getPropertySourcesPlaceholdersResolver(),
getConversionService(),
getPropertyEditorInitializer());
}
return this.binder;
}

至此, 我们见到了Binder, Bindable, 接下来还会见到BindResult, 这都是Spring Boot 2.x中Binder API中的东西

所以对比过1.x源码你会发现, 这里是最大的变化

文档中说明了: 一个Binder采用一个Bindable并返回一个BindResult

接下来你也会发现, Bindable在整个绑定过程中, 贯穿始终!

回到这个getBinder()方法, 解释下涉及到的几个接口:

  • ConfigurationPropertySources: 这也是Spring Boot 2.x中加入的, 所以相较之前版本, 2.x开始使用ConfigurationPropertySource作为配置属性源进行属性绑定, 而非之前的propertySources

    通过前面携带配置属性信息的propertySources对象而来
    它返回的是一个Iterable container, 通过注释也说明了, 他会展开嵌套的属性

  • ConversionService: 这也是spring-core中的, 很容易想到, 在对资源文件和属性bean之间绑定属性时需要这样一个提供类型转换功能的转换器

大致看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  /**
* Add converters useful for most Spring Boot applications.
* @param registry the registry of converters to add to (must also be castable to
* ConversionService, e.g. being a {@link ConfigurableConversionService})
* @throws ClassCastException if the given ConverterRegistry could not be cast to a
* ConversionService
*/
public static void addApplicationConverters(ConverterRegistry registry) {
addDelimitedStringConverters(registry);
registry.addConverter(new StringToDurationConverter());
registry.addConverter(new DurationToStringConverter());
registry.addConverter(new NumberToDurationConverter());
registry.addConverter(new DurationToNumberConverter());
registry.addConverterFactory(new StringToEnumIgnoringCaseConverterFactory());
}

/**
* Add converters to support delimited strings.
* @param registry the registry of converters to add to (must also be castable to
* ConversionService, e.g. being a {@link ConfigurableConversionService})
* @throws ClassCastException if the given ConverterRegistry could not be cast to a
* ConversionService
*/
public static void addDelimitedStringConverters(ConverterRegistry registry) {
ConversionService service = (ConversionService) registry;
registry.addConverter(new ArrayToDelimitedStringConverter(service));
registry.addConverter(new CollectionToDelimitedStringConverter(service));
registry.addConverter(new DelimitedStringToArrayConverter(service));
registry.addConverter(new DelimitedStringToCollectionConverter(service));
}

converters_debug

最终会调用多个类型转换服务

Binder

接下来, 可以进入Binder#bind()

bind()

里面的构造函数很多, 我们着重来看这个

1
2
3
4
5
6
7
8
public <T> BindResult<T> bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(target, "Target must not be null");
handler = (handler != null) ? handler : BindHandler.DEFAULT;
Context context = new Context();
T bound = bind(name, target, handler, context, false);
return BindResult.of(bound);
}

这里还不是真正的绑定阶段, 但有两个点需要说明:

  • 参数: ConfigurationPropertyName: 是对前面传入的配置前缀prefix进行一些基本校验和处理

    e.g.分割.连接的前缀

    注释中也提醒了: they must be lower-case and must start with an alpha-numeric character

  • 返回值: BindResult: 可以简单的看下它的源码, 里面有of(), get(), isBound(), orElse()

    是不是想起了Guava或Java 8中的Optional<T>, 没错, 他们的作用基本一样, 强制让调用方考虑处理绑定结果为null的情况

  • Context: 在这里是绑定上下文, 由前面说过的BindHandler使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public interface BindContext {

    /**
    * Return the current depth of the binding. Root binding starts with a depth of
    * {@code 0}. Each subsequent property binding increases the depth by {@code 1}.
    * @return the depth of the current binding
    */
    int getDepth();

    /**
    * Return an {@link Iterable} of the {@link ConfigurationPropertySource sources} being
    * used by the {@link Binder}.
    * @return the sources
    */
    Iterable<ConfigurationPropertySource> getSources();

    /**
    * Return the {@link ConfigurationProperty} actually being bound or {@code null} if
    * the property has not yet been determined.
    * @return the configuration property (may be {@code null}).
    */
    ConfigurationProperty getConfigurationProperty();
    }

这里的绑定上下文中就提供前面说过的ConfigurationPropertySource Iterable Container

跟着代码往下, 看Binder中的下一个构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected final <T> T bind(ConfigurationPropertyName name, Bindable<T> target, 
BindHandler handler, Context context, boolean allowRecursiveBinding) {
context.clearConfigurationProperty();
try {
if (!handler.onStart(name, target, context)) {
return null;
}
Object bound = bindObject(name, target, handler, context,allowRecursiveBinding);
return handleBindResult(name, target, handler, context, bound);
}
catch (Exception ex) {
return handleBindError(name, target, handler, context, ex);
}
}

看到onStart了吗, 这里就有一个BindHandler回调接口中的一个方法, 也就是绑定开始但还未完成阶段

接下来在Binder中, 会陆续看到其它几个阶段的方法

再来看返回, 是通过handleBindResult()handleBindError()来处理的.

点开这两个handleBindXXX()能看到在里面进行了onSuccess, onFinish, onFailure的调用, 代码不贴

bindObject()

重点来看bindObject()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context, boolean allowRecursiveBinding) {
ConfigurationProperty property = findProperty(name, context); // Part 1
if (property == null && containsNoDescendantOf(context.streamSources(), name)) { // Part 1
return null;
}
AggregateBinder<?> aggregateBinder = getAggregateBinder(target, context);
if (aggregateBinder != null) {
return bindAggregate(name, target, handler, context, aggregateBinder); // Part 2
}
if (property != null) {
try {
return bindProperty(target, context, property); // Part 2
}
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); // Part 2
}

这里过程比较多, 可以分为2个部分来看

Part 1: 寻找属性和匹配过程

开头的findProperty()和一个匹配方法:containsNoDescendantOf(), 它们的参数都有context

还记得吧? 上面说过了–>提供绑定的上下文信息

这里在debug时候可以利用IDE的Evaluate Expression功能来验证判断的逻辑:

context_streamsource

Part 2: 绑定过程

接下来是bindXXX这三个私有方法:

  • bindAggregate(): 从注释上看出, 主要是负责处理Map, Collections, Array的绑定策略, 及完成多层属性的递归

  • bindProperty(): 是返回属性值的过程(其中包含类型转换)

    例如属性资源文件中配置的name=thank -> java.lang.String thank

  • bindBean(): 会继续调用另外一个私有的重载函数bindBean()

bindBean()

重点看看这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private Object bindBean(ConfigurationPropertyName name, Bindable<?> target, BindHandler handler, Context context, boolean allowRecursiveBinding) {
if (containsNoDescendantOf(context.streamSources(), name)
|| isUnbindableBean(name, target, context)) {
return null;
}
BeanPropertyBinder propertyBinder =
(propertyName, propertyTarget) -> bind(
name.append(propertyName), propertyTarget, handler, context, false);
Class<?> type = target.getType().resolve(Object.class);
if (!allowRecursiveBinding && context.hasBoundBean(type)) {
return null;
}
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);
});
}

里面又调用了上层的Binder.bind(), 递归完成绑定

终止条件是上面提到过的containsNoDescendantOf()和另外一个判断isUnbindableBean()

关注一下最终的返回结果, 是调用了另外一个类: JavaBeanBinderbind方法

JavaBeanBinder

JavaBeanBinder是哪来的呢? 在Binder的静态方法中, 已经定义好了, 并被添加到一个不可变集合中

1
2
3
4
5
6
7
8
9
10
11
12
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);
}

//...
}
bind()

进入JavaBeanBinder#bind()之后看到继续调用了另一个私有的重载方法

1
2
3
4
5
6
7
private <T> boolean bind(BeanPropertyBinder propertyBinder, Bean<T> bean, BeanSupplier<T> beanSupplier) {
boolean bound = false;
for (Map.Entry<String, BeanProperty> entry : bean.getProperties().entrySet()) {
bound |= bind(beanSupplier, propertyBinder, entry.getValue());
}
return bound;
}

里面会迭代该配置bean中的所有属性, 调试模式下随便取一个来看看:

1
2
3
4
5
6
7
8
9
10
0 = {LinkedHashMap$Entry@7470} "name" -> 
key = "name"
value = {JavaBeanBinder$BeanProperty@7475}
name = "name"
declaringClassType = {ResolvableType@7481} "com.example.cache.config.CustomProperties"
getter = {Method@7482} "public java.lang.String com.example.cache.config.CustomProperties.getName()"
setter = {Method@7483} "public void com.example.cache.config.CustomProperties.setName(java.lang.String)"
field = {Field@7484} "private java.lang.String com.example.cache.config.CustomProperties.name"
1 = {LinkedHashMap$Entry@7471} "age" ->
2 = {LinkedHashMap$Entry@7472} "email" ->

没错, 配置bean中: 的属性名, Setter和Getter 该有的都有了

下面来到JavaBeanBinder的最后一个重载方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private <T> boolean bind(BeanSupplier<T> beanSupplier, 
BeanPropertyBinder propertyBinder, BeanProperty property) {
String propertyName = property.getName();
ResolvableType type = property.getType();
Supplier<Object> value = property.getValue(beanSupplier);
Annotation[] annotations = property.getAnnotations();
Object bound = propertyBinder.bindProperty(
propertyName, Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations)
);
if (bound == null) {
return false;
}
if (property.isSettable()) {
property.setValue(beanSupplier, bound);
}
else if (value == null || !bound.equals(value.get())) {
throw new IllegalStateException("No setter found for property: " + property.getName());
}
return true;
}

这里就比较简单了, 分步拿到了配置Bean属性的定义和值:

  • field: 即propertyName, e.g. name

  • 属性类型: type, e.g. java.lang.String

  • getter and setter

    e.g.public void com.example.cache.config.CustomProperties.setName(java.lang.String)

  • 以及调用propertyBinder.bindProperty()拿到了资源属性文件中的属性值bound

    该方法的作用前面也提到过(e.g. thank )

然后调用了属性的setValue()方法: 执行property.setValue(beanSupplier, bound);

至此, 看到了调用属性的set方法, 终于可以放心了!

ConfigurationPropertiesBindingPostProcessor开始到调用setter结束, 完整的调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
bind:85, JavaBeanBinder {org.springframework.boot.context.properties.bind}
bind:62, JavaBeanBinder {org.springframework.boot.context.properties.bind}
bind:54, JavaBeanBinder {org.springframework.boot.context.properties.bind}
lambda$null$5:341, Binder {org.springframework.boot.context.properties.bind}
apply:-1, 1445225850 {org.springframework.boot.context.properties.bind.Binder$$Lambda$267}
accept:193, ReferencePipeline$3$1 {java.util.stream}
tryAdvance:1351, ArrayList$ArrayListSpliterator {java.util}
forEachWithCancel:126, ReferencePipeline {java.util.stream}
copyIntoWithCancel:498, AbstractPipeline {java.util.stream}
copyInto:485, AbstractPipeline {java.util.stream}
wrapAndCopyInto:471, AbstractPipeline {java.util.stream}
evaluateSequential:152, FindOps$FindOp {java.util.stream}
evaluate:234, AbstractPipeline {java.util.stream}
findFirst:464, ReferencePipeline {java.util.stream}
lambda$bindBean$6:342, Binder {org.springframework.boot.context.properties.bind}
get:-1, 2008619427 {org.springframework.boot.context.properties.bind.Binder$$Lambda$266}
withIncreasedDepth:441, Binder$Context {org.springframework.boot.context.properties.bind}
withBean:427, Binder$Context {org.springframework.boot.context.properties.bind}
access$400:381, Binder$Context {org.springframework.boot.context.properties.bind}
bindBean:339, Binder {org.springframework.boot.context.properties.bind}
bindObject:278, Binder {org.springframework.boot.context.properties.bind}
bind:221, Binder {org.springframework.boot.context.properties.bind}
bind:210, Binder {org.springframework.boot.context.properties.bind}
bind:192, Binder {org.springframework.boot.context.properties.bind}
bind:82, ConfigurationPropertiesBinder {org.springframework.boot.context.properties}
bind:107, ConfigurationPropertiesBindingPostProcessor {org.springframework.boot.context.properties}
postProcessBeforeInitialization:93, ConfigurationPropertiesBindingPostProcessor {org.springframework.boot.context.properties}

有兴趣debug的可以按照调用栈的顺序由下至上来参考

五. 总结

总体阅读下来, 相较1.x版本封装度更高, 而且代码风格大量java 8中的写法, 确实给阅读带来了些麻烦, 但是优秀的封装能明显感觉到职责更加明确

顺着源码读下来, 有意略过了很多细节, 例如:

  • Validator结合配置Bean怎么进行属性验证的?
  • 2.x中的宽松绑定约束是在什么地方体现的?
  • 等等

重心还是放在了如何发现我们的属性配置Bean以及如何将属性资源文件中的值绑定到属性配置Bean中这两点

所以只是针对以上两点知其然, 初步窥探了ConfigurationProperties的黑盒

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×