读下源码,具体分析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以后新建并更新上去的。
我还能说什么呢?
感觉这个问题可以给官方提bug了
我遇到的也是这个问题,debug了两天,也和原来的2.0.4版本的对比,发现的也是
public @interface EnableConfigurationProperties
这个annotation中import的class不一样,之前是EnableConfigurationPropertiesImportSelector.class
现在是EnableConfigurationPropertiesRegistrar.class
目前我出得问题也是能够在Spring容器中找到bean,但是属性全为空