首页 > 经验记录 > java > 读下源码,具体分析SpringBoot2.2版本后用@EnableConfigurationProperties + @PropertySource 指定配置文件时遇到的陨石坑

读下源码,具体分析SpringBoot2.2版本后用@EnableConfigurationProperties + @PropertySource 指定配置文件时遇到的陨石坑

 

前情提要:

本来我是有在写一个自己的项目, 有一些配置类想将其配置变得优雅一些,就想着用配置文件的形式。也方便打包编译的时候快速切换不同环境。

由于是SpringBoot项目,那么用配置文件的形式呢,当然少不了@ConfigurationProperties 这个注解

使用了这个注解来将配置文件的值注入到配置类之中呢,我还不满足。  想着配置文件都放一个application.yml里或者bootstrap.yml里,也挺丑陋的。

就一个顺手将配置文件分离了,然后用 @PropertySource 来指定配置类所使用的配置文件具体路径。

然后我没有使用 @Component、@Configuration 之类的注解来将其注册到Spring上下文中。

使用的是 @EnableConfigurationProperties 这个注解,  为什么呢。这是因为使用此种方式可以在一个地方加载到所有的配置类,比较符合单一职责原则。以后配置多了要找的话比起每个类自己注册自己也要方便的多。

大体的话是一个这样子的形式:

 

 

根据我的经验来说是没有问题的。

可是偏偏它就出了问题了

 

 

 

问题说明:

开发环境:

IntelliJ IDEA:  2019.3

SpringBoot version:  2.2.4

 

项目启动后出现了奇怪的情景,启动没有报错,但是配置类中的属性注入失败了。

而启动没有报错,我又打断点看了一下使用此配置的地方。

就发现这个配置类可以被 Spring 成功的注入(即已作为一个 Bean 被 Spring 管理),但是里面的值却又都是默认值

我一时以为是我 yml 配置写错了,或者说我实现了 PropertySourceFactory 的加用来载 yml 配置文件的工厂类内部逻辑有问题。

 

之后就是各种测试,搞到后面心态都有些崩了

具体做了哪些实验就不说了,总之浪费了挺多无意义的时间。最终确定了几个情形:

1、 使用 @EnableConfigurationProperties 可以成功将 application.yml 中的配置加载进 Bean

2、若使用 @PropertySource 指定配置文件,则 @EnableConfigurationProperties 无法将指定的配置文件参数注入进 Bean

3、无论如何,在@EnableConfigurationProperties 设置的配置类都会被 Spring 实例化。

4、@PropertySource 指定配置文件后,若是在类上使用 @Configuration、@Component 注解形式来实现IOC,则 Spring 可以成功将配置文件的值注入进 Bean

 

 

出现了这种问题。就很令人疑惑,而我在网上找的资料都说 @EnableConfigurationProperties 可以正确加载配置文件。

而到了我这,这个Bean生成是生成了,但这个配置文件里的值怎么都注入不进去,就很怪。必须要用 @Component 这种注解形式来注册 Bean 才行。

我不禁陷入了深深的思考。

 

 

 

具体分析:

既然遇到了这种问题,也没在网上找到具体的原因。那我就自己来分析分析,为什么会出现这种情况。

分析的话,那就只能看源码咯

我们首先来看一下@PropertySource这个东西是怎么被Spring解析出来的,  看下具体的源码,分析一下流程,先看看是不是在解析过程中出现的问题。

 

 

@PropertySource 在 Spring Bean生命周期中的具体解析流程

我们点开 PropertySource.class 文件, 在 IDEA 中按 ctrl+鼠标左键点击一下类名。 可以找到在什么地方引入了此class。

很轻松的可以定位到一个方法, 只有在这个方法之中, 才会被处理: org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass

这是他的判定逻辑( 为了方便观看,我去掉了其他的注解判定逻辑 ):

 

继续,深入到  processPropertySource() 方法的源码中, 看看他是怎么处理的。

这里我注释加的比较详细,  可以看到就是在这个方法内部根据@PropertySource 中定义的所有参数对我们具体的类做了处理,将配置文件的属性都注入进去。

 

一直往上翻动, 找到调用 ConfigurationClassParser#doProcessConfigurationClass 此方法最起始的入口点,最终我找到的是Spring的这个类 : ConfigurationClassPostProcessor

他的定义是这样子的:

 

他是一个 BeanDefinitionRegistryPostProcessor 的实现类。

而 BeanDefinitionRegistryPostProcessor 这个类熟悉 Spring Bean 生命周期的就知道,这玩意是用来增强 BeanDefinition 的。

是一个在 Spring 的 Bean 生命周期非常靠前的处理钩子,此时这个 BeanDefinition 还在解析中,都没注册到 BeanFactory 里去。

看名字其实就知道, ConfigurationClassPostProcessor 这个类是专门扫描、解析、注册所有配置类的。

而判断是否为配置类的方法我看了一下,里面写的是有@Configuration、@Component、@ComponentScan、@Import、@ImportResource、@Bean 这些注解定义的/加载的Class就是配置类。

 

结论:

可以知道, 声明为配置类从而初始化Bean实例,这样子不会有问题。

正常注册到Spring容器内部的Bean定义, 类上使用@PropertySource 注解可以成功的被 ConfigurationClassPostProcessor 这个类处理,最终交给ConfigurationClassParser#doProcessConfigurationClass() 来解析。

 

 

@EnableConfigurationProperties 在 Spring Bean生命周期中的具体解析流程

看了下 @PropertySource 的解析流程,没发现问题,那就只能再看下 @EnableConfigurationProperties 究竟干了些什么咯

首先要做的是先点开 @EnableConfigurationProperties 这个注解

他是这样定义的:

利用的Spring @Import 机制,来将 ImportBeanDefinitionRegistrar 实现导入, 从而对@EnableConfigurationProperties 内包含的内容进行解析。

他这个具体实现的源码读起来非常简单:

流程就是这么简单几步:

1、 初始化好BeanDefinition注册器

2、取得 @EnableConfigurationProperties  value属性表示的所有 Class 对象

3、调用注册器的 register(Class class) 方法,将这些 Class表示的对象生成 BeanDefinition 注册到 Spring 上下文里

 

而 Spring 的 @Import 机制这里就得简单说一下。

根据我上边说明的@PropertySource 处理流程就可以知道,ConfigurationClassPostProcessor 是一个对所有的配置类进行处理的类。

而这个 @Import 注解,自然也会被其所解析。

然后我打了个debug看了下, 发现他是 Spring 在解析 @SpringBootApplication 这个启动类注解的时候,通过 ConfigurationClassBeanDefinitionReader 类的 loadBeanDefinitions() 方法顺带解析出来的。

深入到此方法里边去几层就可以找到 loadBeanDefinitionsFromRegistrars()这个方法, Spring 就是使用这个方法专门处理 @Import 注解。

 

loadBeanDefinitionsFromRegistrars 方法逻辑是这样的:

如果该类有@Import,且Import进来的类实现了ImportBeanDefinitionRegistrar接口,则调用Import进来的类的registerBeanDefinitions方法。

 

而@EnableConfigurationProperties 导入的 EnableConfigurationPropertiesRegistrar 究竟做了什么,上面已经解释的很清楚了。

他是手动将配置类生成出来然后直接生成 BeanDefinition 再将其注册到 BeanFactory 中的。

 

 

 

 

魔法解开了

我就说为什么。原因经过这么一顿分析以后总算是明白了。

使用注解来实现IOC,会经过完整的 Bean 生命周期,所以 ConfigurationClassPostProcessor 会成功的处理相应配置。

EnableConfigurationPropertiesRegistrar 是在处理@SpringBootApplication这个配置时加载出来的。ConfigurationClassPostProcessor 经过倒是也经过了,不过处理的是项目启动类。

EnableConfigurationPropertiesRegistrar 它内部实现加载 @ConfigurationProperties 修饰的类时,都不会走那个完整的Bean 生命周期,直接生成 BeanDefinition 就往 BeanFactory 里塞了。

所以也没有地方会对 @PropertySource 注解进行处理了。

 

那为啥网上的人说 @EnableConfigurationProperties 可以成功的导入自定义配置呢? 我看了下,@EnableConfigurationProperties 他在SpringBoot 2.2.0以前 @Import 导入的不是 EnableConfigurationPropertiesRegistrar 这个类

这个类是在SpringBoot 2.2.0以后新建并更新上去的。

我还能说什么呢?

 

 

 

           


1 COMMENT

  1. 1112020-03-14 22:49

    感觉这个问题可以给官方提bug了

EA PLAYER &

历史记录 [ 注意:部分数据仅限于当前浏览器 ]清空

      00:00/00:00