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


原文: Spring Boot 2.0源码解析-配置绑定
date: 2019-04-01 17:39:04


[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的使用极为简单, 以下三步

  • 属性配置文件
custom:
  name: thank
  age: 18
  address[0]: 上海
  address[1]: 北京
  • 属性配置类

    @Data
    @Component
    @ConfigurationProperties(prefix = "custom")
    public class CustomProperties {
    
      private String name;
    
      private Integer age;
    
      private List<String> address;
    }
    
  • 简单验证

    @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的源码, 我去掉了注释部分

@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中标明了以下两个类:

@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()

源码如下:

@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的构造函数

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注解即可:

@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()

@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()方法, 它的源码如下:

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(), 猜想它一定负责绑定阶段

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)

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是怎么构造出来的

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之间绑定属性时需要这样一个提供类型转换功能的转换器

大致看一下:

  /**
 * 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()

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

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使用

    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中的下一个构造函数:

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()

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()

重点看看这个方法

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的静态方法中, 已经定义好了, 并被添加到一个不可变集合中

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()之后看到继续调用了另一个私有的重载方法

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中的所有属性, 调试模式下随便取一个来看看:

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的最后一个重载方法

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结束, 完整的调用栈如下:

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的黑盒

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容