这是我参与11月更文挑战的第 10 天,活动详情查看:2021最后一次更文挑战
本文源自近期项目中遇到的问题, bug 总是出现在你自以为是的地方...
问题描述
下面是一个简单复现的代码片段,在你没有阅读完本文时,如果能做出正确的判断,那恭喜你可以节省阅读本文的时间了。
1、自动配置类:AutoTestConfiguration
java @Configuration @EnableConfigurationProperties(TestProperties.class) @ConditionalOnProperty(prefix = "test", name = "enable") public class AutoTestConfiguration { @Bean @ConditionalOnMissingBean public TestBean testBean(TestProperties properties){ System.out.println("this is executed....."); return new TestBean(); } }
2、配置类 TestProperties
java @ConfigurationProperties(prefix = "test") public class TestProperties { private boolean enable = true; public boolean isEnable() { return enable; } public void setEnable(boolean enable) { this.enable = enable; } }
这两个类都在 root package
下,可以保证能够正常被 Spring
扫描到;那么问题是 TestBean
会不会被正常创建?当然这里的结论是不会。
可能有的同学会说你的 TestProperties
没有加 @Configuration
注解,Spring
不认识它,那真的是这样吗?很显然也不是。
在排查这个问题的过程中,也有遇到其他问题,也是之前没有遇到过的;即使 Spring
源码我看过很多遍,但是仍然会有一些边边角角让你意想不到的地方;下面就针对这个问题,慢慢来揭开它的面纱。
@EnableConfigurationProperties 注解行为
在之前的版本中,TestProperties
是有被 @Configuration
注解标注的 java @Configuration // 可以被 spring 扫描 @ConfigurationProperties(prefix = "test") public class TestProperties { private boolean enable = true; public boolean isEnable() { return enable; } public void setEnable(boolean enable) { this.enable = enable; } }
常规的思路是,当 TestProperties
被扫描到之后,spring env 中就会有 test.enable=true
的 k-v 存在,当执行 AutoTestConfiguration
自动配置类刷新时,@ConditionalOnProperty(prefix = "test", name = "enable")
则会生效,进而 TestBean
被正常创建。
但事实并非如此,下面是对于此问题的验证
配置有效,AutoTestConfiguration 未刷新
两个点:
- 1、AutoTestConfiguration#testBean 执行会输出一个 log(用于判断 AutoTestConfiguration 是否正常刷新)
- 监听 ApplicationReadyEvent 事件,拿
test.enable
值(用于判端配置是否正常加载,也就是 TestProperties 是否被正常刷新)
代码如下:
java @SpringBootApplication public class Application implements ApplicationListener<ApplicationReadyEvent> { @Autowired private ApplicationContext applicationContext; public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Override public void onApplicationEvent(ApplicationReadyEvent event) { System.out.println(this.applicationContext.getEnvironment().getProperty("test.enable") + "------"); } }
执行得到的结果是 AutoTestConfiguration#testBean
没有被执行,但test.enable
为 true。
这里说明 TestProperties
是有被刷新的,但是并没有对 @ConditionalOnProperty
起到作用,那么这里基本可以猜到是自动配置类上的 @ConditionalOnProperty
和 @EnableConfigurationProperties
的作用顺序问题。
在验证顺序问题之前,我尝试在 application.properties 中增加如下配置,re run 项目:
properties test.enable=true
到这里我得到了另一个 bean 冲突的问题。
prefix-type
异常提示如下: Parameter 0 of method testBean in com.glmapper.bridge.boot.config.AutoTestConfiguration required a single bean, but 2 were found: - testProperties: defined in file [/Users/glmapper/Documents/project/exception-guides/target/classes/com/glmapper/bridge/boot/config/TestProperties.class] - test-com.glmapper.bridge.boot.config.TestProperties: defined in null
这里出现了 test-com.glmapper.bridge.boot.config.TestProperties
这个 name 的 bean。我尝试在代码中去检查是否有显示给定这个 bean 名字,但是没有找到,那只有一种可能,就是这个是被 spring 自己创建的。
这个过程在 spring 刷新阶段非常靠前,在排查这个问题时,还是耽误了一些时间,最后还是把问题定位一致前置到 beandefinitions 初始化才找到。
这里是 @EnableConfigurationProperties
注解的一个行为,依赖 EnableConfigurationPropertiesRegistrar
,源码如下:
```java class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegistrar { .getQualifiedAttributeName(EnableConfigurationPropertiesRegistrar.class, "methodValidationExcludeFilter");
@Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { registerInfrastructureBeans(registry); registerMethodValidationExcludeFilter(registry); ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry); // to register getTypes(metadata).forEach(beanRegistrar::register); } ```
通过代码比较容易看出,EnableConfigurationPropertiesRegistrar 会将目标 metadata 注册成 bean;继续 debug,找到了产生 prefix-type 格式 name 的 bean。
下面是 getName 的具体代码
java private String getName(Class<?> type, MergedAnnotation<ConfigurationProperties> annotation) { // 拿 prefix String prefix = annotation.isPresent() ? annotation.getString("prefix") : ""; // prefix + "-" + 类全限定名 return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName() : type.getName()); }
到这里我们先明确一个问题:
如果你使用
@EnableConfigurationProperties
来开启配置类,那么就不要在配置类上使用@Configuration 等能够被 Spring scan 识别到的注解,以免在后续的使用中同一类型的 bean 多个实例
@ConditionalOnProperty
在回到配置不生效问题上来,这里在官方 issue 是有记录的:https://github.com/spring-projects/spring-boot/issues/2282。
不过这里还是通过分析代码来还原下问题产生的根本原因;这里主要从两个方面来分析:
- @ConditionalOnProperty match 值逻辑,需要明确在匹配 value 时,从哪些 PropertySource 读取的。
- @ConditionalOnProperty match 失败和 bean 刷新的逻辑
@ConditionalOnProperty match 逻辑
首先是 @ConditionalOnProperty 在执行计算时,匹配 value 的值来源问题,通过 debug 代码很容易就得到了所有的 source 来源,如下图:
从 debug 看,本案例有 4 个来源(具体如上图),实际上从源码来看,source 涵盖了 spring env 所有来源:
[ConfigurationPropertySourcesPropertySource {name='configurationProperties'}, StubPropertySource {name='servletConfigInitParams'}, StubPropertySource {name='servletContextInitParams'}, PropertiesPropertySource {name='systemProperties'}, OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}, RandomValuePropertySource {name='random'}, OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}]
所以本文案例中不生效原因就是上面这些 PropertySource 都没有 test.enable,也就是 TestProperties 没被刷新,或者其在自动配置类之后才刷新。
@ConditionalOnProperty skip 逻辑
这里主要解释 @ConditionalOnPropert 和 bean 被刷新的逻辑关系,具体实现在 ConditionEvaluator 类中
java public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) { // 1、没有 Conditional 注解,则扫描时不会跳过当前 bean // 2、遍历 conditions 进行判断是否满足 }
所以对于自动配置类上的注解,Conditional 是作为当前类是否允许被刷新的前提,只有 Conditional 条件满足,才会将当前的自动配置类加入到待刷新 bean 列表中去,如果 Conditional 不满足,这个 bean 将直接被跳过,不会被放到 BeandefinitonMap 中去,也就不会有后续的刷新动作。
@ConditionalOnProperty 作用时机在 BeanDefiniton 被创建之前,其执行时机要比 @EnableConfigurationProperties
作用要早,这也就说明了,为什么 TestProperties 中 test.enable=true, AutoTestConfiguration
也不会刷新的原因了。
总结
本文通过一个简单 case,对于项目中遇到的 SpringBoot 配置失效导致 bean 未被刷新问题进行了回溯,总结如下: * Conditional 相关注解对于自动配置类来说,作用时机较早,用于决定当前自动配置类是否允许被刷新 * @EnableConfigurationProperties enable 的类,会默认注册一个 bean,bean 名字格式为 prefix-type