精盡Spring Boot原始碼分析 - @ConfigurationProperties 註解的實現

月圓吖發表於2021-07-09

該系列文章是筆者在學習 Spring Boot 過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 Spring Boot 原始碼分析 GitHub 地址 進行閱讀

Spring Boot 版本:2.2.x

最好對 Spring 原始碼有一定的瞭解,可以先檢視我的 《死磕 Spring 之 IoC 篇 - 文章導讀》 系列文章

如果該篇內容對您有幫助,麻煩點選一下“推薦”,也可以關注博主,感激不盡~

該系列其他文章請檢視:《精盡 Spring Boot 原始碼分析 - 文章導讀》

概述

我們的 Spring Boot 應用經常會在 application.yml 配置檔案裡面配置一些自定義的配置,對於不同環境設定不同的值,然後可以通過 @ConfigurationProperties 註解將這些配置作為 Spring Bean 的屬性值進行注入,那麼本文來簡單分析一下這個註解是如何將配置自動設定到 Spring Bean 的。

在開始之前,結合我前面的這麼多 Spring 相關的原始碼分析文章,想必你會知道原理的,無非就是在 Spring Bean 的載入過程的某個階段(大概率是初始化的時候)通過 BeanPostProcessor 解析該註解,並獲取對應的屬性值設定到其中。

先來看看這個註解

@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;
}

使用方式有兩種:

  • @ConfigurationProperties + @Component 註解(一個類)
  • @EnableConfigurationProperties(某個 Bean)+ @ConfigurationProperties 註解(另一個普通類)

第二種方式和第一種原理都是一樣的,不過第二種方式會註冊一個 BeanPostProcessor 用於處理帶有 @ConfigurationProperties 註解的 Spring Bean,同時會將指定的 Class 們解析出 BeanDefinition(Bean 的前身)並註冊,這也就是為什麼第二種不用標註 @Component 註解

那麼第一種方式在哪註冊的 BeanPostProcessor 呢?因為 Spring Boot 有一個 ConfigurationPropertiesAutoConfiguration 自動配置類,如下:

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
public class ConfigurationPropertiesAutoConfiguration { }

很簡單,也是通過 @EnableConfigurationProperties 註解註冊的這個 BeanPostProcessor 物件

這裡有一個疑問,為什麼 @ConfigurationProperties 註解上面不直接加一個 @Component 註解呢?

可能是因為這個註解的作用就是讓 配置類 外部化配置吧

@EnableConfigurationProperties

org.springframework.boot.context.properties.EnableConfigurationProperties,支援將指定的帶有 @ConfigurationProperties 註解的類解析出 BeanDefinition(Bean 的前身)並註冊,同時註冊一個 BeanPostProcessor 去處理帶有 @ConfigurationProperties 註解的 Bean

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesRegistrar.class)
public @interface EnableConfigurationProperties {

	/**
	 * The bean name of the configuration properties validator.
	 * @since 2.2.0
	 */
	String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";

	/**
	 * 指定的 Class 類物件們
	 */
	Class<?>[] value() default {};
}

可以看到這個註解也是通過 @Import 註解來驅動某個功能的,是不是發現 @EnableXxx 驅動註解都是以這樣的方式來實現的

那麼關於 @Import 註解的實現原理我在很多地方都提到過,這裡再提一下,模組驅動註解通常需要結合 @Configuration 註解一起使用,因為需要先被當做一個配置類,然後解析到上面有 @Import 註解後則進行處理,對於 @Import 註解的值有三種情況:

  1. 該 Class 物件實現了 ImportSelector 介面,呼叫它的 selectImports(..) 方法獲取需要被處理的 Class 物件的名稱,也就是可以將它們作為一個 Bean 被 Spring IoC 管理

    • 該 Class 物件實現了 DeferredImportSelector 介面,和上者的執行時機不同,在所有配置類處理完後再執行,且支援 @Order 排序
  2. 該 Class 物件實現了 ImportBeanDefinitionRegistrar 介面,會呼叫它的 registerBeanDefinitions(..) 方法,自定義地往 BeanDefinitionRegistry 註冊中心註冊 BeanDefinition(Bean 的前身)

  3. 該 Class 物件是一個 @Configuration 配置類,會將這個類作為一個 Bean 被 Spring IoC 管理

對於 @Import 註解不熟悉的小夥伴可檢視我前面的 《死磕Spring之IoC篇 - @Bean 等註解的實現原理》 這篇文章

這裡的 @EnableConfigurationProperties 註解,通過 @Import 匯入 EnableConfigurationPropertiesRegistrar 這個類(實現了 ImportBeanDefinitionRegistrar 介面)來實現該功能的,下面會進行分析

EnableConfigurationPropertiesRegistrar

org.springframework.boot.context.properties.EnableConfigurationPropertiesRegistrar,實現了 ImportBeanDefinitionRegistrar 介面,是 @EnableConfigurationProperties 註解的核心類

class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegistrar {

	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
		// <1> 先註冊兩個內部 Bean
		registerInfrastructureBeans(registry);
		// <2> 建立一個 ConfigurationPropertiesBeanRegistrar 物件
		ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry);
		// <3> 獲取 `@EnableConfigurationProperties` 註解指定的 Class 類物件們
		// <4> 依次註冊指定的 Class 類對應的 BeanDefinition
		// 這樣一來這個 Class 不用標註 `@Component` 就可以注入這個配置屬性物件了
		getTypes(metadata).forEach(beanRegistrar::register);
	}

	private Set<Class<?>> getTypes(AnnotationMetadata metadata) {
		return metadata.getAnnotations().stream(EnableConfigurationProperties.class)
				.flatMap((annotation) -> Arrays.stream(annotation.getClassArray(MergedAnnotation.VALUE)))
				.filter((type) -> void.class != type).collect(Collectors.toSet());
	}

	/**
	 * 可參考 ConfigurationPropertiesAutoConfiguration 自動配置類
	 */
	@SuppressWarnings("deprecation")
	static void registerInfrastructureBeans(BeanDefinitionRegistry registry) {
		// 註冊一個 ConfigurationPropertiesBindingPostProcessor 型別的 BeanDefinition(內部角色),如果不存在的話
		// 同時也會註冊 ConfigurationPropertiesBinder 和 ConfigurationPropertiesBinder.Factory 兩個 Bean,如果不存在的話
		ConfigurationPropertiesBindingPostProcessor.register(registry);
		// 註冊一個 ConfigurationBeanFactoryMetadata 型別的 BeanDefinition(內部角色)
		// 這個 Bean 從 Spring 2.2.0 開始就被廢棄了
		ConfigurationBeanFactoryMetadata.register(registry);
	}
}

註冊 BeanDefinition(Bean 的前身)的過程如下:

  1. 先註冊兩個內部 Bean

    • 註冊一個 ConfigurationPropertiesBindingPostProcessor 型別的 BeanDefinition(內部角色),如果不存在的話

      public static void register(BeanDefinitionRegistry registry) {
          Assert.notNull(registry, "Registry must not be null");
          // 註冊 ConfigurationPropertiesBindingPostProcessor 型別的 BeanDefinition(內部角色)
          if (!registry.containsBeanDefinition(BEAN_NAME)) {
              GenericBeanDefinition definition = new GenericBeanDefinition();
              definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class);
              definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
              registry.registerBeanDefinition(BEAN_NAME, definition);
          }
          // 註冊 ConfigurationPropertiesBinder 和 ConfigurationPropertiesBinder.Factory 兩個 BeanDefinition(內部角色)
          ConfigurationPropertiesBinder.register(registry);
      }
      
    • 註冊一個 ConfigurationBeanFactoryMetadata 型別的 BeanDefinition(內部角色),從 Spring 2.2.0 開始就被廢棄了,忽略掉

  2. 建立一個 ConfigurationPropertiesBeanRegistrar 物件

  3. 獲取 @EnableConfigurationProperties 註解指定的 Class 類物件們

  4. 呼叫 ConfigurationPropertiesBeanRegistrarregister(Class<?> type) 方法,依次註冊指定的 Class 類對應的 BeanDefinition,這樣一來這個 Class 不用標註 @Component 就可以注入這個配置屬性物件了

ConfigurationPropertiesBeanRegistrar

org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrar,是 EnableConfigurationPropertiesRegistrar 的輔助類

final class ConfigurationPropertiesBeanRegistrar {

	private final BeanDefinitionRegistry registry;

	private final BeanFactory beanFactory;

	ConfigurationPropertiesBeanRegistrar(BeanDefinitionRegistry registry) {
		this.registry = registry;
		this.beanFactory = (BeanFactory) this.registry;
	}

	void register(Class<?> type) {
		// <1> 先獲取這個 Class 類物件的 `@ConfigurationProperties` 註解
		MergedAnnotation<ConfigurationProperties> annotation = MergedAnnotations
				.from(type, SearchStrategy.TYPE_HIERARCHY).get(ConfigurationProperties.class);
		// <2> 為這個 Class 物件註冊一個 BeanDefinition
		register(type, annotation);
	}
}

過程如下:

  1. 先獲取這個 Class 類物件的 @ConfigurationProperties 註解

  2. 呼叫 register(..) 方法,為這個 Class 物件註冊一個 BeanDefinition

    void register(Class<?> type, MergedAnnotation<ConfigurationProperties> annotation) {
        // <1> 生成一個 Bean 的名稱,為 `@ConfigurationProperties` 註解的 `${prefix}-類全面`,或者`類全名`
        String name = getName(type, annotation);
        if (!containsBeanDefinition(name)) {
            // <2> 如果沒有該名稱的 Bean,則註冊一個 `type` 型別的 BeanDefinition
            registerBeanDefinition(name, type, annotation);
        }
    }
    
    private String getName(Class<?> type, MergedAnnotation<ConfigurationProperties> annotation) {
        String prefix = annotation.isPresent() ? annotation.getString("prefix") : "";
        return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName() : type.getName());
    }
    
    1. 生成一個 Bean 的名稱,為 @ConfigurationProperties 註解的 ${prefix}-類全面,或者類全名
    2. 如果沒有該名稱的 Bean,則註冊一個 type 型別的 BeanDefinition

registerBeanDefinition 方法

註冊帶有 @ConfigurationProperties 註解的 Class 物件

private void registerBeanDefinition(String beanName, Class<?> type,
        MergedAnnotation<ConfigurationProperties> annotation) {
    // 這個 Class 物件必須有 `@ConfigurationProperties` 註解
    Assert.state(annotation.isPresent(), () -> "No " + ConfigurationProperties.class.getSimpleName()
            + " annotation found on  '" + type.getName() + "'.");
    // 註冊一個 `beanClass` 為 `type` 的 GenericBeanDefinition
    this.registry.registerBeanDefinition(beanName, createBeanDefinition(beanName, type));
}

private BeanDefinition createBeanDefinition(String beanName, Class<?> type) {
    if (BindMethod.forType(type) == BindMethod.VALUE_OBJECT) {
        return new ConfigurationPropertiesValueObjectBeanDefinition(this.beanFactory, beanName, type);
    }
    // 建立一個 GenericBeanDefinition 物件,設定 Class 為 `type`
    GenericBeanDefinition definition = new GenericBeanDefinition();
    definition.setBeanClass(type);
    return definition;
}

邏輯比較簡單,就是將這個 @ConfigurationProperties 註解的 Class 物件生成一個 BeanDefinition 並註冊

ConfigurationPropertiesBindingPostProcessor

org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor,將配置繫結到 @ConfigurationProperties 註解的配置類中

public class ConfigurationPropertiesBindingPostProcessor
		implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean {

	public static final String BEAN_NAME = ConfigurationPropertiesBindingPostProcessor.class.getName();

	/**
	 * The bean name of the configuration properties validator.
	 * @deprecated since 2.2.0 in favor of
	 * {@link EnableConfigurationProperties#VALIDATOR_BEAN_NAME}
	 */
	@Deprecated
	public static final String VALIDATOR_BEAN_NAME = EnableConfigurationProperties.VALIDATOR_BEAN_NAME;

	/** Spring 應用上下文 */
	private ApplicationContext applicationContext;

	/** BeanDefinition 註冊中心 */
	private BeanDefinitionRegistry registry;

	/** 屬性繫結器 */
	private ConfigurationPropertiesBinder binder;
    
	/**
	 * Create a new {@link ConfigurationPropertiesBindingPostProcessor} instance.
	 * @deprecated since 2.2.0 in favor of
	 * {@link EnableConfigurationProperties @EnableConfigurationProperties} or
	 * {@link ConfigurationPropertiesBindingPostProcessor#register(BeanDefinitionRegistry)}
	 */
	@Deprecated
	public ConfigurationPropertiesBindingPostProcessor() {
	}
}

setApplicationContext 方法

ApplicationContextAware 的回撥

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    // Aware 介面回撥,獲取 Spring 應用上下文
    this.applicationContext = applicationContext;
}

afterPropertiesSet 方法

InitializingBean 初始化方法

/**
 * 初始化當前 Bean
 */
@Override
public void afterPropertiesSet() throws Exception {
    // We can't use constructor injection of the application context because
    // it causes eager factory bean initialization
    // 從 Spring 應用上下文獲取 BeanDefinition 註冊中心
    this.registry = (BeanDefinitionRegistry) this.applicationContext.getAutowireCapableBeanFactory();
    // 獲取 ConfigurationPropertiesBinder 這個 Bean,在這個類的 `register` 方法中註冊了哦
    this.binder = ConfigurationPropertiesBinder.get(this.applicationContext);
}

getOrder 方法

PriorityOrdered 優先順序

// 次於最高優先順序
@Override
public int getOrder() {
    return Ordered.HIGHEST_PRECEDENCE + 1;
}

1. postProcessBeforeInitialization 方法

BeanPostProcessor 的初始化前置操作

/**
 * 在 Bean 的初始化前會呼叫這個方法
 * 參考 {@link AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsBeforeInitialization(Object, String)}
 */
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    // <1> 先嚐試根據 Bean 解析出一個 ConfigurationPropertiesBean 物件,包含 `@ConfigurationProperties` 註解資訊
    // <2> 然後開始獲取指定 `prefix` 字首的屬性值,設定到這個 Bean 中
    bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
    // <3> 返回屬性填充後的 Bean
    return bean;
}

過程如下:

  1. 呼叫 ConfigurationPropertiesBean#get(..) 方法,嘗試根據 Bean 解析出一個 ConfigurationPropertiesBean 物件,包含 @ConfigurationProperties 註解資訊
  2. 呼叫 bind(..) 方法,開始獲取指定 prefix 字首的屬性值,設定到這個 Bean 中
  3. 返回屬性填充後的 Bean

4. bind 方法

private void bind(ConfigurationPropertiesBean bean) {
    // <1> 如果這個 `bean` 為空,或者已經處理過,則直接返回
    if (bean == null || hasBoundValueObject(bean.getName())) {
        return;
    }
    // <2> 對 `@ConstructorBinding` 的校驗,如果使用該註解但是沒有找到合適的構造器,那麼在這裡丟擲異常
    Assert.state(bean.getBindMethod() == BindMethod.JAVA_BEAN, "Cannot bind @ConfigurationProperties for bean '"
            + bean.getName() + "'. Ensure that @ConstructorBinding has not been applied to regular bean");
    try {
        // <3> 通過 Binder 將指定 `prefix` 字首的屬性值設定到這個 Bean 中,會藉助 Conversion 型別轉換器進行型別轉換,過程複雜,沒看懂...
        this.binder.bind(bean);
    }
    catch (Exception ex) {
        throw new ConfigurationPropertiesBindException(bean, ex);
    }
}

可以看到最後是通過 ConfigurationPropertiesBinder 屬性繫結器來將屬性繫結到 bean 中的

ConfigurationPropertiesBean

org.springframework.boot.context.properties.ConfigurationPropertiesBean,是 @ConfigurationProperties 註解對應的 Bean 的封裝,用於將對應的屬性值繫結到這個 Bean 中

public final class ConfigurationPropertiesBean {
	/**
	 * Bean 的名稱
	 */
	private final String name;
	/**
	 * Bean 的例項物件
	 */
	private final Object instance;
	/**
	 * Bean 的 `@ConfigurationProperties` 註解
	 */
	private final ConfigurationProperties annotation;
	/**
	 * `@Bean` 對應的方法資源物件,包括例項物件和註解資訊
	 */
	private final Bindable<?> bindTarget;
	/**
	 * `@Bean` 對應的方法
	 */
	private final BindMethod bindMethod;

	private ConfigurationPropertiesBean(String name, Object instance, ConfigurationProperties annotation,
			Bindable<?> bindTarget) {
		this.name = name;
		this.instance = instance;
		this.annotation = annotation;
		this.bindTarget = bindTarget;
		this.bindMethod = BindMethod.forType(bindTarget.getType().resolve());
	}
}

參考上面的註釋檢視每個屬性的描述

2. get 方法

獲取某個 @ConfigurationProperties 註解對應的 Bean 的 ConfigurationPropertiesBean

public static ConfigurationPropertiesBean get(ApplicationContext applicationContext, Object bean, String beanName) {
    // <1> 找到這個 `beanName` 對應的工廠方法,例如 `@Bean` 標註的方法就是一個工廠方法,不是 `@Bean` 的話這裡為空
    Method factoryMethod = findFactoryMethod(applicationContext, beanName);
    // <2> 建立一個 ConfigurationPropertiesBean 物件,包含了這個 Bean 的 `@ConfigurationProperties` 註解資訊
    return create(beanName, bean, bean.getClass(), factoryMethod);
}

過程如下:

  1. 找到這個 beanName 對應的工廠方法,例如 @Bean 標註的方法就是一個工廠方法,不是 @Bean 的話這裡為空
  2. 呼叫 create(..) 方法,建立一個 ConfigurationPropertiesBean 物件,包含了這個 Bean 的 @ConfigurationProperties 註解資訊

3. create 方法

private static ConfigurationPropertiesBean create(String name, Object instance, Class<?> type, Method factory) {
    // <1> 找到這個 Bean 上面的 `@ConfigurationProperties` 註解
    // 如果是 `@Bean` 標註的方法 Bean,也會嘗試從所在的 Class 類上面獲取
    ConfigurationProperties annotation = findAnnotation(instance, type, factory, ConfigurationProperties.class);
    // <2> 如果沒有配置 `@ConfigurationProperties` 註解,則直接返回 `null`
    if (annotation == null) {
        return null;
    }
    // <3> 找到這個 Bean 上面的 `@Validated` 註解
    Validated validated = findAnnotation(instance, type, factory, Validated.class);
    // <4> 將 `@ConfigurationProperties`、`Validated`註解資訊,目標 Bean 以及它的 Class 物件,繫結到一個 Bindable 物件中
    Annotation[] annotations = (validated != null) ? new Annotation[] { annotation, validated }
            : new Annotation[] { annotation };
    ResolvableType bindType = (factory != null) ? ResolvableType.forMethodReturnType(factory)
            : ResolvableType.forClass(type);

    Bindable<Object> bindTarget = Bindable.of(bindType).withAnnotations(annotations);
    if (instance != null) {
        bindTarget = bindTarget.withExistingValue(instance);
    }
    // <5> 將 `beanName`、目標 Bean、`ConfigurationProperties` 註解、第 `4` 步的 Bindable 物件封裝到一個 ConfigurationPropertiesBean 物件中
    return new ConfigurationPropertiesBean(name, instance, annotation, bindTarget);
}

過程如下:

  1. 找到這個 Bean 上面的 @ConfigurationProperties 註解,如果是 @Bean 標註的方法 Bean,也會嘗試從所在的 Class 類上面獲取
  2. 如果沒有配置 @ConfigurationProperties 註解,則直接返回 null
  3. 找到這個 Bean 上面的 @Validated 註解
  4. @ConfigurationPropertiesValidated註解資訊,目標 Bean 以及它的 Class 物件,繫結到一個 Bindable 物件中
  5. beanName、目標 Bean、ConfigurationProperties 註解、第 4 步的 Bindable 物件封裝到一個 ConfigurationPropertiesBean 物件中

ConfigurationPropertiesBinder

org.springframework.boot.context.properties.ConfigurationPropertiesBinder,對 ConfigurationPropertiesBean 進行屬性繫結

5. bind 方法

對 ConfigurationPropertiesBean 進行屬性繫結,如下:

BindResult<?> bind(ConfigurationPropertiesBean propertiesBean) {
    // <1> 獲取這個 Bean 的 Bindable 物件(包含了 `@ConfigurationProperties`、`@Validated` 配置資訊和這個 Bean)
    Bindable<?> target = propertiesBean.asBindTarget();
    // <2> 獲取這個 Bean 的 `@ConfigurationProperties` 註解資訊
    ConfigurationProperties annotation = propertiesBean.getAnnotation();
    // <3> 獲取一個 BindHandler 繫結處理器
    BindHandler bindHandler = getBindHandler(target, annotation);
    // <4> 獲取一個 Binder 物件,包含了 Spring 應用上下文的所有配置資訊,佔位符處理器,型別轉換器
    // <5> 通過這個 Binder 將指定 `prefix` 字首的屬性值設定到這個 Bean 中,會藉助 Conversion 型別轉換器進行型別轉換,過程複雜,沒看懂...
    return getBinder().bind(annotation.prefix(), target, bindHandler);
}

過程如下:

  1. 獲取這個 Bean 的 Bindable 物件(包含了 @ConfigurationProperties@Validated 配置資訊和這個 Bean)

  2. 獲取這個 Bean 的 @ConfigurationProperties 註解資訊

  3. 獲取一個 BindHandler 繫結處理器

    private <T> BindHandler getBindHandler(Bindable<T> target, ConfigurationProperties annotation) {
        // <1> 獲取幾個 Validator 校驗器
        List<Validator> validators = getValidators(target);
        // <2> 建立一個最頂層的 BindHandler
        BindHandler handler = new IgnoreTopLevelConverterNotFoundBindHandler();
        // <3> 如果忽略無效的欄位(預設為 `false`)
        if (annotation.ignoreInvalidFields()) {
            handler = new IgnoreErrorsBindHandler(handler);
        }
        // <4> 如果不忽略不知道的欄位(預設也不會進入這裡)
        if (!annotation.ignoreUnknownFields()) {
            UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter();
            handler = new NoUnboundElementsBindHandler(handler, filter);
        }
        // <5> 如果檢驗器不為空,則將其封裝成 ValidationBindHandler 物件,裡面儲存了這幾個 Validator
        if (!validators.isEmpty()) {
            handler = new ValidationBindHandler(handler, validators.toArray(new Validator[0]));
        }
        // <6> 獲取 ConfigurationPropertiesBindHandlerAdvisor 對 `handler` 應用,暫時忽略
        for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) {
            handler = advisor.apply(handler);
        }
        // <7> 返回這個 `handler` 配置繫結處理器
        return handler;
    }
    
    private List<Validator> getValidators(Bindable<?> target) {
        List<Validator> validators = new ArrayList<>(3);
        if (this.configurationPropertiesValidator != null) {
            validators.add(this.configurationPropertiesValidator);
        }
        if (this.jsr303Present && target.getAnnotation(Validated.class) != null) {
            validators.add(getJsr303Validator());
        }
        if (target.getValue() != null && target.getValue().get() instanceof Validator) {
            validators.add((Validator) target.getValue().get());
        }
        return validators;
    }
    
  4. 獲取一個 Binder 物件,包含了 Spring 應用上下文的所有配置資訊,佔位符處理器,型別轉換器

    private Binder getBinder() {
        if (this.binder == null) {
            this.binder = new Binder(getConfigurationPropertySources(), // Spring 應用的 PropertySource 屬性資源
                    getPropertySourcesPlaceholdersResolver(), // 佔位符處理器
                    getConversionService(),  // 型別轉換器
                    getPropertyEditorInitializer(), // 屬性編輯器
                    null,
                    ConfigurationPropertiesBindConstructorProvider.INSTANCE);
        }
        return this.binder;
    }
    
  5. 通過這個 Binder 將指定 prefix 字首的屬性值設定到這個 Bean 中,會藉助 ConversionService 型別轉換器進行型別轉換

整個處理過程主要在第 5 步,有點複雜,藉助於 Binder 繫結器實現的,這裡就不講述了,感興趣的可以去研究研究?

加餐

我們在編寫 application.yml 檔案時,當你輸入一個字母時,IDE 是不是會提示很多選項供你選擇,這個就要歸功於 META-INF/spring-configuration-metadata.jsonMETA-INF/additional-spring-configuration-metadata.json 兩個檔案,在這兩個檔案裡面可以定義你需要的配置的資訊,例如 Spring Boot 提供的:

{
  "groups": [
    {
      "name": "logging",
      "type": "org.springframework.boot.context.logging.LoggingApplicationListener"
    }
  ],
  "properties": [
    {
      "name": "logging.config",
      "type": "java.lang.String",
      "description": "Location of the logging configuration file. For instance, `classpath:logback.xml` for Logback.",
      "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener"
    },
    {
      "name": "spring.application.name",
      "type": "java.lang.String",
      "description": "Application name.",
      "sourceType": "org.springframework.boot.context.ContextIdApplicationContextInitializer"
    },
    {
      "name": "spring.profiles",
      "type": "java.util.List<java.lang.String>",
      "description": "Comma-separated list of profile expressions that at least one should match for the document to be included.",
      "sourceType": "org.springframework.boot.context.config.ConfigFileApplicationListener"
    },
    {
      "name": "spring.profiles.active",
      "type": "java.util.List<java.lang.String>",
      "description": "Comma-separated list of active profiles. Can be overridden by a command line switch.",
      "sourceType": "org.springframework.boot.context.config.ConfigFileApplicationListener"
    }
  ],
  "hints": [
    {
      "name": "logging.level.values",
      "values": [
        {
          "value": "trace"
        },
        {
          "value": "debug"
        },
        {
          "value": "info"
        },
        {
          "value": "warn"
        },
        {
          "value": "error"
        },
        {
          "value": "fatal"
        },
        {
          "value": "off"
        }
      ],
      "providers": [
        {
          "name": "any"
        }
      ]
    }
  ]
}

上面僅列出了部分內容,可以看到定義了每個配置的名稱、型別、描述和來源,同時可以定義每個配置能夠輸入的值,這樣一來,我們就能夠在 IDE 中快速的輸入需要的配置項。

這個檔案是通過 Spring Boot 提供的 spring-boot-configuration-processor 工具模組生成的,藉助於 SPI 機制配置了一個 ConfigurationMetadataAnnotationProcessor 註解處理器,它繼承 javax.annotation.processing.AbstractProcessor 抽象類。也就是說這個處理器在編譯階段,會解析每個 @ConfigurationProperties 註解標註的類,將這些類對應的一些配置項(key)的資訊儲存在 META-INF/spring-configuration-metadata.json 檔案中,例如型別、預設值,來幫助你編寫 application.yml 的時候會有相關提示。

而且,當我們使用 @ConfigurationProperties 註解後,IDE 會提示我們引入這個工具類:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

關於這部分內容可參考 Spring Boot 官方文件

總結

本文分析了 Spring Boot 中的 @ConfigurationProperties 註解的實現過程,原理就是通過註冊的一個 BeanPostProcessor 會在載入 Spring Bean 初始化的時候進行前置處理,解析出 @ConfigurationProperties 註解相關資訊,然後找到對應字首的屬性值繫結到這個 Bean 中。

使用這個註解有兩種方式:

  • @ConfigurationProperties + @Component 註解(一個類)
  • @EnableConfigurationProperties(某個 Bean)+ @ConfigurationProperties 註解(另一個普通類)

關於 @EnableConfigurationProperties 註解的處理過程也比較簡單,通過 @Import 註解的方法,註冊一個 BeanPostProcessor 用於處理 @ConfigurationProperties 註解的 Bean,同時會將指定的帶有 @ConfigurationProperties 註解的 Class 物件註冊到 Spring IoC 容器中,這也就是為什麼不用加 @Component 註解的原因

關於上面第一種方式是通過一個 ConfigurationPropertiesAutoConfiguration 自動配置類藉助 @EnableConfigurationProperties 註解註冊的這個 BeanPostProcessor 去處理 @ConfigurationProperties 註解的 Bean

學習完 Spring Boot 原始碼後,個人覺得是非常有幫助的,讓自己能夠清楚的瞭解 Sprig Boot 應用的執行原理,在處理問題以及調優等方面會更加輕鬆。另外,熟悉 Spring Boot 的自動配置功能後,編寫一個 Spring Boot Starter 可以說是輕而易舉。

至此,關於 Spirng 和 Spring Boot 兩個流行的基礎框架的原始碼已經全部分析完了,接下來筆者要開始學習其他的東西了,例如 MySQL、Dubbo 和 Spring Cloud,敬請期待吧,加油?‍?

這裡提一句,Apache Dubbo 3.0 正式釋出,全面擁抱雲原生,先深入學習一下 Dubbo ~

路漫漫其修遠兮,吾將上下而求索

相關文章