3、@ConfigurationProperties實現原理與實戰
在SpringBoot中,當需要獲取到配置文件數據時,除了可以用Spring自帶的@Value註解外,SpringBoot提供了一種更加方便的方式:@ConfigurationProperties。只要在bean上添加上這個註解,指定好配置文件的前綴,那麼對應的配置文件數據就會自動填充到bean中。舉個栗子,現在有如下配置:
myconfig.name=test
myconfig.age=22
myconfig.desc=這是我的測試描述
添加對應的配置類,並添加上註解@ConfigurationProperties,指定前綴爲myconfig
@Component
@ConfigurationProperties(prefix = "myconfig")
public class MyConfig {
private String name;
private Integer age;
private String desc;
//get/set 略
@Override
public String toString() {
return "MyConfig [name=" + name + ", age=" + age + ", desc=" + desc + "]";
}
}
添加使用:
public static void main(String[] args) throws Exception {
SpringApplication springApplication = new SpringApplication(Application.class);
// 非web環境
springApplication.setWebEnvironment(false);
ConfigurableApplicationContext application = springApplication.run(args);
MyConfig config = application.getBean(MyConfig.class);
log.info(config.toString());
application.close();
}
可以看到輸出log
com.cml.chat.lesson.lesson3.Application - MyConfig [name=test, age=22, desc=這是我的測試描述]
對應的屬性都注入了配置中的值,而且不需要其他操作。是不是非常神奇?那麼下面來剖析下@ConfigurationProperties到底做了啥?
首先進入@ConfigurationProperties源碼中,可以看到如下注釋提示:
See Also 中給我們推薦了ConfigurationPropertiesBindingPostProcessor,EnableConfigurationProperties兩個類,EnableConfigurationProperties先放到一邊,因爲後面的文章中會詳解EnableXX框架的實現原理,這裏就先略過。那麼重點來看看ConfigurationPropertiesBindingPostProcessor,光看類名是不是很親切?不知上篇文章中講的BeanPostProcessor還有印象沒,沒有的話趕緊回頭看看哦。
ConfigurationPropertiesBindingPostProcessor
一看就知道和BeanPostProcessor有扯不開的關係,進入源碼可以看到,該類實現的BeanPostProcessor和其他多個接口:
public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,
BeanFactoryAware, EnvironmentAware, ApplicationContextAware, InitializingBean,
DisposableBean, ApplicationListener<ContextRefreshedEvent>, PriorityOrdered
這裏是不是非常直觀,光看類的繼承關係就可以猜出大概這個類做了什麼。
BeanFactoryAware,EnvironmentAware,ApplicationContextAware是Spring提供的獲取Spring上下文中指定對象的方法而且優先於BeanPostProcessor調用,至於如何工作的後面的文章會進行詳解,這裏只要先知道下作用就可以了。
此類同樣實現了InitializingBean接口,從上篇文章中已經知道了InitializingBean是在BeanPostProcessor.postProcessBeforeInitialization之後調用,那麼postProcessBeforeInitialization目前就是我們需要關注的重要入口方法。
先上源碼看看:
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
//直接通過查找添加了ConfigurationProperties註解的的類
ConfigurationProperties annotation = AnnotationUtils
.findAnnotation(bean.getClass(), ConfigurationProperties.class);
if (annotation != null) {
postProcessBeforeInitialization(bean, beanName, annotation);
}
//查找使用工廠bean中是否有ConfigurationProperties註解
annotation = this.beans.findFactoryAnnotation(beanName,
ConfigurationProperties.class);
if (annotation != null) {
postProcessBeforeInitialization(bean, beanName, annotation);
}
return bean;
}
private void postProcessBeforeInitialization(Object bean, String beanName,
ConfigurationProperties annotation) {
Object target = bean;
PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>(
target);
factory.setPropertySources(this.propertySources);
factory.setValidator(determineValidator(bean));
// If no explicit conversion service is provided we add one so that (at least)
// comma-separated arrays of convertibles can be bound automatically
factory.setConversionService(this.conversionService == null
? getDefaultConversionService() : this.conversionService);
if (annotation != null) {
factory.setIgnoreInvalidFields(annotation.ignoreInvalidFields());
factory.setIgnoreUnknownFields(annotation.ignoreUnknownFields());
factory.setExceptionIfInvalid(annotation.exceptionIfInvalid());
factory.setIgnoreNestedProperties(annotation.ignoreNestedProperties());
if (StringUtils.hasLength(annotation.prefix())) {
factory.setTargetName(annotation.prefix());
}
}
try {
factory.bindPropertiesToTarget();
}
catch (Exception ex) {
String targetClass = ClassUtils.getShortName(target.getClass());
throw new BeanCreationException(beanName, "Could not bind properties to "
+ targetClass + " (" + getAnnotationDetails(annotation) + ")", ex);
}
}
在postProcessBeforeInitialization方法中,會先去找所有添加了ConfigurationProperties註解的類對象,找到後調用postProcessBeforeInitialization進行屬性數據裝配。
那麼現在可以將實現拆分成如何尋找和如何裝配兩部分來說明,首先先看下如何查找到ConfigurationProperties註解類。
查找ConfigurationProperties
在postProcessBeforeInitialization方法中先通過AnnotationUtils查找類是否添加了@ConfigurationProperties註解,然後再通過 this.beans.findFactoryAnnotation(beanName,
ConfigurationProperties.class);繼續查找,下面詳解這兩步查找的作用。
- AnnotationUtils
AnnotationUtils.findAnnotation(bean.getClass(),ConfigurationProperties.class);這個是Spring中常用的工具類了,通過反射的方式獲取類上的註解,如果此類添加了註解@ConfigurationProperties那麼這個方法會返回這個註解對象和類上配置的註解屬性。
- beans.findFactoryAnnotation
這裏的beans是ConfigurationBeanFactoryMetaData對象。在Spring中,可以以工廠bean的方式添加bean,這個類的作用就是在工程bean中找到@ConfigurationProperties註解。下面分析下實現過程:
ConfigurationBeanFactoryMetaData
public class ConfigurationBeanFactoryMetaData implements BeanFactoryPostProcessor {
private ConfigurableListableBeanFactory beanFactory;
private Map<String, MetaData> beans = new HashMap<String, MetaData>();
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
this.beanFactory = beanFactory;
//迭代所有的bean定義,找出那些是工廠bean的對象添加到beans中
for (String name : beanFactory.getBeanDefinitionNames()) {
BeanDefinition definition = beanFactory.getBeanDefinition(name);
String method = definition.getFactoryMethodName();
String bean = definition.getFactoryBeanName();
if (method != null && bean != null) {
this.beans.put(name, new MetaData(bean, method));
}
}
}
public <A extends Annotation> Map<String, Object> getBeansWithFactoryAnnotation(
Class<A> type) {
Map<String, Object> result = new HashMap<String, Object>();
for (String name : this.beans.keySet()) {
if (findFactoryAnnotation(name, type) != null) {
result.put(name, this.beanFactory.getBean(name));
}
}
return result;
}
public <A extends Annotation> A findFactoryAnnotation(String beanName,
Class<A> type) {
Method method = findFactoryMethod(beanName);
return (method == null ? null : AnnotationUtils.findAnnotation(method, type));
}
//略...
private static class MetaData {
private String bean;
private String method;
//構造方法和其他方法略...
}
}
通過以上代碼可以得出ConfigurationBeanFactoryMetaData的工作機制,通過實現BeanFactoryPostProcessor,在回調方法postProcessBeanFactory中,查找出所有通過工廠bean實現的對象,並將其保存到beans map中,通過方法findFactoryAnnotation可以查詢到工廠bean中是否添加了對應的註解。那麼這裏的功能就是查找工廠bean中有添加@ConfigurationProperties註解的類了。
屬性值注入
通過上述步驟,已經確認了當前傳入的bean是否添加了@ConfigurationProperties註解。如果添加了則下一步就需要進行屬性值注入了,核心代碼在方法postProcessBeforeInitialization中:
private void postProcessBeforeInitialization(Object bean, String beanName,
ConfigurationProperties annotation) {
Object target = bean;
PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>(
target);
//重點,這裏設置數據來源
factory.setPropertySources(this.propertySources);
factory.setValidator(determineValidator(bean));
//設置轉換器
factory.setConversionService(this.conversionService == null
? getDefaultConversionService() : this.conversionService);
if (annotation != null) {
//將annotation中配置的屬性配置到factory中
}
try {
//這裏是核心,綁定屬性值到對象中
factory.bindPropertiesToTarget();
}
catch (Exception ex) {
//拋出異常
}
}
繼續跟進factory.bindPropertiesToTarget方法,在bindPropertiesToTarget方法中,調用的是doBindPropertiesToTarget方法:
private void doBindPropertiesToTarget() throws BindException {
RelaxedDataBinder dataBinder
//略...
//1、獲取bean中所有的屬性名稱
Set<String> names = getNames(relaxedTargetNames);
//2、將屬性名稱和前綴轉換爲配置文件的key值
PropertyValues propertyValues = getPropertySourcesPropertyValues(names,relaxedTargetNames);
//3、通過上面兩個步驟找到的屬性從配置文件中獲取數據通過反射注入到bean中
dataBinder.bind(propertyValues);
//數據校驗
if (this.validator != null) {
dataBinder.validate();
}
//判斷數據綁定過程中是否有錯誤
checkForBindingErrors(dataBinder);
}
上面代碼中使用dataBinder.bind方法進行屬性值賦值,源碼如下:
public void bind(PropertyValues pvs) {
MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues) ?
(MutablePropertyValues) pvs : new MutablePropertyValues(pvs);
doBind(mpvs);
}
protected void doBind(MutablePropertyValues mpvs) {
checkAllowedFields(mpvs);
checkRequiredFields(mpvs);
//進行賦值
applyPropertyValues(mpvs);
}
protected void applyPropertyValues(MutablePropertyValues mpvs) {
try {
// Bind request parameters onto target object.
getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
}
catch (PropertyBatchUpdateException ex) {
// Use bind error processor to create FieldErrors.
for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) {
getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult());
}
}
}
經過以上步驟連續的方法調用後,最終調用的是ConfigurablePropertyAccessor.setPropertyValues使用反射進行設置屬性值,到這裏就不繼續深入了。想要繼續深入瞭解的可以繼續閱讀源碼,到最後可以發現調用的是AbstractNestablePropertyAccessor.processLocalProperty中使用反射進行賦值。
上面的代碼分析非常清晰明瞭的解釋瞭如何查找@ConfigurationProperties對象和如何使用反射的方式進行賦值。
總結
在上面的步驟中我們分析了@ConfigurationProperties從篩選bean到注入屬性值的過程,整個過程的難度還不算高,沒有什麼特別的難點,這又是一個非常好的BeanPostProcessor使用場景說明。
從本文中可以學習到BeanPostProcessor是在SpringBoot中運用,以及如何通過AnnotationUtils與ConfigurationBeanFactoryMetaData結合對系統中所有添加了指定註解的bean進行掃描。