記一次循環(huán)依賴踩坑

下面我講述下這次踩坑的過程,主要涉及的知識點(diǎn)有三個:模板方法、Bean加載順序和循環(huán)依賴。
這次踩坑的起因要從模板方法說起,最近寫的一個需求,在Manager中需要對A、B、C三類數(shù)據(jù)進(jìn)行處理,處理過程類似且較多,而只是數(shù)據(jù)類型和細(xì)節(jié)上有些差異。為了復(fù)用,自然想到了用模板方法重寫,這也是我第一次嘗試在Spring中使用模板方法,然后就踩坑了T T。
下面我大概重現(xiàn)下場景,在Manager中有一個fun方法會根據(jù)傳入的type使用相應(yīng)的工具類處理數(shù)據(jù),工具類是通過屬性注入的UtilA、UtilB和UtilC。Manager中還有一個preHandle方法做一些數(shù)據(jù)預(yù)處理,后續(xù)會用到,但不是現(xiàn)在。
@Component
public?class?Manager?{
?@Autowired
?private?UtilA?utilA;
?@Autowired
?private?UtilB?utilB;
?@Autowired
?private?UtilC?utilC;
?public?void?fun(String?type,?String?data)?{
??switch?(type)?{
???case?"A"?:
????utilA.process(data);
????break;
???case?"B"?:
????utilB.process(data);
????break;
???case?"C":
????utilC.process(data);
????break;
???default:
????utilA.doProcess(data);
??}
?}
?public?String?preHandle(String?data)?{
??//?我是一個假預(yù)處理...我什么都沒做,嘿嘿
??return?data;
?}
}
UtilA、UtilB和UtilC都繼承了一個模板類Template。process方法是一個模板方法用于處理數(shù)據(jù),同時調(diào)用了doProcess抽象方法,其具體邏輯將由UtilA、UtilB和UtilC實現(xiàn)。
public?abstract?class?Template?{
?public?void?process(String?data)?{
????????//?我是一個模板方法...我可以做很多工作,省得兒子們都寫一遍
????????//?而特殊的工作交給doProcess由兒子們來具體實現(xiàn)
??doProcess(data);
?}
?protected?abstract?void?doProcess(String?data);
}
以UtilA為例,如下:
@Component
public?class?UtilA?extends?Template?{
?@Override
?protected?void?doProcess(String?data)?{
??System.out.println("我是A,處理數(shù)據(jù):"?+?data);
?}
}
模板方法我們都寫出來了,沒什么問題。但現(xiàn)在我還有這樣一個需求,我要在process方法中調(diào)用Manager的preHandle方法(別問我為啥不直接復(fù)制過來,實際情況更復(fù)雜些,在preHandle中還用到了很多其他方法和依賴,所以最好是復(fù)用),因此需要在Template中獲得Manager的實例,可是Template是一個抽象類,都沒法實例化成Bean,更別提依賴注入了。這里我的解決辦法是,引入了一個SpringContextHolder,這是一個ApplicationContext的包裝類,通過它來獲得Manager實例,其定義如下:
@Component
public?class?SpringContextHolder?implements?ApplicationContextAware?{
?private?static?ApplicationContext?applicationContext;
?@Override
?public?void?setApplicationContext(ApplicationContext?context)?throws?BeansException?{
??applicationContext?=?context;
?}
?public?static??T?getBean(String?name)?{
??return?(T)?applicationContext.getBean(name);
?}
?
}
然后是改寫Template類,在構(gòu)造函數(shù)中獲得Manager實例,然后在process方法就可以順利調(diào)用preHandle方法了。
public?abstract?class?Template?{
?private?Manager?manager;
?public?Template()?{
??manager?=?SpringContextHolder.getBean("manager");
?}
?public?void?process(String?data)?{
??manager.preHandle(data);
??doProcess(data);
?}
?protected?abstract?void?doProcess(String?data);
}
下面是主函數(shù),開始運(yùn)行了:
public?class?Main?{
?public?static?void?main(String[]?args)?{
??ApplicationContext?context?=?new?ClassPathXmlApplicationContext("spring-context.xml");
??Manager?manager?=?(Manager)?context.getBean("manager");
??manager.fun("A",?"123");
?}
}
調(diào)用manager的fun方法,由于我們傳入的參數(shù)是"A",所以將會使用utilA處理數(shù)據(jù)。一切看起來都很好,但這時候就遇到第一個問題了,啟動容器時,會加載UtilA,將調(diào)用構(gòu)造器進(jìn)行實例化,而在構(gòu)造器中我們指定通過SpringContextHolder的getBean方法來獲得manager,這時由于SpringContextHolder還未被加載,所以applicationContext是null,因此會報出空指針問題,所以我們需要保證在加載UtilA之前先加載SpringContextHolder,也就是控制Bean的加載順序。我們可以借助@DependsOn注解,加在UtilA上,并傳入?yún)?shù)“springContextHolder”,當(dāng)加載UtilA時就會先完成SpringContextHolder的加載。
@Component
@DependsOn("springContextHolder")
public?class?UtilA?extends?Template?{
?@Override
?protected?void?doProcess(String?data)?{
??System.out.println("我是A,處理數(shù)據(jù):"?+?data);
?}
}
這下搞定了,能跑了。當(dāng)我把代碼上傳到測試環(huán)境,應(yīng)用無法啟動了。一看日志,是發(fā)生了循環(huán)依賴,Spring容器起不來。仔細(xì)一看,確實發(fā)生了循環(huán)依賴。Manager中通過屬性注入UtilA,而UtilA的父類Template在構(gòu)造函數(shù)中通過getBean獲得Manger。可是問題來了,為什么我在本地能運(yùn)行,而測試環(huán)境卻報錯了?說細(xì)點(diǎn)就是,為什么本地不會發(fā)生循環(huán)依賴,而測試環(huán)境會發(fā)生循環(huán)依賴。如果你之前看過《Spring源碼-循環(huán)依賴(附25張調(diào)試截圖)》或者對循環(huán)依賴有所了解,想必已經(jīng)知道如果X和Y都是屬性注入的循環(huán)依賴,Spring能通過三級緩存解決,不會報錯,而對于X和Y都是構(gòu)造器注入的循環(huán)依賴,Spring是無法解決的,會報錯。現(xiàn)在的情況是,我一處用了屬性注入,而另一處用了構(gòu)造器注入。所以猜想,在本地是先加載的Manager,先做的屬性注入,所以不報錯,而測試環(huán)境是先加載的UtilA,先做的構(gòu)造器注入,所以產(chǎn)生循環(huán)依賴錯誤。為什么兩個環(huán)境的加載順序不同呢?查了些資料,Spring自動掃描的加載順序和hashCode有關(guān),而hashCode和操作系統(tǒng)有關(guān),所以兩個環(huán)境的操作系統(tǒng)不同可能會導(dǎo)致加載順序不同。這也就是本地環(huán)境和測試環(huán)境運(yùn)行結(jié)果不同的原因了。
下面說下怎么解決這個問題,大概的思路有兩種:
去除構(gòu)造器依賴; 控制加載順序。
第一種方法,就是不要在構(gòu)造器中獲取依賴了,我們可以在process方法中獲取:
public?abstract?class?Template?{
?private?Manager?manager;
?public?Template()?{
?}
?public?void?process(String?data)?{
??manager?=?SpringContextHolder.getBean("manager");
??manager.preHandle(data);
??doProcess(data);
?}
?protected?abstract?void?doProcess(String?data);
}
第二種方法,就是控制Manager始終在UtilA之前加載,利用@DependsOn注解:
@Component
@DependsOn({"springContextHolder",?"manager"})
public?class?UtilA?extends?Template?{
?@Override
?protected?void?doProcess(String?data)?{
??System.out.println("我是A,處理數(shù)據(jù):"?+?data);
?}
}
我最后采用的是方法一,考慮的是只需要修改一處即可,第二種方法需要修改三個子類,改動處較多。大家如果遇到這種問題,還是根據(jù)自己的實際情況來解決。
最后總結(jié)下,自己這次踩坑的原因有兩點(diǎn):
在學(xué)習(xí)循環(huán)依賴時,只考慮到了X和Y都用屬性注入或構(gòu)造器注入,沒思考過X使用屬性注入、Y使用構(gòu)造器注入是否會發(fā)生循環(huán)依賴問題。 對Bean的加載順序缺乏關(guān)注。為了保證程序的正確運(yùn)行,Bean的加載順序需要保證正確。
