SpringBoot(六)外部化配置 - @ConfigurationProperties


3、外部化配置的核心

        接着上一章,《Spring Boot 外部化配置(一)》

3.2 @ConfigurationProperties

眾所周知,當 Spring Boot 集成外部組件后,就可在 propertiesYAML 配置文件中定義組件需要的屬性,如 Redis 組件:

spring.redis.url=redis://user:password@example.com:6379
spring.redis.host=localhost
spring.redis.password=123456
spring.redis.port=6379

其中都是以 spring.redis 為前綴。這其實是 Spring Boot 為每個組件提供了對應的 Properties 配置類,並將配置文件中的屬性值給映射到配置類中,而且它們有個特點,都是以 Properties 結尾,如 Redis 對應的配置類是 RedisProperties

@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
    private String url;

	private String host = "localhost";

	private String password;

	private int port = 6379;
	
	...
}

其中有個名為 @ConfigurationProperties 的注解,它的 prefix 參數就是約定好的前綴。該注解的功能就是將配置文件中的屬性和 Properties 配置類中的屬性進行映射,來達到自動配置的目的。這個過程分為兩步,第一步是注冊 Properties 配置類,第二步是綁定配置屬性,過程中還涉及到一個注解,它就是 @EnableConfigurationProperties ,該注解是用來觸發那兩步操作的。我們以 Redis 為例來看它使用方式:

...
@EnableConfigurationProperties(RedisProperties.class)
public class RedisAutoConfiguration {
    ...
}

可以看到它的參數是 RedisProperties 配置類。通過前面的 《Spring Boot 自動裝配(一)》 我們知道,該注解是屬於 @Enable 模塊注解,所以,該注解中必然有 @Import 導入的實現了 ImportSelectorImportBeanDefinitionRegistrar 接口的類,具體的功能都由導入的類來實現。我們進入該注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {

	/**
	 * Convenient way to quickly register {@link ConfigurationProperties} annotated beans
	 * with Spring. Standard Spring Beans will also be scanned regardless of this value.
	 * @return {@link ConfigurationProperties} annotated beans to register
	 */
	Class<?>[] value() default {};

}

果不其然,通過 @Import 導入了 EnableConfigurationPropertiesImportSelector 類,整個的處理流程都是在該類中進行處理:

class EnableConfigurationPropertiesImportSelector implements ImportSelector {

	private static final String[] IMPORTS = { ConfigurationPropertiesBeanRegistrar.class.getName(),
			ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };

	@Override
	public String[] selectImports(AnnotationMetadata metadata) {
		return IMPORTS;
	}

	...
}

該類實現了 ImportSelector 接口,並重寫了 selectImports 方法,該方法返回的類會被 Spring 加載。可以看到這里返回了兩個類,其中 ConfigurationPropertiesBeanRegistrar 就是用來注冊 Properties 配置類的,而 ConfigurationPropertiesBindingPostProcessorRegistrar 則是用來綁定配置屬性,且它們都實現了 ImportBeanDefinitionRegistrar 接口,會在重寫的 registerBeanDefinitions 方法中進行直接注冊 Bean 的操作。以上特性都在 《Spring Boot 自動裝配(一)》的 3.1 小節介紹過,這里不在敘述。接下來,我們分別介紹這兩個類。

3.2.1 注冊 Properties 配置類

我們先來看看 ConfigurationPropertiesBeanRegistrar 是如何注冊這些配置類的。我們直接進入該類的實現:

public static class ConfigurationPropertiesBeanRegistrar implements ImportBeanDefinitionRegistrar {

        // 1、第一步會先執行重寫的 registerBeanDefinitions 方法,
        // 入參分別是 AnnotationMetadata 和 BeanDefinitionRegistry。
        // AnnotationMetadata 是獲取類的元數據的,如注解信息、 classLoader 等,
        // BeanDefinitionRegistry 則是直接注冊所需要的 Bean 
		public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
			
			// 2、調用 getTypes 方法,返回 Properties 配置類集合。進入 2.1 詳細查看
			// 3、調用 register 方法,把 Properties 配置類注冊到 Spring 容器中。進入 3.1 詳細查看
			getTypes(metadata).forEach((type) -> register(registry, (ConfigurableListableBeanFactory) registry, type));
		}

        // 2.1 
		private List<Class<?>> getTypes(AnnotationMetadata metadata) {
			
			// 獲取指定注解的所有屬性值,key是屬性名稱,Value是值
			MultiValueMap<String, Object> attributes = metadata
					.getAllAnnotationAttributes(EnableConfigurationProperties.class.getName(), false);
			
			// 返回 key 名稱為 value 的值,這里返回的就是 Properties 配置類
			return collectClasses((attributes != null) ? attributes.get("value") : Collections.emptyList());
		}

        // 3.1
		private void register(BeanDefinitionRegistry registry, ConfigurableListableBeanFactory beanFactory,
				Class<?> type) {
			// getName 返回的是 Bean 的名稱。進入 3.2 詳細查看
			String name = getName(type);
			
			// 判斷有沒有注冊過這個 Bean
			if (!containsBeanDefinition(beanFactory, name)) {
			
			    // 沒有則注冊該 Bean。入參是注冊器、Bean 的名稱、需注冊的 Bean。進入 4 詳細查看
				registerBeanDefinition(registry, name, type);
			}
		}

        // 3.2
		private String getName(Class<?> type) {
			
			// 獲取 Properties 配置類上標注的 ConfigurationProperties 注解信息
			ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type, ConfigurationProperties.class);
			
			// 獲取該注解中 prefix 的屬性值
			String prefix = (annotation != null) ? annotation.prefix() : "";
			
			// 最后返回的是名稱格式是 屬性前綴-配置類全路徑名,如:
			// spring.redis-org.springframework.boot.autoconfigure.data.redis.RedisProperties
			return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName() : type.getName());
		}

        // 4、
		private void registerBeanDefinition(BeanDefinitionRegistry registry, String name, Class<?> type) {
			assertHasAnnotation(type);
			GenericBeanDefinition definition = new GenericBeanDefinition();
			definition.setBeanClass(type);
			
			// 通過 registerBeanDefinition 方法,注冊 Bean 。
			// 后期會有 Spring 系列的文章詳細介紹該過程,到時候大家再一起討論。
			registry.registerBeanDefinition(name, definition);
		}
	}

執行完后,我們所有的 Properties 配置類就被注冊到了 Spring 容器中。接下來,我們來看看配置文件中的數據是如何與 Properties 配置類中的屬性進行綁定的。

3.2.2 綁定配置屬性

我們直接進入 ConfigurationPropertiesBindingPostProcessorRegistrar 類中進行查看:

public class ConfigurationPropertiesBindingPostProcessorRegistrar implements ImportBeanDefinitionRegistrar {

	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		if (!registry.containsBeanDefinition(ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) {
			registerConfigurationPropertiesBindingPostProcessor(registry);
			registerConfigurationBeanFactoryMetadata(registry);
		}
	}

	...
}

這里也是在重寫的 registerBeanDefinitions 方法中注冊了兩個 Bean,一個是 ConfigurationBeanFactoryMetadata,這個是用來存儲元數據的,我們不做過多關注;另一個是 ConfigurationPropertiesBindingPostProcessor ,該類就是用來綁定屬性的,我們主要對該類進行討論:

public class ConfigurationPropertiesBindingPostProcessor
		implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean {

    ...
}

可以看到,該類實現了幾個接口,且都是 Spring 提供的擴展接口。這里我們簡要介紹一下:

1、BeanPostProcessor:這是 Bean 的后置處理器。該類有兩個方法,一個是 postProcessBeforeInitialization ,Bean 初始化前該方法會被調用;
另一個是 postProcessAfterInitialization ,Bean 初始化后該方法會被調用;需注意的是,Spring 上下文中所有 Bean 的初始化都會觸發這兩個方法。

2、ApplicationContextAware:這是 Spring Aware 系列接口之一。該類有一個 setApplicationContext 方法,主要是用來獲取 ApplicationContext 上下文對象;同理,如果是其它前綴的 Aware,則獲取相應前綴名的對象。

3、InitializingBean:這是 Bean 的生命周期相關接口。該類有一個 afterPropertiesSet 方法,當 Bean 的所有屬性初始化后,該方法會被調用。

其中, BeanPostProcessorInitializingBean 的功能都是在 Bean 的生命周期中執行額外的操作。

這里我們簡單的了解就行,后面會在 Spring 系列的文章中詳細討論。

接着,我們介紹該類中的方法:

public class ConfigurationPropertiesBindingPostProcessor
		implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean {

    ...

    public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";

	private ConfigurationBeanFactoryMetadata beanFactoryMetadata;

	private ApplicationContext applicationContext;

	private ConfigurationPropertiesBinder configurationPropertiesBinder;

    // 1、這是重寫的 ApplicationContextAware 接口中的方法,用來獲取 ApplicationContext 上下文對象
	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

    // 2、這是重寫的 InitializingBean 接口中的方法,當 Bean 的屬性初始化后會被調用。
    // 該方法主要對 ConfigurationBeanFactoryMetadata 和 ConfigurationPropertiesBinder 進行實例化
	@Override
	public void afterPropertiesSet() throws Exception {
		this.beanFactoryMetadata = this.applicationContext.getBean(ConfigurationBeanFactoryMetadata.BEAN_NAME,
				ConfigurationBeanFactoryMetadata.class);
		this.configurationPropertiesBinder = new ConfigurationPropertiesBinder(this.applicationContext,
				VALIDATOR_BEAN_NAME);
	}

    // 3、這是重寫的 BeanPostProcessor 接口中的方法,在 Bean 初始化前會被調用,綁定屬性的操作就是從這里開始。
    // 入參 bean 就是待初始化的 Bean,beanName 就是 Bean 的名稱
	@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;
	}

	...
}

我們先來看第二步的 afterPropertiesSet 方法,該方法中實例化了兩個類,一個是從 ApplicationContext 中獲取的
ConfigurationBeanFactoryMetadata 類,是用來操作元數據的,不做過多關注;另一個是通過帶參構造器初始化的 ConfigurationPropertiesBinder 類,參數是 ApplicationContext 對象和 configurationPropertiesValidator 字符串。我們進入該類的構造器中:

class ConfigurationPropertiesBinder {

    private final ApplicationContext applicationContext;

	private final PropertySources propertySources;

	private final Validator configurationPropertiesValidator;

	private final boolean jsr303Present;

	...

	ConfigurationPropertiesBinder(ApplicationContext applicationContext, String validatorBeanName) {
		this.applicationContext = applicationContext;
		this.propertySources = new PropertySourcesDeducer(applicationContext).getPropertySources();
		this.configurationPropertiesValidator = getConfigurationPropertiesValidator(applicationContext,
				validatorBeanName);
		this.jsr303Present = ConfigurationPropertiesJsr303Validator.isJsr303Present(applicationContext);
	}
	...
}

該類中又實例化了四個類,我們重點關注 PropertySources 的實例化過程,具體是通過 PropertySourcesDeducer 類的 getPropertySources 方法,我們進入該類:

class PropertySourcesDeducer {
    
    ...
    private final ApplicationContext applicationContext;

	PropertySourcesDeducer(ApplicationContext applicationContext) {
		this.applicationContext = applicationContext;
	}

    // 1、通過 extractEnvironmentPropertySources 方法,返回 MutablePropertySources 對象,
    // MutablePropertySources 是 PropertySources 的實現類
	public PropertySources getPropertySources() {
		
		...
		
		MutablePropertySources sources = extractEnvironmentPropertySources();
		if (sources != null) {
			return sources;
		}
		throw new IllegalStateException(
				"Unable to obtain PropertySources from " + "PropertySourcesPlaceholderConfigurer or Environment");
	}
	
    // 2、調用 Environment 的 getPropertySources 方法,返回 MutablePropertySources
	private MutablePropertySources extractEnvironmentPropertySources() {
		Environment environment = this.applicationContext.getEnvironment();
		if (environment instanceof ConfigurableEnvironment) {
			return ((ConfigurableEnvironment) environment).getPropertySources();
		}
		return null;
	}
	...
    
}

看到這,大家應該比較熟悉了,Environment 就是我們在 《Spring Boot 外部化配置(一)》中 3.1 小節講過的應用運行時的環境,通過該類可獲取所有的外部化配置數據,而 MutablePropertySources 則是底層真正存儲外部化配置對象的。

到這里,第二步的 afterPropertiesSet 方法就執行完了,主要是實例化了 ConfigurationPropertiesBinder 對象,而該對象中存儲了所有的外部化配置。接着看第三步的 postProcessBeforeInitialization 方法:

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;
}

上面說過,所有 Bean 初始化都會調用這個方法,所以先判斷當前 Bean 有沒有標注 @ConfigurationProperties 注解,有則表示當前 BeanProperties 配置類,並調用 bind 方法對該類進行綁定屬性的操作,我們進入該方法:

private void bind(Object bean, String beanName, ConfigurationProperties annotation) {
	
	...
	
	try {
		this.configurationPropertiesBinder.bind(target);
	}
	catch (Exception ex) {
		throw new ConfigurationPropertiesBindException(beanName, bean, annotation, ex);
	}
}

這里調用了在第二步實例化的 ConfigurationPropertiesBinder 對象中的 bind 方法:

class ConfigurationPropertiesBinder {
    
    ...
    
    public void bind(Bindable<?> target) {
		
		...
		
		getBinder().bind(annotation.prefix(), target, bindHandler);
	}
	
	...
	
	private Binder getBinder() {
		if (this.binder == null) {
			this.binder = new Binder(getConfigurationPropertySources(), getPropertySourcesPlaceholdersResolver(),
					getConversionService(), getPropertyEditorInitializer());
		}
		return this.binder;
	}
	
	private Iterable<ConfigurationPropertySource> getConfigurationPropertySources() {
		return ConfigurationPropertySources.from(this.propertySources);
	}
	
	...
}

里面先通過 getBinder() 返回 Binder 對象。在 getBinder 方法中是通過 Binder 帶參構造器創建的該對象,我們主要關注 getConfigurationPropertySources 方法返回的第一個參數:

class ConfigurationPropertiesBinder {
    
    ...
    
    private final PropertySources propertySources;
    
    ...
    
    private Iterable<ConfigurationPropertySource> getConfigurationPropertySources() {
		return ConfigurationPropertySources.from(this.propertySources);
	}
	
    ...
}

具體的是通過 ConfigurationPropertySources 中的 from 方法返回,入參 propertySources 是在第二步實例化 ConfigurationPropertiesBinder 對象時初始化好的值,里面存儲的是外部化配置的源對象 PropertySource ,我們進入該方法:

public final class ConfigurationPropertySources {
    
    ...

    public static Iterable<ConfigurationPropertySource> from(Iterable<PropertySource<?>> sources) {
		return new SpringConfigurationPropertySources(sources);
	}
	
	...
}

最終返回的就是 SpringConfigurationPropertySources 配置源對象,在 《Spring Boot 外部化配置(一)》中講過,該類主要是做一個適配器的工作,將 MutablePropertySources 轉換為 ConfigurationPropertySource

之后,該對象傳入了 Binder 的構造器中,用於創建該對象:

public class Binder {
    
    ...
    
    private final Iterable<ConfigurationPropertySource> sources;
    
    ...
    
    public Binder(Iterable<ConfigurationPropertySource> sources,
			PlaceholdersResolver placeholdersResolver,
			ConversionService conversionService,
			Consumer<PropertyEditorRegistry> propertyEditorInitializer) {
		
		this.sources = sources;
		
		...
	}
	
    ...
}

至此, Binder 對象中就存有一份外部化配置的數據,且后續所有的綁定操作都在該類中進行。因后續中間過程實在太過龐雜,且不易理解,這里我們直接進入最后一步,對詳細過程感興趣的同學請自行研究,這里不再贅述。

進入最后階段的 bind 方法:

// 這里着重介紹一下 BeanProperty 類,該類存儲了 properties 配置類中的字段及字段的set、get方法,存儲的是反射中的類。
// 如 RedisProperties 中的 url 字段,則 BeanProperty 對象中存儲的是
// url 的 Field 類、setUrl 的 Method 類、getUrl 的 Method 類。
private <T> boolean bind(BeanSupplier<T> beanSupplier,
			BeanPropertyBinder propertyBinder, BeanProperty property) {
	
	// 這里獲取的是字段名
	String propertyName = property.getName();
	
	// 這里獲取的是字段類型
	ResolvableType type = property.getType();
	Supplier<Object> value = property.getValue(beanSupplier);
	Annotation[] annotations = property.getAnnotations();
	
	// 這里獲取到了配置文件中的值,該值來源於 SpringConfigurationPropertySources 對象
	Object bound = propertyBinder.bindProperty(propertyName,
			Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations));
	if (bound == null) {
		return false;
	}
	if (property.isSettable()) {
	    
	    // 最后則是通過 set Method 的 invoke 方法,也就是反射的形式進行賦值。
		property.setValue(beanSupplier, bound);
	}
	else if (value == null || !bound.equals(value.get())) {
		throw new IllegalStateException(
				"No setter found for property: " + property.getName());
	}
	return true;
}

至此,整個綁定配置屬性的流程結束。可以看到,最終獲取的外部化配置數據來源於前文加載的 Environment 對象。

最后來簡單回顧一下 @ConfigurationProperties 注解實現配置文件中屬性值和配置類屬性映射的過程:

1、首先將 @ConfigurationProperties 標注在 Properties 配置類中,參數是約定好的屬性前綴。

2、然后通過 @EnableConfigurationProperties 來觸發整個流程,參數是 Properties 配置類。

3、在 @EnableConfigurationProperties 中通過 @import 導入了 EnableConfigurationPropertiesImportSelector 類,該類中又加載了兩個類,一個用來注冊 Properties 配置類,另一個用來綁定配置屬性。

4、最后,是通過反射的方式進行屬性綁定,且屬性值來源於 Environment

3.1.3 ConfigurationPropertiesAutoConfiguration

其實,當我們使用 @ConfigurationProperties 時,無需標注 @EnableConfigurationProperties 注解,因為 Spring Boot 在自動裝配的過程中會幫我們加載一個名為 ConfigurationPropertiesAutoConfiguration 的類,該類是在 spring.factories 中定義好的:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration

具體的自動裝配過程在 《Spring Boot 自動裝配(二)》 這篇文章中討論過,這里不再贅述。我們來看看 ConfigurationPropertiesAutoConfiguration 實現:

@Configuration
@EnableConfigurationProperties
public class ConfigurationPropertiesAutoConfiguration {

}

很簡單,直接通過標注 @EnableConfigurationProperties 注解來開啟自動配置的流程。那這樣怎么注冊 Properties 配置類呢?因為上面說過,Properties 配置類是通過該注解的參數傳遞進來的。其實,只需在配置類上標注 @Component 注解就行了,之后會被 Spring 掃描到,然后注冊。

4、總結

        最后,來對 Spring Boot 外部化配置做一個整體的總結:

1、首先,外部化配置是 Spring Boot 的一個特性,主要是通過外部的配置資源實現與代碼的相互配合,來避免硬編碼,提供應用數據或行為變化的靈活性。

2、然后介紹了幾種外部化配置的資源類型,如 propertiesYAML 配置文件類型,並介紹了獲取外部化配置資源的幾種方式。

3、其次,介紹了 Environment 類的加載流程,以及所有外部化配置加載到 Environment 中的底層是實現。EnvironmentSpring Boot 外部化配置的核心類,該類存儲了所有的外部化配置資源,且其它獲取外部化配置資源的方式也都依賴於該類。

4、最后,介紹了 Spring Boot 框架中核心的 @ConfigurationProperties 注解,該注解是將 application 配置文件中的屬性值和 Properties 配置類中的屬性進行映射,來達到自動配置的目的,並帶大家探討了這一過程的底層實現。

以上就是本章的內容,如過文章中有錯誤或者需要補充的請及時提出,本人感激不盡。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM