目錄html
接着上一章,《Spring Boot 外部化配置(一)》java
衆所周知,當 Spring Boot 集成外部組件後,就可在 properties
或 YAML
配置文件中定義組件須要的屬性,如 Redis
組件: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
:spring
@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
爲例來看它使用方式:app
... @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
類,整個的處理流程都是在該類中進行處理:less
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 小節介紹過,這裏不在敘述。接下來,咱們分別介紹這兩個類。ide
咱們先來看看 ConfigurationPropertiesBeanRegistrar
是如何註冊這些配置類的。咱們直接進入該類的實現:post
public static class ConfigurationPropertiesBeanRegistrar implements ImportBeanDefinitionRegistrar { // 一、第一步會先執行重寫的 registerBeanDefinitions 方法, // 入參分別是 AnnotationMetadata 和 BeanDefinitionRegistry。 // AnnotationMetadata 是獲取類的元數據的,如註解信息、 classLoader 等, // BeanDefinitionRegistry 則是直接註冊所須要的 Bean public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { // 二、調用 getTypes 方法,返回 Properties 配置類集合。進入 2.1 詳細查看 // 三、調用 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()); } // 四、 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
配置類中的屬性進行綁定的。ui
咱們直接進入 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
提供的擴展接口。這裏咱們簡要介紹一下:
一、BeanPostProcessor:這是
Bean
的後置處理器。該類有兩個方法,一個是 postProcessBeforeInitialization ,Bean
初始化前該方法會被調用;
另外一個是 postProcessAfterInitialization ,Bean
初始化後該方法會被調用;需注意的是,Spring
上下文中全部Bean
的初始化都會觸發這兩個方法。
二、ApplicationContextAware:這是Spring
的Aware
系列接口之一。該類有一個 setApplicationContext 方法,主要是用來獲取ApplicationContext
上下文對象;同理,若是是其它前綴的Aware
,則獲取相應前綴名的對象。
三、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; // 一、這是重寫的 ApplicationContextAware 接口中的方法,用來獲取 ApplicationContext 上下文對象 @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } // 二、這是重寫的 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); } // 三、這是重寫的 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; } // 一、經過 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"); } // 二、調用 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
註解實現配置文件中屬性值和配置類屬性映射的過程:
一、首先將
@ConfigurationProperties
標註在Properties
配置類中,參數是約定好的屬性前綴。
二、而後經過@EnableConfigurationProperties
來觸發整個流程,參數是Properties
配置類。
三、在@EnableConfigurationProperties
中經過@import
導入了EnableConfigurationPropertiesImportSelector
類,該類中又加載了兩個類,一個用來註冊Properties
配置類,另外一個用來綁定配置屬性。
四、最後,是經過反射的方式進行屬性綁定,且屬性值來源於Environment
。
其實,當咱們使用 @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
掃描到,而後註冊。
最後,來對 Spring Boot
外部化配置作一個總體的總結:
一、首先,外部化配置是
Spring Boot
的一個特性,主要是經過外部的配置資源實現與代碼的相互配合,來避免硬編碼,提供應用數據或行爲變化的靈活性。
二、而後介紹了幾種外部化配置的資源類型,如properties
和YAML
配置文件類型,並介紹了獲取外部化配置資源的幾種方式。
三、其次,介紹了Environment
類的加載流程,以及全部外部化配置加載到Environment
中的底層是實現。Environment
是Spring Boot
外部化配置的核心類,該類存儲了全部的外部化配置資源,且其它獲取外部化配置資源的方式也都依賴於該類。
四、最後,介紹了Spring Boot
框架中核心的@ConfigurationProperties
註解,該註解是將application
配置文件中的屬性值和Properties
配置類中的屬性進行映射,來達到自動配置的目的,並帶你們探討了這一過程的底層實現。
以上就是本章的內容,如過文章中有錯誤或者須要補充的請及時提出,本人感激涕零。