[SpringBoot源码分析三]:@PropertySource和@ConfigurationProperties

1. 使用场景

首先我们先了解下这两个注解的使用场景,看下他们到底是干什么的,下面就是他们的一个简单的例子,是为了将user.yml中以user前缀开头的值设置到当前这个对象中

虽然网上很多例子是从配置文件中读取属性的时候只使用了@ConfigurationProperties,但这是因为读取的这些属性都是在application.yml等默认注册到容器中的配置文件中的,而一旦我们的属性是在自定义的配置类文件,那就需要使用到@PropertySource

1.1 @PropertySource

@PropertySource:用于导入配置文件,比如说.yml文件

java 复制代码
public @interface PropertySource {

   String name() default "";

   /**
    * 指示要加载的属性文件的资源位置
    */
   String[] value();

   /**
    * 指示是否应该忽略查找属性资源失败
    */
   boolean ignoreResourceNotFound() default false;

   /**
    * 编码格式,例如:UTF-8
    */
   String encoding() default "";

   Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;

}

1.2 @ConfigurationProperties

@ConfigurationProperties:为Bean绑定一些外部属性(例如,从. Properties文件中),也可以将此添加到类定义或@Configuration类中的@Bean方法中。

java 复制代码
public @interface ConfigurationProperties {

   /**
    * 可以绑定到此对象的有效属性的前缀。一个有效的前缀是由一个或多个用点分隔的单词定义的
    */
   @AliasFor("prefix")
   String value() default "";

   /**
    * 可有效绑定到此对象的属性的前缀
    */
   @AliasFor("value")
   String prefix() default "";

   /**
    * 在绑定过程出现了错误,比如说字段类型错误,是否忽略
    */
   boolean ignoreInvalidFields() default false;

   /**
    * 绑定过程中是否应该忽略未知字段。一个未知的字段可能是属性中出现错误的标志
    */
   boolean ignoreUnknownFields() default true;

}

2. @PropertySource是如何注册配置文件的

@PropertySource其实是随着Bean注册过程中起作用的,所以我们第一点要先知道SpringBoot中Bean是如何注册 ,那就一定要先知道他的入口在哪里,而这个入口就是ConfigurationClassPostProcessor

2.1 ConfigurationClassPostProcessor是如何注册的

紧接着我们看ConfigurationClassPostProcessor是如何注册到容器的

首先我们启动SpringBoot项目都是通过SpringApplication.run(...)启动的,而这个run(...)方法中有一行代码是创建ApplicationContext的方法,就是createApplicationContext()

而由于我使用的是Servlet环境,自然而然就是实例化的的是AnnotationConfigServletWebServerApplicationContext

而这个类在实例化过程中也就是调用其构造方法的过程中会创建AnnotatedBeanDefinitionReader,这个AnnotatedBeanDefinitionReader的构造方法如下

而这个registerAnnotationConfigProcessors(...)方法就会注册ConfigurationClassPostProcessor

java 复制代码
public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
      BeanDefinitionRegistry registry, @Nullable Object source) {
      
   ...
   Set<BeanDefinitionHolder> beanDefs = new LinkedHashSet<>(8);

   if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
      RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
      def.setSource(source);
      beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
   }

   ...
   return beanDefs;
}

ConfigurationClassPostProcessor :其中使用@ComponentScan后可以进行Bean的扫描注册

2.2 ConfigurationClassPostProcessor的执行时机

在PostProcessorRegistrationDelegate中有一个方法invokeBeanDefinitionRegistryPostProcessors , 这个方法中的postProcessor只有一个,就是我们的ConfigurationClassPostProcessor

2.3. 开始进行Bean注册

我们开始看postProcessBeanDefinitionRegistry(...)方法, 这个方法主要是就是调用了processConfigBeanDefinitions(...)方法, 这个方法比较的复杂,我就单拎出两段核心代码

第一段 :是为了确定候选配置类,这里的候选配置默认类只会有一个那就是我们的启动类,启动类在这里的原因也是因为我们通过执行springapplication.run(...)的时候传入了启动类

java 复制代码
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
   List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
   //获取所有的BeanDefinition信息,其中就有启动类
   String[] candidateNames = registry.getBeanDefinitionNames();

   for (String beanName : candidateNames) {
      BeanDefinition beanDef = registry.getBeanDefinition(beanName);
      //判断对象是否是带有@Configuration的类
      //是下面的ConfigurationClassUtils.checkConfigurationClassCandidate()方法中设置的
      //其中Configuration注解,并且是否proxyBeanMethods属性不为false,模式为full,其他为lite
      if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
         if (logger.isDebugEnabled()) {
            logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
         }
      }
      //判断是否是候选配置类
      else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
         configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
      }
   }
   ...
}

然后我们看为什么启动类算一个候选配置类,从checkConfigurationClassCandidate(...)方法就能看出来,这里是要求携带@Configuration,这也是为什么@SpringBootApplication 内部带有 @SpringBootConfiguration的原因

java 复制代码
public static boolean checkConfigurationClassCandidate(
      BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {
   ...
   //获取有关@Configuration注解的信息
   Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());
   //判断是否设置了proxyBeanMethods属性
   //false:每个@Bean方法被调用多少次返回的组件都是新创建的
   //true:保证每个@Bean方法被调用多少次返回的组件都是单实例的
   if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {
      beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
   }
   //当标志@Configuration注解,且
   else if (config != null || isConfigurationCandidate(metadata)) {
      beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
   }
   else {
      return false;
   }
   ...

第二段 :这段代码非常重要,也是通过这段代码开始扫描和注册Bean,其中最重要的就是通过ConfigurationClassParser解析配置类

java 复制代码
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
   ...
   //此do while是为了处理 配置类中又导入(扫描)配置类,而这个配置类是一个标注了@Configuration的类
   do {
      //重点:开始解析配置类
      parser.parse(candidates);
      //校验所有的配置类
      parser.validate();

      //获得所有解析出来的配置类
      Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
      configClasses.removeAll(alreadyParsed);

      //创建转换器(ConfigurationClass -> BeanDefinitio)
      if (this.reader == null) {
         this.reader = new ConfigurationClassBeanDefinitionReader(
               registry, this.sourceExtractor, this.resourceLoader, this.environment,
               this.importBeanNameGenerator, parser.getImportRegistry());
      }
      //将所有的configClasses,内部的@Bean方法等等,注册到bean工厂中
      this.reader.loadBeanDefinitions(configClasses);
      //添加在已经处理的配置类集合中
      alreadyParsed.addAll(configClasses);

      //清除候选配置类,这里指的是最开始的配置类比如是启动类,而不是导入或者扫描的类
      candidates.clear();

      //最开始的候选配置类比bean工厂的bean数量要多,就说明通过候选配置类又导入了新类
      //而新类可能又需要进行导入操作
      if (registry.getBeanDefinitionCount() > candidateNames.length) {
         String[] newCandidateNames = registry.getBeanDefinitionNames();
         Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
         Set<String> alreadyParsedClasses = new HashSet<>();
         //alreadyParsedClasses表示这一次循环 已经导入到bean工厂的配置类
         for (ConfigurationClass configurationClass : alreadyParsed) {
            alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
         }
         for (String candidateName : newCandidateNames) {
            //如果是最开始的候选配置类就不用加入了
            if (!oldCandidateNames.contains(candidateName)) {
               BeanDefinition bd = registry.getBeanDefinition(candidateName);
               //如果不需要跳过,又不是才处理的配置类,那么就是新的配置类,需要再次执行
               if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
                     !alreadyParsedClasses.contains(bd.getBeanClassName())) {
                  candidates.add(new BeanDefinitionHolder(bd, candidateName));
               }
            }
         }
         candidateNames = newCandidateNames;
      }
   }
   while (!candidates.isEmpty());
   ...
}

2.4. ConfigurationClassParser

我们直接到parse(...) -> parse(...) -> processConfigurationClass(...)方法

这个方法就是为了通过doProcessConfigurationClass(...)方法递归处理候选配置类

java 复制代码
protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException {
   //判断这个配置类是否需要跳过
   if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
      return;
   }

   //看这个配置类是否已经被导入过
   ConfigurationClass existingClass = this.configurationClasses.get(configClass);
   //如果已经被导入过了
   if (existingClass != null) {
      //这个配置类又被其他类导入了
      if (configClass.isImported()) {
         //旧的也是被其他人导入的
         if (existingClass.isImported()) {
            //然后都合并到existingClass的importedBy中
            existingClass.mergeImportedBy(configClass);
         }
         //否则忽略新导入的配置类;现有的非导入类将覆盖它。
         return;
      }
      else {
         //到这就说明新的配置类不是被@Import导入的,那么就移除旧的,换新的
         this.configurationClasses.remove(configClass);
         this.knownSuperclasses.values().removeIf(configClass::equals);
      }
   }

   //递归处理有父类的情况
   SourceClass sourceClass = asSourceClass(configClass, filter);
   //do while 表示递归处理类,因为一个类可能有父类
   //如果存在父类,则需要将configClass变成sourceClass去解析,然后返回sourceClass的父类
   do {
      sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
   }
   while (sourceClass != null);

   //放入已经处理的配置类集合中
   this.configurationClasses.put(configClass, configClass);
}

doProcessConfigurationClass(...)方法中就会出现很多我们常见的注解了,这里就有关于@PropertySource的处理

java 复制代码
protected final SourceClass doProcessConfigurationClass(
      ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
      throws IOException {

   //判断是否标志了@Component注解
   if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
      //首先处理任何成员(嵌套)类
      processMemberClasses(configClass, sourceClass, filter);
   }

   //处理标注了@PropertySource注解的配置类
   for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
         sourceClass.getMetadata(), PropertySources.class,
         org.springframework.context.annotation.PropertySource.class)) {
      if (this.environment instanceof ConfigurableEnvironment) {
         //这个方法只是将当前需要加载的配置类加入到环境上下文的属性源集合中
         //毕竟现在bean还没初始化,还只是一个ConfigurationClass,甚至还不是BeanDefinition
         processPropertySource(propertySource);
      }
      else {
         logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
               "]. Reason: Environment must implement ConfigurableEnvironment");
      }
   }

   //处理标注了@ComponentScans和@ComponentScan的配置类
   Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
         sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
   //当设置对应的注解和是否需要跳过
   if (!componentScans.isEmpty() &&
         !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
      //遍历所有的componentScan注解:一个类可以使用多个ComponentScan和@ComponentScans
      for (AnnotationAttributes componentScan : componentScans) {
         //通过componentScan扫描器 获取满足条件的BeanDefinitionHolder
         //BeanDefinitionHolder是beanDefinition + beanName
         Set<BeanDefinitionHolder> scannedBeanDefinitions =
               this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
         //检查所有候选BeanDefinitionHolder,看是否是标志了@Configuration注解,并根据需要递归解析
         for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
            //获得原始BeanDefinition
            BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
            //有些可能就是原始的BeanDefinition,并没有进行封装,所有直接getBeanDefinition就是原始的BeanDefinition
            //像RootBeanDefinition会在代理中用到,他的getOriginatingBeanDefinition()就不为空
            if (bdCand == null) {
               bdCand = holder.getBeanDefinition();
            }
            //判断当前BeanDefinition是否标志了@Configuration注解
            if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
               //又递归解析
               parse(bdCand.getBeanClassName(), holder.getBeanName());
            }
         }
      }
   }

   //处理标注了@Imports注解
   processImports(configClass, sourceClass, getImports(sourceClass), filter, true);

   //处理标注了@ImportResource注解
   //看是否有关于@ImportResource注解的属性
   AnnotationAttributes importResource =
         AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
   if (importResource != null) {
      //获得需要导入的xml文件的位置
      String[] resources = importResource.getStringArray("locations");
      //获得怎么将xml中的bean转为BeanDefinition的转换器
      Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
      for (String resource : resources) {
         //通过上下文环境解析xml文件位置,如果找不到会抛出异常
         String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
         //加入到对应的集合中,后面会处理
         configClass.addImportedResource(resolvedResource, readerClass);
      }
   }

   //处理内部标注了@Bean的方法
   //返回内部标注了@Bean的方法元数据
   Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
   for (MethodMetadata methodMetadata : beanMethods) {
      //放入configClass中保存起来
      configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
   }

   //看class实现的接口内部是否有标注了@Bean的方法,如果有加入对应的集合中
   processInterfaces(configClass, sourceClass);

   //如果有父类,继续处理
   if (sourceClass.getMetadata().hasSuperClass()) {
      String superclass = sourceClass.getMetadata().getSuperClassName();
      //如果有父类,并且父类不是java开头的,并且还没有处理过,就继续递归调用
      if (superclass != null && !superclass.startsWith("java") &&
            !this.knownSuperclasses.containsKey(superclass)) {

         //加入已处理过父类集合中
         this.knownSuperclasses.put(superclass, configClass);
         //返回父类
         return sourceClass.getSuperClass();
      }
   }

   //没有父类,处理完成
   return null;
}

我们单拎出来有关@PropertySource的处理代码,这里很明显这里是当类上带有了@PropertySources之后执行processPropertySource(...)方法

紧接着我们看processPropertySource(...)方法,这个方法其实就是读取@PropertySource上配置的文件地址,然后读取这些配置文件,最终注册到Environment

java 复制代码
private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
   String name = propertySource.getString("name");
   if (!StringUtils.hasLength(name)) {
      name = null;
   }
   //获得编码格式
   String encoding = propertySource.getString("encoding");
   if (!StringUtils.hasLength(encoding)) {
      encoding = null;
   }
   //获得需要导入的配置文件
   String[] locations = propertySource.getStringArray("value");
   Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
   //获得找不到配置文件是否忽略的标志位
   boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");

   //获取专门用于创建属性源的工程
   Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
   //如果用户没有配置就用默认的DefaultPropertySourceFactory
   PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
         DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));

   //如果设置了配置文件,可能有多个
   for (String location : locations) {
      try {
         String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
         //获取当前的配置文件
         Resource resource = this.resourceLoader.getResource(resolvedLocation);
         //添加配置文件对应的属性源到环境上线文的属性源集合中
         addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
      }
      catch (IllegalArgumentException | FileNotFoundException | UnknownHostException | SocketException ex) {
         // Placeholders not resolvable or resource not found when trying to open it
         if (ignoreResourceNotFound) {
            if (logger.isInfoEnabled()) {
               logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());
            }
         }
         else {
            throw ex;
         }
      }
   }
}

到此为止,我们的配置类文件就已经注册到Environment中了,但是还没有将配置文件中的值设置到对应的实体中

3. ConfigurationPropertiesBindingPostProcessor

在Bean的生命周期中,SpringBoot提供了很多的回调类以及回调方法供我们使用, 其中ConfigurationPropertiesBindingPostProcessor正是其中之一,我们先看看他的结构

这个回调类正是负责处理@ConfigurationProperties的,其中postProcessBeforeInitialization(...)方法是当Bean初始化之前执行的,我们看实现是什么样子

紧接着我们进入bind(...)方法,此方法一进来就通过hasBoundValueObject(...)方法判断了bean是否需要绑定属性

hasBoundValueObject(...)方法其实就是要求bean的BeanDefinition是@ConfigurationProperties对应的BeanDefinition

由于这里的属性绑定层次很复杂,我这里就直接点出属性是从哪里获取的,我们可以直接在BinderfindProperty(...)方法中打Debug断点

这里是从Context中获取ConfigurationPropertySource,然后从ConfigurationPropertySource中获取属性 ,而这里面就有我们通过@PropertySource注册的user.yml

4. 总结

总结

  • ConfigurationClassParser中会将@PropertySource中的配置文件注册到容器中
  • 然后ConfigurationPropertiesBindingPostProcessorpostProcessBeforeInitialization(...)方法会见检测Bean是否带有的了@ConfigurationProperties注解
  • 一旦带有了就会从容器中注册的配置文件中读取属性然后设置到Bean中
相关推荐
Rocket MAN2 小时前
Spring Boot 缓存:工具选型、两级缓存策略、注解实现与进阶优化
spring boot·后端·缓存
谷哥的小弟2 小时前
Spring Framework源码解析——TaskExecutor
spring·源码
程序定小飞4 小时前
基于springboot的民宿在线预定平台开发与设计
java·开发语言·spring boot·后端·spring
FREE技术4 小时前
山区农产品售卖系统
java·spring boot
摇滚侠7 小时前
Spring Boot3零基础教程,云服务停机不收费,笔记71
java·spring boot·笔记
摇滚侠7 小时前
Spring Boot3零基础教程,监听 Kafka 消息,笔记78
spring boot·笔记·kafka
摇滚侠8 小时前
Spring Boot3零基础教程,RedisTemplate 定制化,笔记70
spring boot·笔记·后端
刘一说8 小时前
深入浅出 Spring Boot 自动配置(Auto-Configuration):原理、机制与最佳实践
java·spring boot·后端
顽疲10 小时前
SpringBoot + Vue 集成阿里云OSS直传最佳实践
vue.js·spring boot·阿里云