該系列文章是筆者在學習 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
註解的值有三種情況:
-
該 Class 物件實現了
ImportSelector
介面,呼叫它的selectImports(..)
方法獲取需要被處理的 Class 物件的名稱,也就是可以將它們作為一個 Bean 被 Spring IoC 管理- 該 Class 物件實現了
DeferredImportSelector
介面,和上者的執行時機不同,在所有配置類處理完後再執行,且支援@Order
排序
- 該 Class 物件實現了
-
該 Class 物件實現了
ImportBeanDefinitionRegistrar
介面,會呼叫它的registerBeanDefinitions(..)
方法,自定義地往 BeanDefinitionRegistry 註冊中心註冊 BeanDefinition(Bean 的前身) -
該 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 的前身)的過程如下:
-
先註冊兩個內部 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 開始就被廢棄了,忽略掉
-
-
建立一個
ConfigurationPropertiesBeanRegistrar
物件 -
獲取
@EnableConfigurationProperties
註解指定的 Class 類物件們 -
呼叫
ConfigurationPropertiesBeanRegistrar
的register(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);
}
}
過程如下:
-
先獲取這個 Class 類物件的
@ConfigurationProperties
註解 -
呼叫
register(..)
方法,為這個 Class 物件註冊一個 BeanDefinitionvoid 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()); }
- 生成一個 Bean 的名稱,為
@ConfigurationProperties
註解的${prefix}-類全面
,或者類全名
- 如果沒有該名稱的 Bean,則註冊一個
type
型別的 BeanDefinition
- 生成一個 Bean 的名稱,為
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;
}
過程如下:
- 呼叫
ConfigurationPropertiesBean#get(..)
方法,嘗試根據 Bean 解析出一個 ConfigurationPropertiesBean 物件,包含@ConfigurationProperties
註解資訊 - 呼叫
bind(..)
方法,開始獲取指定prefix
字首的屬性值,設定到這個 Bean 中 - 返回屬性填充後的 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);
}
過程如下:
- 找到這個
beanName
對應的工廠方法,例如@Bean
標註的方法就是一個工廠方法,不是@Bean
的話這裡為空 - 呼叫
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);
}
過程如下:
- 找到這個 Bean 上面的
@ConfigurationProperties
註解,如果是@Bean
標註的方法 Bean,也會嘗試從所在的 Class 類上面獲取 - 如果沒有配置
@ConfigurationProperties
註解,則直接返回null
- 找到這個 Bean 上面的
@Validated
註解 - 將
@ConfigurationProperties
、Validated
註解資訊,目標 Bean 以及它的 Class 物件,繫結到一個 Bindable 物件中 - 將
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);
}
過程如下:
-
獲取這個 Bean 的 Bindable 物件(包含了
@ConfigurationProperties
、@Validated
配置資訊和這個 Bean) -
獲取這個 Bean 的
@ConfigurationProperties
註解資訊 -
獲取一個 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; }
-
獲取一個 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; }
-
通過這個 Binder 將指定
prefix
字首的屬性值設定到這個 Bean 中,會藉助 ConversionService 型別轉換器進行型別轉換
整個處理過程主要在第 5
步,有點複雜,藉助於 Binder 繫結器實現的,這裡就不講述了,感興趣的可以去研究研究?
加餐
我們在編寫 application.yml
檔案時,當你輸入一個字母時,IDE 是不是會提示很多選項供你選擇,這個就要歸功於 META-INF/spring-configuration-metadata.json
、META-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 ~
路漫漫其修遠兮,吾將上下而求索