Spring Boot 注解 @ConfigurationProperties

2020年4月17日 | 作者 Siran | 3100字 | 阅读大约需要7分钟
归档于 spring | 标签 #spring boot

简述

由于在分析Gateway的时候,Gateway通过 GatewayProperties 来自动配置外部配置,故写此文章。

先思考一个问题 Springboot 中我们加入Redis 配置文件,Springboot 是如何加载配置的呢?

spring.redis.url=127.0.0.1:6379
spring.redis.host=localhost
spring.redis.password=123

其中都是以 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 参数就是spring.redis。该注解的功能就是将配置文件中的属性和 Properties 配置类中的属性进行映射,来达到自动配置的目的。这个过程分为两步,第一步是注册 Properties 配置类,第二步是绑定配置属性,过程中还涉及到一个注解,它就是 @EnableConfigurationProperties ,该注解是用来触发那两步操作的。我们以 Redis 为例来看它使用方式:

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

可以看到它的参数是 RedisProperties 配置类。这个注解就是关键,我们就来从此分析。


源码分析

@EnableConfigurationProperties

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

}


EnableConfigurationPropertiesImportSelector

class EnableConfigurationPropertiesImportSelector implements ImportSelector {

	private static final String[] IMPORTS = { 
    //① 用于注册 Properties类
    ConfigurationPropertiesBeanRegistrar.class.getName(),
    //②用于进行绑定配置属性
			ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };

	@Override
	public String[] selectImports(AnnotationMetadata metadata) {
		return IMPORTS;
	}
}
  • 用于注册 Properties类

    //org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector的内部类
    public static class ConfigurationPropertiesBeanRegistrar implements ImportBeanDefinitionRegistrar {
      
          @Override
          public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
          //① 调用 getTypes 方法,返回 Properties 配置类集合。
          //② 调用 register 方法,把 Properties 配置类注册到 Spring 容器中。
              getTypes(metadata).forEach((type) -> register(registry, (ConfigurableListableBeanFactory) registry, type));
          }
      }
    
    • ① getTypes,返回Properties 配置类集合

      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());
              }
          
      private List<Class<?>> collectClasses(List<?> values) {
                  return values.stream().flatMap((value) -> Arrays.stream((Object[]) value)).map((o) -> (Class<?>) o)
                          .filter((type) -> void.class != type).collect(Collectors.toList());
              }
      
    • ② 调用 register 方法,把 Properties 配置类注册到 Spring 容器中。

      private void register(BeanDefinitionRegistry registry, ConfigurableListableBeanFactory beanFactory,Class<?> type) {
            // getName 返回的是 Bean 的名称
                  String name = getName(type);
            // 判断有没有注册过这个 Bean
                  if (!containsBeanDefinition(beanFactory, name)) {
              //不存在则,则进行注册
                      registerBeanDefinition(registry, name, type);
                  }
              }
          
          
      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());
              }
          
      //下面这两个方法就是Spring注入,不是本文分析的重点。
      private boolean containsBeanDefinition(ConfigurableListableBeanFactory beanFactory, String name) {
                  if (beanFactory.containsBeanDefinition(name)) {
                      return true;
                  }
                  BeanFactory parent = beanFactory.getParentBeanFactory();
                  if (parent instanceof ConfigurableListableBeanFactory) {
                      return c
          				
          				
                      ontainsBeanDefinition((ConfigurableListableBeanFactory) parent, name);
                  }
                  return false;
              }
          
      private void registerBeanDefinition(BeanDefinitionRegistry registry, String name, Class<?> type) {
                  assertHasAnnotation(type);
                  GenericBeanDefinition definition = new GenericBeanDefinition();
                  definition.setBeanClass(type);
                  registry.registerBeanDefinition(name, definition);
              }
      

      到这里所有需要的 Properties配置类 就被注册到了 Spring 容器中。接下来就是与配制类进行绑定。

  • 用于进行绑定配置属性

    public class ConfigurationPropertiesBindingPostProcessorRegistrar implements ImportBeanDefinitionRegistrar {
      
    @Override
      public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
          if (!registry.containsBeanDefinition(ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) {
          //① 真正用来绑定属性的
              registerConfigurationPropertiesBindingPostProcessor(registry);
          //② 存储元数据
              registerConfigurationBeanFactoryMetadata(registry);
          }
      }
    }
    
    • ① 绑定属性

          private void registerConfigurationPropertiesBindingPostProcessor(BeanDefinitionRegistry registry) {
              GenericBeanDefinition definition = new GenericBeanDefinition();
              definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class);
              definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
              registry.registerBeanDefinition(ConfigurationPropertiesBindingPostProcessor.BEAN_NAME, definition);
          
          }
      
      public class ConfigurationPropertiesBindingPostProcessor
              implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean {
          
          /**
           * The bean name that this post-processor is registered with.
           */
          public static final String BEAN_NAME = ConfigurationPropertiesBindingPostProcessor.class.getName();
          
          /**
           * The bean name of the configuration properties validator.
           */
          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 的属性初始化后会被调用。
          @Override
          public void afterPropertiesSet() throws Exception {
          //初始化beanFactoryMetadata
              this.beanFactoryMetadata = this.applicationContext.getBean(ConfigurationBeanFactoryMetadata.BEAN_NAME,
                      ConfigurationBeanFactoryMetadata.class);
          //初始化configurationPropertiesBinder
              this.configurationPropertiesBinder = new ConfigurationPropertiesBinder(this.applicationContext,
                      VALIDATOR_BEAN_NAME);
          }
          
        //排序
          @Override
          public int getOrder() {
              return Ordered.HIGHEST_PRECEDENCE + 1;
          }
          
        //③ 重写的 BeanPostProcessor 接口中的方法,在 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;
          }    
      }
      
      • 实现的几个接口:

        • BeanPostProcessor:这是 Bean 的后置处理器。该类有两个方法,一个是 postProcessBeforeInitialization ,Bean 初始化前该方法会被调用; 另一个是 postProcessAfterInitialization ,Bean 初始化后该方法会被调用;需注意的是,Spring 上下文中所有 Bean 的初始化都会触发这两个方法。

        • ApplicationContextAware:这是 SpringAware系列接口之一。该类有一个 setApplicationContext 方法,主要是用来获取 ApplicationContext 上下文对象;同理,如果是其它前缀的 Aware,则获取相应前缀名的对象。

        • InitializingBean:这是 Bean 的生命周期相关接口。该类有一个 afterPropertiesSet 方法,当 Bean 的所有属性初始化后,该方法会被调用。

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

      • ① setApplicationContext : 重写的 ApplicationContextAware 接口中的方法,用来获取 ApplicationContext 上下文对象

      • ② afterPropertiesSet : 重写的 InitializingBean 接口中的方法,当 Bean 的属性初始化后会被调用。该方法主要是会初始化configurationPropertiesBinder

        class ConfigurationPropertiesBinder {
              
          ConfigurationPropertiesBinder(ApplicationContext applicationContext, String validatorBeanName) {
            //① 上下文
              this.applicationContext = applicationContext;
            //② 获取外部的所有配置
              this.propertySources = new PropertySourcesDeducer(applicationContext).getPropertySources();
            //③ 配置的校验器
              this.configurationPropertiesValidator = getConfigurationPropertiesValidator(applicationContext,
                      validatorBeanName);
              this.jsr303Present = ConfigurationPropertiesJsr303Validator.isJsr303Present(applicationContext);
          }
        }
        

        主要看②,获取外部的所有配置。

        // ①通过 extractEnvironmentPropertySources 方法,返回 MutablePropertySources 对象,MutablePropertySources 是 PropertySources 的实现类
        public PropertySources getPropertySources() {
              PropertySourcesPlaceholderConfigurer configurer = getSinglePropertySourcesPlaceholderConfigurer();
              if (configurer != null) {
                  return configurer.getAppliedPropertySources();
              }
            //② 调用 Environment 的 getPropertySources 方法,返回 MutablePropertySources
              MutablePropertySources sources = extractEnvironmentPropertySources();
              if (sources != null) {
                  return sources;
              }
              throw new IllegalStateException(
                      "Unable to obtain PropertySources from " + "PropertySourcesPlaceholderConfigurer or Environment");
          }
              
        //这里的Environment 就是在SpringBoot 初始化的时候创建的  Environment
        private MutablePropertySources extractEnvironmentPropertySources() {
              Environment environment = this.applicationContext.getEnvironment();
              if (environment instanceof ConfigurableEnvironment) {
                  return ((ConfigurableEnvironment) environment).getPropertySources();
              }
              return null;
          }
        

        到这里就是已经获取到所有外部的配置了。具体的配置文件是如何被加载成Environment的,

        可以看我的这篇文章。

        Spring Boot additional—location 参数

      • ③ postProcessBeforeInitialization : 重写的 BeanPostProcessor 接口中的方法,在 Bean 初始化前会被调用,绑定属性的操作就是从这里开始

        通过上面 ② 中的 afterPropertiesSet 方法,初始化好了 ConfigurationPropertiesBinder 对象以及获取到了 Environment 对象,那么这里就开始与配置类进行绑定了

        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            //① 判断当前类是否被@ConfigurationProperties 注解
              ConfigurationProperties annotation = getAnnotation(bean, beanName, ConfigurationProperties.class);
              if (annotation != null) {
              //② 进行绑定
                  bind(bean, beanName, annotation);
              }
              return bean;
          }
              
              
        private void bind(Object bean, String beanName, ConfigurationProperties annotation) {
              ResolvableType type = getBeanType(bean, beanName);
              Validated validated = getAnnotation(bean, beanName, Validated.class);
              Annotation[] annotations = (validated != null) ? new Annotation[] { annotation, validated }
                      : new Annotation[] { annotation };
              Bindable<?> target = Bindable.of(type).withExistingValue(bean).withAnnotations(annotations);
              try {
              //③ 会发现这个是调用了在afterPropertiesSet方法初始化好的configurationPropertiesBinder中的bind方法。
                  this.configurationPropertiesBinder.bind(target);
              }
              catch (Exception ex) {
                  throw new ConfigurationPropertiesBindException(beanName, bean, annotation, ex);
              }
          }
        
        • ③ 会发现这个是调用了在afterPropertiesSet方法初始化好的configurationPropertiesBinder中的bind方法。

          public void bind(Bindable<?> target) {
              //校验是否是@ConfigurationProperties 注解的
                  ConfigurationProperties annotation = target.getAnnotation(ConfigurationProperties.class);
                  Assert.state(annotation != null, () -> "Missing @ConfigurationProperties on " + target);
                  List<Validator> validators = getValidators(target);
                  BindHandler bindHandler = getBindHandler(annotation, validators);
              //绑定
                  getBinder().bind(annotation.prefix(), target, bindHandler);
              }
          

          到这里就已经知道了 @ConfigurationProperties 它是怎么运作的,我个人就分析到这。如果要分析如何绑定的,那么请自行分析


    总结:

    • 其实当我们使用 @ConfigurationProperties 时,无需标注 @EnableConfigurationProperties 注解,因为 Spring Boot 在自动装配的过程中会帮我们加载一个名为 ConfigurationPropertiesAutoConfiguration 的类,该类是在 spring.factories 中定义好的:

      # Auto Configure
      org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
      org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
      
    • 外部的所有配置文件都会被填充到 Environment 对象中,@ConfigurationProperties 其实也就会获取绑定器然后获取 Environment 对象,根据被注解的类与之进行映射达到自动配置的目的。

    • 这篇文章主要是为了我在分析Gateway 而写的

      • Gateway 通过 GatewayProperties 来自动装配我们在 properties 或者 yml 中的配置,所以为什么我们在进行配置的时候需要加上spring.cloud.gateway

        @ConfigurationProperties("spring.cloud.gateway") //① 表明以 "spring.cloud.gateway" 前缀的 properties 会绑定 GatewayProperties。
        @Validated
        public class GatewayProperties {
          //Route
          private List<RouteDefinition> routes = new ArrayList<>();
          //filter
          private List<FilterDefinition> defaultFilters = new ArrayList<>();
        }  
        
        //看到这里是不是已经豁然开朗了
        spring:
          cloud:
            gateway:
              routes:
              - id: websocket_test
                uri: ws://localhost:9000
                order: 9000
                predicates:
                - Path=/echo
        
      • 但是Gateway 的自动装配没有加入ConfigurationPropertiesAutoConfiguration,它是通过 GatewayAutoConfiguration 来开启 @ConfigurationProperties

        # Auto Configure
        org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
        org.springframework.cloud.gateway.config.GatewayAutoConfiguration,\
                  
                
        @EnableConfigurationProperties// 开启 @ConfigurationProperties
        public class GatewayAutoConfiguration {
        }