Spring Boot中@ConfigurationProperties註解實現原理源碼解析
- 2019 年 10 月 3 日
- 筆記
0. 開源項目推薦
Pepper Metrics是我與同事開發的一個開源工具(https://github.com/zrbcool/pepper-metrics),其通過收集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邏輯如下:
@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; } 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 { // 在這裡完成了,關鍵的prefix到PropertyBean的值綁定部分,所以各種@ConfigurationProperties註解最終生效就靠這部分程式碼了 this.configurationPropertiesBinder.bind(target); } catch (Exception ex) { throw new ConfigurationPropertiesBindException(beanName, bean, annotation, ex); } }
- PriorityOrdered
- 其實現如下介面:
- 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僅用於提供上面處理中需要的一些元數據資訊
4. 作者其他文章