Spring Boot 外部化配置(二) - @ConfigurationProperties 、@EnableConfigurationProperties
目錄
- 3、外部化配置的核心
- 3.2 @ConfigurationProperties
- 3.2.1 註冊 Properties 配置類
- 3.2.2 繫結配置屬性
- 3.1.3 ConfigurationPropertiesAutoConfiguration
- 3.2 @ConfigurationProperties
- 4、總結
3、外部化配置的核心
接著上一章,《Spring Boot 外部化配置(一)》
3.2 @ConfigurationProperties
眾所周知,當 Spring Boot 整合外部元件後,就可在 properties
或 YAML
配置檔案中定義元件需要的屬性,如 Redis
元件:
spring.redis.url=redis://user:[email protected]: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
匯入的實現了 ImportSelector
或 ImportBeanDefinitionRegistrar
介面的類,具體的功能都由匯入的類來實現。我們進入該註解:
@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
的所有屬性初始化後,該方法會被呼叫。
其中,BeanPostProcessor
和InitializingBean
的功能都是在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
註解,有則表示當前 Bean
是 Properties
配置類,並呼叫 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、然後介紹了幾種外部化配置的資源型別,如properties
和YAML
配置檔案型別,並介紹了獲取外部化配置資源的幾種方式。
3、其次,介紹了Environment
類的載入流程,以及所有外部化配置載入到Environment
中的底層是實現。Environment
是Spring Boot
外部化配置的核心類,該類儲存了所有的外部化配置資源,且其它獲取外部化配置資源的方式也都依賴於該類。
4、最後,介紹了Spring Boot
框架中核心的@ConfigurationProperties
註解,該註解是將application
配置檔案中的屬性值和Properties
配置類中的屬性進行對映,來達到自動配置的目的,並帶大家探討了這一過程的底層實現。
以上就是本章的內容,如過文章中有錯誤或者需要補充的請及時提出,本人感激不