1. 程式人生 > 程式設計 >詳解@ConfigurationProperties實現原理與實戰

詳解@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原始碼中,可以看到如下注釋提示:

enter image description here

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,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,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進行掃描。

到此這篇關於詳解@ConfigurationProperties實現原理與實戰的文章就介紹到這了,更多相關@ConfigurationProperties原理內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!