• 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 配置类中的属性进行映射,来达到自动配置的目的,并带大家探讨了这一过程的底层实现。

    以上就是本章的内容,如过文章中有错误或者需要补充的请及时提出,本人感激不尽。

  • 相关阅读:
    September 29th 2017 Week 39th Friday
    September 28th 2017 Week 39th Thursday
    September 27th 2017 Week 39th Wednesday
    September 26th 2017 Week 39th Tuesday
    September 25th 2017 Week 39th Monday
    September 24th 2017 Week 39th Sunday
    angular2 学习笔记 ( Form 表单 )
    angular2 学习笔记 ( Component 组件)
    angular2 学习笔记 ( Http 请求)
    angular2 学习笔记 ( Router 路由 )
  • 原文地址:https://www.cnblogs.com/loongk/p/12076402.html
Copyright © 2020-2023  润新知