Spring Boot中@ConfigurationProperties註解實現原理原始碼解析
0. 開源專案推薦
Pepper Metrics是我與同事開發的一個開源工具(github.com/zrbcool/pep…),其通過收集jedis/mybatis/httpservlet/dubbo/motan的執行效能統計,並暴露成prometheus等主流時序資料庫相容資料,通過grafana展示趨勢。其外掛化的架構也非常方便使用者擴充套件並整合其他開源元件。
請大家給個star,同時歡迎大家成為開發者提交PR一起完善專案。
1. 概述
不用說大家都知道Spring Boot非常的方便,快捷,讓開發的同學簡單的幾行程式碼加上幾行配置甚至零配置就能啟動並使用一個專案,專案當中我們也可能經常使用
@ConfigurationProperties將某個Bean與properties配置當中的prefix相繫結,使配置值與定義配置的Bean分離,方便管理。
那麼,這個@ConfigurationProperties是什麼機制,如何實現的呢?我們今天來聊聊這個話題
2. 正文
2.1 從EnableConfigurationProperties說起
為什麼從EnableConfigurationProperties講? Spring Boot專案自身當中大量autoconfigure都是使用EnableConfigurationProperties註解啟用XXXProperties功能,例如spring-data-redis的 這個RedisAutoConfiguration
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class) //看這裡
@Import({ LettuceConnectionConfiguration.class,JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
// ...
}
複製程式碼
而RedisProperties中又帶有註解@ConfigurationProperties(prefix = "spring.redis"),這樣就將spring.redis這個字首的配置項與RedisProperties 這個實體類進行了繫結。
2.2 EnableConfigurationProperties內部實現解析
說完來由,我們就來說說內部實現,先來看看
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {
Class<?>[] value() default {};
}
複製程式碼
@Import(EnableConfigurationPropertiesImportSelector.class)指明瞭這個註解的處理類EnableConfigurationPropertiesImportSelector, 檢視EnableConfigurationPropertiesImportSelector原始碼
class EnableConfigurationPropertiesImportSelector implements ImportSelector {
private static final String[] IMPORTS = { ConfigurationPropertiesBeanRegistrar.class.getName(),ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };
@Override
public String[] selectImports(AnnotationMetadata metadata) {
return IMPORTS;
}
// 省略部分其他方法
}
複製程式碼
我們先看這塊關鍵部分返回了一個IMPORTS陣列,陣列中包含ConfigurationPropertiesBeanRegistrar.class,ConfigurationPropertiesBindingPostProcessorRegistrar.class兩個元素
根據@Import及ImportSelector介面的原理(其原理可以參考同事寫的一篇文章:相親相愛的@Import和@EnableXXX),我們得知spring會初始化上面兩個Registrar到spring容器當中,而兩個Registrar均實現了ImportBeanDefinitionRegistrar介面,
而ImportBeanDefinitionRegistrar會在處理Configuration時觸發呼叫(其原理可以參考文章:這塊找一篇文章),下面我們分別深入兩個Registrar的原始碼:
- ConfigurationPropertiesBeanRegistrar
- ConfigurationPropertiesBindingPostProcessorRegistrar
2.2.1 ConfigurationPropertiesBindingPostProcessorRegistrar
直接看程式碼
public class ConfigurationPropertiesBindingPostProcessorRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,BeanDefinitionRegistry registry) {
if (!registry.containsBeanDefinition(ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) {
registerConfigurationPropertiesBindingPostProcessor(registry);
registerConfigurationBeanFactoryMetadata(registry);
}
}
private void registerConfigurationPropertiesBindingPostProcessor(BeanDefinitionRegistry registry) {
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(ConfigurationPropertiesBindingPostProcessor.BEAN_NAME,definition);
}
private void registerConfigurationBeanFactoryMetadata(BeanDefinitionRegistry registry) {
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(ConfigurationBeanFactoryMetadata.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(ConfigurationBeanFactoryMetadata.BEAN_NAME,definition);
}
}
複製程式碼
可以看到註冊了兩個Bean到spring容器
- ConfigurationPropertiesBindingPostProcessor
- 其實現如下介面:
BeanPostProcessor,PriorityOrdered,ApplicationContextAware,InitializingBean
- PriorityOrdered
Ordered.HIGHEST_PRECEDENCE + 1保證前期執行,且非最先 - ApplicationContextAware
獲取到ApplicationContext設定到內部變數 - InitializingBean
afterPropertiesSet方法在Bean建立時被呼叫,保證內部變數configurationPropertiesBinder被初始化,這個binder類就是使prefix與propertyBean進行值繫結的關鍵工具類 - BeanPostProcessor postProcessBeforeInitialization方法處理具體的bind邏輯如下:
- PriorityOrdered
- 其實現如下介面:
BeanPostProcessor,PriorityOrdered,ApplicationContextAware,InitializingBean
@Override
public Object postProcessBeforeInitialization(Object bean,String beanName) throws BeansException {
ConfigurationProperties annotation = getAnnotation(bean,beanName,ConfigurationProperties.class);
if (annotation != null) {
bind(bean,annotation);
}
return bean;
}
private void bind(Object bean,String beanName,ConfigurationProperties annotation) {
ResolvableType type = getBeanType(bean,beanName);
Validated validated = getAnnotation(bean,Validated.class);
Annotation[] annotations = (validated != null) ? new Annotation[] { annotation,validated }
: new Annotation[] { annotation };
Bindable<?> target = Bindable.of(type).withExistingValue(bean).withAnnotations(annotations);
try {
// 在這裡完成了,關鍵的prefix到PropertyBean的值繫結部分,所以各種@ConfigurationProperties註解最終生效就靠這部分程式碼了
this.configurationPropertiesBinder.bind(target);
}
catch (Exception ex) {
throw new ConfigurationPropertiesBindException(beanName,bean,annotation,ex);
}
}
複製程式碼
- ConfigurationBeanFactoryMetadata
如果某些Bean是通過FactoryBean建立,則該類用於儲存FactoryBean的各種原資訊,用於ConfigurationPropertiesBindingPostProcessor當中的元資料查詢,這裡就不做展開
2.2.2 ConfigurationPropertiesBeanRegistrar
其實ConfigurationPropertiesBeanRegistrar是EnableConfigurationPropertiesImportSelector的靜態內部類,在前面貼程式碼時被省略的部分,上程式碼
public static class ConfigurationPropertiesBeanRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
getTypes(metadata).forEach((type) -> register(registry,(ConfigurableListableBeanFactory) registry,type));
}
private List<Class<?>> getTypes(AnnotationMetadata metadata) {
MultiValueMap<String,Object> attributes = metadata
.getAllAnnotationAttributes(EnableConfigurationProperties.class.getName(),false);
return collectClasses((attributes != null) ? attributes.get("value") : Collections.emptyList());
}
private List<Class<?>> collectClasses(List<?> values) {
return values.stream().flatMap((value) -> Arrays.stream((Object[]) value)).map((o) -> (Class<?>) o)
.filter((type) -> void.class != type).collect(Collectors.toList());
}
private void register(BeanDefinitionRegistry registry,ConfigurableListableBeanFactory beanFactory,Class<?> type) {
String name = getName(type);
if (!containsBeanDefinition(beanFactory,name)) {
registerBeanDefinition(registry,name,type);
}
}
private String getName(Class<?> type) {
ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type,ConfigurationProperties.class);
String prefix = (annotation != null) ? annotation.prefix() : "";
return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName() : type.getName());
}
private void registerBeanDefinition(BeanDefinitionRegistry registry,String name,Class<?> type) {
assertHasAnnotation(type);
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(type);
registry.registerBeanDefinition(name,definition);
}
private void assertHasAnnotation(Class<?> type) {...}
private boolean containsBeanDefinition(ConfigurableListableBeanFactory beanFactory,String name) {...}
}
複製程式碼
邏輯解讀:
主邏輯入口(registerBeanDefinitions)
1 -> getTypes(metadata)拿到標註EnableConfigurationProperties註解的配置值,整理成List<Class<?>>然後逐個處理
2 -> 對每個元素(Class<?>)呼叫register方法處理
3 -> register方法通過類當中的ConfigurationProperties註解的prefix值加類名字作為beanName通過registry.registerBeanDefinition呼叫將Class<?>註冊到registry當中
當標註註解@ConfigurationProperties的XXXProperties的BeanDefinition加入到registry中後,Bean的初始化就交給spring容器,
而這個過程中前面提到的ConfigurationPropertiesBindingPostProcessorRegistrar就完成一系列的後置操作幫助我們完成最終的值繫結
3. 總結
@ConfigurationProperties的整體處理過程,本文已經基本講述完畢,現在大體總結一下: EnableConfigurationProperties完成ConfigurationPropertiesBindingPostProcessorRegistrar及ConfigurationPropertiesBeanRegistrar的引入 其中:
- ConfigurationPropertiesBeanRegistrar完成標註@ConfigurationProperties的類的查詢並組裝成BeanDefinition加入registry
- ConfigurationPropertiesBindingPostProcessorRegistrar完成ConfigurationPropertiesBindingPostProcessor及ConfigurationBeanFactoryMetadata
- ConfigurationPropertiesBindingPostProcessor完成所有標註@ConfigurationProperties的Bean到prefix的properties值繫結
- ConfigurationBeanFactoryMetadata僅用於提供上面處理中需要的一些元資料資訊