19

读下源码,具体分析SpringBoot2.2版本后用@EnableConfigurationProperties + @Propert...

 3 years ago
source link: https://www.skypyb.com/2020/02/jishu/1393/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

前情提要:

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

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

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

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

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

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

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

@ConfigurationProperties(prefix = "myprefix")
@PropertySource(
        value = "classpath:myconfig.yml",
        factory = YamlPropertySourceFactory.class
@Data
public class MyProperties {
    //...
@Configuration
@EnableConfigurationProperties({MyProperties.class})
public class ApplicationConfig {

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

可是偏偏它就出了问题了

问题说明:

开发环境:

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

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

protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
        throws IOException {
    //...
    // 处理定义了 @PropertySources 注解的类
    for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
            sourceClass.getMetadata(), PropertySources.class,
            org.springframework.context.annotation.PropertySource.class)) {
        if (this.environment instanceof ConfigurableEnvironment) {
            processPropertySource(propertySource);
        } else {
            logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
                    "]. Reason: Environment must implement ConfigurableEnvironment");
    //...
    return null;

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

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");
    //是否忽略找不到的property source
    boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");
    //取得设置的属性来源工厂。 默认的是 DefaultPropertySourceFactory。 只能加载 .properties 文件
    Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
    PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
            DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));
    //遍历资源文件, 处理占位符后得到具体的资源(比如文件流)
    for (String location : locations) {
            String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
            Resource resource = this.resourceLoader.getResource(resolvedLocation);
            //使用上面得到的工厂来处理资源生成属性源, 这一步的具体操作就是可以自己实现来定义的。 比如实现一个yml处理工厂
            addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
        } catch (IllegalArgumentException | FileNotFoundException | UnknownHostException 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;

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

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

他的定义是这样子的:

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,
PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {

他是一个 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 这个注解

他是这样定义的:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesRegistrar.class)
public @interface EnableConfigurationProperties {
* The bean name of the configuration properties validator.
* @since 2.2.0
String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";
* Convenient way to quickly register
* {@link ConfigurationProperties @ConfigurationProperties} annotated beans with
* Spring. Standard Spring Beans will also be scanned regardless of this value.
* @return {@code @ConfigurationProperties} annotated beans to register
Class<?>[] value() default {};

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

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

class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
registerInfrastructureBeans(registry);
ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry);
getTypes(metadata).forEach(beanRegistrar::register);
private Set<Class<?>> getTypes(AnnotationMetadata metadata) {
return metadata.getAnnotations().stream(EnableConfigurationProperties.class)
.flatMap((annotation) -> Arrays.stream(annotation.getClassArray(MergedAnnotation.VALUE)))
.filter((type) -> void.class != type).collect(Collectors.toSet());
@SuppressWarnings("deprecation")
static void registerInfrastructureBeans(BeanDefinitionRegistry registry) {
ConfigurationPropertiesBindingPostProcessor.register(registry);
ConfigurationPropertiesBeanDefinitionValidator.register(registry);
ConfigurationBeanFactoryMetadata.register(registry);

流程就是这么简单几步:

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以后新建并更新上去的。

我还能说什么呢?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK