詳談spring boot中幾種常見的依賴注入問題
最近有空總結(jié)一下之前在使用spring boot時(shí)遇到過的幾種依賴注入時(shí)的坑,如果不了解spring內(nèi)部的處理過程,使用起來總是感覺有種迷糊。
在分析場(chǎng)景前,需要大概了解一下spring對(duì)于bean的實(shí)例化過程是需要先注冊(cè)BeanDefinition信息然后才進(jìn)行實(shí)例化,在org.springframework.context.support.AbstractApplicationContext#refresh中定義的基本的流程。部分代碼
try { // Allows post-processing of the bean factory in context subclasses. postProcessBeanFactory(beanFactory); // 1. 包含了BeanDefinition注冊(cè)過程 invokeBeanFactoryPostProcessors(beanFactory); // Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory); // Initialize message source for this context. initMessageSource(); // Initialize event multicaster for this context. initApplicationEventMulticaster(); // Initialize other special beans in specific context subclasses. onRefresh(); // Check for listener beans and register them. registerListeners(); // 2. 根據(jù)BeanDefinition處理Bean實(shí)例化過程 finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event. finishRefresh(); }
@Autowired依賴注入問題–邏輯使用先于@Autowired注解處理
之前不熟悉spring bean的實(shí)例化過程可能會(huì)遇到的坑就是使用@Autowired依賴注入的對(duì)象是null沒有注入到相應(yīng)的對(duì)象里面,或者準(zhǔn)確的來說是在我程序的某一塊邏輯代碼執(zhí)行時(shí)使用到@Autowired依賴的bean,但是bean確實(shí)null。
這種場(chǎng)景一般就是在當(dāng)時(shí),@Autowired注解并沒有被處理,所以依賴的bean為null。
如果了解spring boot自動(dòng)化裝配可以直達(dá)我們通過實(shí)現(xiàn)ImportBeanDefinitionRegistrar接口自定義注冊(cè)BeanDefinition信息,實(shí)現(xiàn)邏輯是ImportBeanDefinitionRegistrar#registerBeanDefinitions的具體實(shí)現(xiàn)中,這個(gè)方法的執(zhí)行過程屬于
// 1. 包含了BeanDefinition注冊(cè)過程 invokeBeanFactoryPostProcessors(beanFactory);
之前曾經(jīng)遇到過在自定義的ImportBeanDefinitionRegistrar實(shí)現(xiàn)類中,使用@Autowired依賴某個(gè)bean,但是在使用時(shí)無法得到具體的實(shí)現(xiàn)對(duì)象,因?yàn)锧Autowired注解的處理過程是在
//2. 根據(jù)BeanDefinition處理Bean實(shí)例化過程 finihBeanFactoryInitialization(beanFactory);
當(dāng)程序執(zhí)行ImportBeanDefinitionRegistrar#registerBeanDefinitions時(shí),依賴的bean為null,報(bào)空指針。
測(cè)試用例
引導(dǎo)程序代碼
@SpringBootApplication(scanBasePackages = "garine.learn.test.auwired") @Import(TestAutowiredRegistar.class) public class BootstrapTestApplication3 { public static void main(String[] args) { ApplicationContext applicationContext = SpringApplication.run(BootstrapTestApplication3.class, args); } }
@Component public class TestAutowiredRegistar implements ImportBeanDefinitionRegistrar,BeanFactoryAware,InitializingBean{ private BeanFactory beanFactory; @Autowired TestRegistarDependOn testRegistarDependOn; @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { System.out.println(TestAutowiredRegistar.class.getSimpleName() + "-----" +testRegistarDependOn); } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } @Override public void afterPropertiesSet() throws Exception { System.out.println(TestAutowiredRegistar.class.getSimpleName() + "-----" + testRegistarDependOn); } }
@Component public class TestRegistarDependOn { }
執(zhí)行輸出:TestAutowiredRegistar-----null
**這種場(chǎng)景一般就是在當(dāng)時(shí),@Autowired注解并沒有被處理,所以依賴的bean為null。**如果遇到依賴注入為空時(shí),如果確定已經(jīng)定義了對(duì)應(yīng)的bean,那么不妨看看代碼使用依賴bean時(shí),到底@Autowired注解有沒有被處理。
這種場(chǎng)景的解決辦法就是使用BeanFactory來獲取bean,修改代碼如下。
@Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { System.out.println(TestAutowiredRegistar.class.getSimpleName() + "-----" +testRegistarDependOn); System.out.println(TestAutowiredRegistar.class.getSimpleName() + "-----" +beanFactory.getBean(TestRegistarDependOn.class)); }
再次執(zhí)行輸出:
TestAutowiredRegistar-----null
TestAutowiredRegistar-----garine.learn.test.auwired.TestRegistarDependOn@7808fb9
BeanFactory.getBean問題–getBean調(diào)用先于BeanDefinition信息注冊(cè)
這里就是beanFactory.getBean方法如果獲取不到bean就會(huì)調(diào)用bean的初始化過程。但是需要注意bean對(duì)應(yīng)的BeanDefinition信息必須已經(jīng)注冊(cè)完成。所以這種getBean的方式不是絕對(duì)安全。
一般而言ConfigurationClassParser#processConfigurationClass為入口,可以看到整個(gè)對(duì)Configclass的處理過程。對(duì)于@Configuration標(biāo)注的類都是有排序的,排序在前的先進(jìn)行處理。
那么會(huì)不會(huì)出現(xiàn)在ImportBeanDefinitionRegistrar#registerBeanDefinitions中使用beanFactory.getBean方法獲取bean而報(bào)錯(cuò)的場(chǎng)景呢?答案是會(huì),假如定義兩個(gè)@Configuration標(biāo)注的類,a和b,a先于b處理,a通過@Import導(dǎo)入TestAutowiredRegistar,b中定義TestRegistarDependOn的bean實(shí)例化方法,代碼如下。
配置類:
@Configuration @Order(1) @Import(TestAutowiredRegistar.class) public class FirstConfig { //1.先處理TestAutowiredRegistar的ImportBeanDefinitionRegistrar#registerBeanDefinitions
@Configuration @Order(2) public class SecondConfig { @Bean public TestRegistarDependOn testRegistarDependOn(){ return new TestRegistarDependOn(); } }
TestRegistarDependOn去掉@Component注解,避免被掃描到提前注冊(cè)BeanDefinition
引導(dǎo)程序,去掉提前Import TestAutowiredRegistar.class
@SpringBootApplication(scanBasePackages = "garine.learn.test.auwired") public class BootstrapTestApplication3 { public static void main(String[] args) { ApplicationContext applicationContext = SpringApplication.run(BootstrapTestApplication3.class, args); } }
執(zhí)行啟動(dòng)程序,輸出報(bào)錯(cuò)信息
A component required a bean of type ‘garine.learn.test.auwired.TestRegistarDependOn' that could not be found.
所以實(shí)際上在getBean時(shí),如果bean的BeanDefinition并沒有注冊(cè)到Beanfactory,那么久會(huì)報(bào)出上述錯(cuò)誤。
把兩個(gè)配置類的@Order順序換一下,就能處理成功,執(zhí)行輸出。--------------------------然并卵。。。一樣報(bào)錯(cuò),
what?源碼里面明明有排序的,在
org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions
對(duì)所有的配置類都有
// Sort by previously determined @Order value, if applicable configCandidates.sort((bd1, bd2) -> { int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition()); int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition()); return Integer.compare(i1, i2); });
經(jīng)過調(diào)試發(fā)現(xiàn),應(yīng)該是在spring.factories中定義的Configuration類才會(huì)在這里做處理,可以稱之為最高優(yōu)先級(jí)配置,對(duì)于這些配置@Order才會(huì)起作用。
那么我們自定義的@Configuration標(biāo)注的類在哪里處理?經(jīng)過調(diào)試,定位在@ComponentScan注解處理處,有如下代碼。
// Process any @ComponentScan annotations Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { for (AnnotationAttributes componentScan : componentScans) { // The config class is annotated with @ComponentScan -> perform the scan immediately Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); // Check the set of scanned definitions for any further config classes and parse recursively if needed for (BeanDefinitionHolder holder : scannedBeanDefinitions) { BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition(); if (bdCand == null) { bdCand = holder.getBeanDefinition(); } //判斷掃描到的是不是配置類,是的話就進(jìn)行配置處理 if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) { parse(bdCand.getBeanClassName(), holder.getBeanName()); } } } }
也就是說,我們一般自定義的配置順序@Order是不起作用的,全靠掃描文件得到的先后順序,所以,文件名稱是關(guān)鍵。。
這里把FirstConfig改成TFirstConfig試試,輸出
TestAutowiredRegistar-----null
TestAutowiredRegistar-----garine.learn.test.auwired.TestRegistarDependOn@189b5fb1
所以猜想通過。總結(jié)就是@Order對(duì)spring.factories中定義的配置類起作用,我們自定義的配置類處理順序需要文件名稱來控制。
在Configuration中使用@Autowired注解
通過調(diào)試都可以知道@Bean注冊(cè)實(shí)例的方式,實(shí)際代理調(diào)用@Bean修飾的方法是在
// 2. 根據(jù)BeanDefinition處理Bean實(shí)例化過程 finishBeanFactoryInitialization(beanFactory);
的過程中的,所以假如在@Bean修飾的方法中使用到@Autowired注解依賴的bean是怎么樣的場(chǎng)景?
spring 實(shí)例化Bean過程
先了解一下實(shí)例化bean的過程是怎么樣的。在finishBeanFactoryInitialization中,遍歷所有的BeanDefinition信息來實(shí)例化bean。遍歷的順序調(diào)試幾次后發(fā)現(xiàn)是按照注冊(cè)BeanDefinition信息的先后順序。所以可以有幾個(gè)簡(jiǎn)單規(guī)則可以判斷哪個(gè)bean會(huì)先實(shí)例化。
- 同是@ComponentScan掃描注冊(cè)的bean,按照class文件名順序,排在前面的先注冊(cè),所以先實(shí)例化,例如 ATest類實(shí)例化先于BTest類.
- @Conguration 配置類實(shí)例化先于其內(nèi)部定義的@Bean方法執(zhí)行實(shí)例化,例如Config類實(shí)例化先于其內(nèi)部任意@Bean 方法實(shí)例化bean。
那么考慮,假如在@Conguration 修飾的類的@Bean方法里面使用@Autowired引入依賴,而這個(gè)依賴實(shí)例化順序要比@Conguration 修飾的類要遲,會(huì)怎么樣?
定義下面三個(gè)類,在同一個(gè)包里面順序也是如下所示:
ConfigInitBean
TestDependOnConfig
ZTestConfigurationDependOn
各自代碼如下:
public class ConfigInitBean { }
@Configuration public class TestDependOnConfig { @Autowired ZTestConfigurationDependOn zTestConfigurationDependOn;//觀察這個(gè)依賴什么時(shí)候進(jìn)行初始化,斷點(diǎn)getBean調(diào)試 @Bean public ConfigInitBean configInitBean(){ zTestConfigurationDependOn.saySome(); return new ConfigInitBean(); } }
@Component public class ZTestConfigurationDependOn { public void saySome(){ System.out.println("say some"); } }
在DefaultListableBeanFactory#preInstantiateSingletons方法中斷點(diǎn)查看beanNames的順序
根據(jù)spring 對(duì)@Configuration標(biāo)注的類的處理過程,能夠?qū)?yīng)的上,先掃描到TestDependOnConfig所以先注冊(cè),ZTestConfigurationDependOn后掃描所以比TestDependOnConfig實(shí)例化要晚。ConfigInitBean是由@Bean定義的,在對(duì)配置類的處理中,都是先處理完@ComponentScan的BeanDefinition注冊(cè),再處理@Bean、@Import導(dǎo)入的配置、@ImportResource導(dǎo)入的xml等等BeanDefinition注冊(cè)。
總結(jié)來說就是bean實(shí)例化的順序符合猜想,實(shí)際上還有一點(diǎn)就是每個(gè)bean實(shí)例化時(shí),都會(huì)對(duì)其@Autowired注解的依賴進(jìn)行注入,如果當(dāng)時(shí)依賴沒有實(shí)例化,就根據(jù)依賴的BeanDefinition進(jìn)行g(shù)etBean過程所以一般情況下,我們平常使用業(yè)務(wù)代碼模型都不會(huì)出現(xiàn)注入為null問題。
當(dāng)然,如果依賴的Beandefinition不存在,那么就會(huì)報(bào)錯(cuò):
Consider defining a bean of type ‘XXXx' in your configuration.
在這里例子中,TestDependOnConfig依賴ZTestConfigurationDependOn,但是比ZTestConfigurationDependOn實(shí)例化要早,所以會(huì)調(diào)用getBean.ZTestConfigurationDependOn,提前實(shí)例化ZTestConfigurationDependOn來注入依賴。
具體源碼在AbstractAutowireCapableBeanFactory#populateBean方法中,填充bean。
for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof InstantiationAwareBeanPostProcessor) { InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; //填充屬性 pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName); if (pvs == null) { return; } } }
填充具體使用的實(shí)現(xiàn)方法是AutowiredAnnotationBeanPostProcessor#postProcessPropertyValues來進(jìn)行填充屬性,最終會(huì)調(diào)用依賴bean的getBean,從而實(shí)例化依賴的bean或者直接獲取依賴bean。
所以就算是配置類也好,普通的組件也好,都會(huì)在實(shí)例化時(shí)注入@Autowired依賴的屬性實(shí)例,如果是該實(shí)例沒有定義BeanDefinition,那么就會(huì)無法注入。
@Bean內(nèi)部使用配置類@Autowired注解引入依賴
了解完上面的過程,可以知道,@Bean方法是在finishBeanFactoryInitialization過程實(shí)例化對(duì)應(yīng)的bean時(shí)才會(huì)被代理調(diào)用,并且順序比對(duì)應(yīng)配置類要后,這時(shí)對(duì)應(yīng)配置類早已經(jīng)實(shí)例化完畢,依賴屬性也已經(jīng)注入,可以放心在@Bean方法內(nèi)部使用。
還有一種情況是,假如@Bean方法被提前調(diào)用,例如@Bean的實(shí)例被另一個(gè)比@Bean所在配置類還要早實(shí)例化的組件中引入,那么此時(shí)@Bean所在配置類還沒實(shí)例化,這樣調(diào)用會(huì)出錯(cuò)嗎?答案是不會(huì),因?yàn)闅w根到底,@Bean方法的調(diào)用都是代理方式,程序還是需要先實(shí)例化一個(gè)@Bean所在配置類的實(shí)例,才能進(jìn)行@Bean方法的調(diào)用,從而實(shí)例化一個(gè)@Bean方法的bean。
InitializingBean#afterPropertiesSet內(nèi)部使用依賴
了解到上面的知識(shí),推測(cè)一下,在InitializingBean#afterPropertiesSet里面使用@Autowired依賴進(jìn)行邏輯處理是否可以?看如下InitializingBean#afterPropertiesSet的調(diào)用時(shí)機(jī)。
try { //填充依賴屬性 populateBean(beanName, mbd, instanceWrapper); //最終調(diào)用到InitializingBean#afterPropertiesSet方法 exposedObject = initializeBean(beanName, exposedObject, mbd); }
所以,InitializingBean#afterPropertiesSet是在填充玩依賴屬性之后調(diào)用的,因此可以使用依賴的bean進(jìn)行一些邏輯操作的。
總結(jié)
1、所以總結(jié)來說就是,我們的代碼邏輯在
// 根據(jù)BeanDefinition處理Bean實(shí)例化過程 finishBeanFactoryInitialization(beanFactory);
過程之前都沒有使用的@Autowired依賴bean的話,那是沒問題的,因?yàn)锧Autowired注解處理都是在finishBeanFactoryInitialization()也就是bean實(shí)例化時(shí)才會(huì)進(jìn)行處理。如果使用了,那就是空指針;
2、在@Bean方法內(nèi)部使用@Autowired注解的依賴,只要設(shè)計(jì)好程序也是可以的的,只要依賴的BeanDefinition已經(jīng)注冊(cè)過,配置類實(shí)例化時(shí)就能主動(dòng)發(fā)起依賴的實(shí)例化過程,然后注入依賴,不會(huì)出現(xiàn)空指針。
而@Bean方法是在finishBeanFactoryInitialization過程實(shí)例化對(duì)應(yīng)的bean時(shí)才會(huì)被代理調(diào)用,并且順序比對(duì)應(yīng)配置類要后,這時(shí)對(duì)應(yīng)配置類早已經(jīng)實(shí)例化完畢,依賴屬性也已經(jīng)注入,可以放心在@Bean方法內(nèi)部使用。
所以這里@Bean方法實(shí)例化bean時(shí)如果使用到@Autowired依賴的bean時(shí),就對(duì)配置類的實(shí)例有很強(qiáng)的依賴性,這種依賴順序spring都幫我們保證先實(shí)例化配置類,再調(diào)用@Bean方法。
3、InitializingBean#afterPropertiesSet內(nèi)部使用也是沒問題,原理如上。所以只要理解在使用到@Autowired的依賴時(shí),到底在哪個(gè)時(shí)機(jī),就能分析清楚是不是適合使用。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java集合類的組織結(jié)構(gòu)和繼承、實(shí)現(xiàn)關(guān)系詳解
這篇文章主要介紹了Java集合類的組織結(jié)構(gòu)和繼承、實(shí)現(xiàn)關(guān)系,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11Spring?boot2.0?實(shí)現(xiàn)日志集成的方法(2)
這篇文章主要介紹了Spring?boot2.0?實(shí)現(xiàn)日志集成的方法,上一章講解了spring?boot日志簡(jiǎn)單集成,這篇我們將日志進(jìn)行分類,常規(guī)日志、異常日志、監(jiān)控日志等,需要將日志輸出到不同的文件,具體內(nèi)容需要的小伙伴可以參考一下2022-04-04Java實(shí)現(xiàn)簡(jiǎn)單的飛機(jī)大戰(zhàn)游戲(敵機(jī)下落篇)
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)簡(jiǎn)單的飛機(jī)大戰(zhàn)游戲,敵機(jī)下落篇,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05SSH框架網(wǎng)上商城項(xiàng)目第21戰(zhàn)之詳解易寶支付的流程
這篇文章主要為大家詳細(xì)介紹了SSH框架網(wǎng)上商城項(xiàng)目第21戰(zhàn)之易寶支付的流程,感興趣的小伙伴們可以參考一下2016-06-06SpringBoot使用@ResponseBody返回圖片的實(shí)現(xiàn)
這篇文章主要介紹了SpringBoot使用@ResponseBody返回圖片的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11Java編程在ICPC快速IO實(shí)現(xiàn)源碼
這篇文章主要介紹了Java Fast IO in ICPC實(shí)現(xiàn)源碼,具有一定參考價(jià)值,需要的朋友可以了解下。2017-09-09java抓取12306信息實(shí)現(xiàn)火車余票查詢示例
這篇文章主要介紹了java抓取12306信息實(shí)現(xiàn)火車余票查詢示例,需要的朋友可以參考下2014-04-04Spring Boot Starters簡(jiǎn)介及其優(yōu)劣勢(shì)
在這篇文章中,我們將向你介紹Spring Boot Starters,并將討論Spring Boot Starters的優(yōu)點(diǎn)和優(yōu)勢(shì),感興趣的朋友跟隨腳本之家小編一起學(xué)習(xí)吧2018-05-05