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

读下源码,具体分析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) {
        try {
            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以后新建并更新上去的。
我还能说什么呢?
 
 
 

           


3 COMMENTS

  1. 1112020-03-14 22:49

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

  2. jxsrlsl12342020-08-17 15:47

    我遇到的也是这个问题,debug了两天,也和原来的2.0.4版本的对比,发现的也是
    public @interface EnableConfigurationProperties
    这个annotation中import的class不一样,之前是EnableConfigurationPropertiesImportSelector.class
    现在是EnableConfigurationPropertiesRegistrar.class
    目前我出得问题也是能够在Spring容器中找到bean,但是属性全为空

EA PLAYER &

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

      00:00/00:00