SpringBoot 自動配置失效的解決方法
本文源自近期項目中遇到的問題, bug 總是出現在你自以為是的地方…
問題描述
下面是一個簡單復現的代碼片段,在你沒有閱讀完本文時,如果能做出正確的判斷,那恭喜你可以節省閱讀本文的時間瞭。
1、自動配置類:AutoTestConfiguration
@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
@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 註解標註的
@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 未刷新
兩個點:
- AutoTestConfiguration#testBean 執行會輸出一個 log(用於判斷 AutoTestConfiguration 是否正常刷新)
- 監聽 ApplicationReadyEvent 事件,拿 test.enable 值(用於判端配置是否正常加載,也就是 TestProperties 是否被正常刷新)
代碼如下:
@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 項目:
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,源碼如下:
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 的具體代碼
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 是有記錄的:github.com/spring-proj…
不過這裡還是通過分析代碼來還原下問題產生的根本原因;這裡主要從兩個方面來分析:
- @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 類中
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
到此這篇關於SpringBoot 自動配置失效的解決方法的文章就介紹到這瞭,更多相關SpringBoot 自動配置失效內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 聊聊SpringBoot自動裝配的魔力
- springboot自定義starter方法及註解實例
- springboot中一些比較常用的註解總結
- 淺談SpringBoot實現自動裝配的方法原理
- 使用@ConditionalOnProperty控制是否加載的操作