草捏以前寫過一篇《Spring源碼-循環依賴(附25張調試截圖)》,也算是對循環依賴研究了一番。但是今天仍是在循環依賴上踩坑了,真是被安排的明明白白。下面我講述下此次踩坑的過程,主要涉及的知識點有三個:模板方法、Bean加載順序和循環依賴。spring
此次踩坑的原由要從模板方法提及,最近寫的一個需求,在Manager中須要對A、B、C三類數據進行處理,處理過程相似且較多,而只是數據類型和細節上有些差別。爲了複用,天然想到了用模板方法重寫,這也是我第一次嘗試在Spring中使用模板方法,而後就踩坑了T T。緩存
下面我大概重現下場景,在Manager中有一個fun方法會根據傳入的type使用相應的工具類處理數據,工具類是經過屬性注入的UtilA、UtilB和UtilC。Manager中還有一個preHandle方法作一些數據預處理,後續會用到,但不是如今。app
@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) { // 我是一個假預處理...我什麼都沒作,嘿嘿 return data; } }
UtilA、UtilB和UtilC都繼承了一個模板類Template。process方法是一個模板方法用於處理數據,同時調用了doProcess抽象方法,其具體邏輯將由UtilA、UtilB和UtilC實現。ide
public abstract class Template { public void process(String data) { // 我是一個模板方法...我能夠作不少工做,免得兒子們都寫一遍 // 而特殊的工做交給doProcess由兒子們來具體實現 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,處理數據:" + data); } }
模板方法咱們都寫出來了,沒什麼問題。但如今我還有這樣一個需求,我要在process方法中調用Manager的preHandle方法(別問我爲啥不直接複製過來,實際狀況更復雜些,在preHandle中還用到了不少其餘方法和依賴,因此最好是複用),所以須要在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> T getBean(String name) { return (T) applicationContext.getBean(name); } }
而後是改寫Template類,在構造函數中得到Manager實例,而後在process方法就能夠順利調用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); }
下面是主函數,開始運行了:測試
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"); } }
調用manager的fun方法,因爲咱們傳入的參數是"A",因此將會使用utilA處理數據。一切看起來都很好,但這時候就遇到第一個問題了,啓動容器時,會加載UtilA,將調用構造器進行實例化,而在構造器中咱們指定經過SpringContextHolder的getBean方法來得到manager,這時因爲SpringContextHolder還未被加載,因此applicationContext是null,所以會報出空指針問題,因此咱們須要保證在加載UtilA以前先加載SpringContextHolder,也就是控制Bean的加載順序。咱們能夠藉助@DependsOn註解,加在UtilA上,並傳入參數「springContextHolder」,當加載UtilA時就會先完成SpringContextHolder的加載。操作系統
@Component @DependsOn("springContextHolder") public class UtilA extends Template { @Override protected void doProcess(String data) { System.out.println("我是A,處理數據:" + data); } }
這下搞定了,能跑了。當我把代碼上傳到測試環境,應用沒法啓動了。一看日誌,是發生了循環依賴,Spring容器起不來。仔細一看,確實發生了循環依賴。Manager中經過屬性注入UtilA,而UtilA的父類Template在構造函數中經過getBean得到Manger。但是問題來了,爲何我在本地能運行,而測試環境卻報錯了?說細點就是,爲何本地不會發生循環依賴,而測試環境會發生循環依賴。若是你以前看過《Spring源碼-循環依賴(附25張調試截圖)》或者對循環依賴有所瞭解,想必已經知道若是X和Y都是屬性注入的循環依賴,Spring能經過三級緩存解決,不會報錯,而對於X和Y都是構造器注入的循環依賴,Spring是沒法解決的,會報錯。如今的狀況是,我一處用了屬性注入,而另外一處用了構造器注入。因此猜測,在本地是先加載的Manager,先作的屬性注入,因此不報錯,而測試環境是先加載的UtilA,先作的構造器注入,因此產生循環依賴錯誤。爲何兩個環境的加載順序不一樣呢?查了些資料,Spring自動掃描的加載順序和hashCode有關,而hashCode和操做系統有關,因此兩個環境的操做系統不一樣可能會致使加載順序不一樣。這也就是本地環境和測試環境運行結果不一樣的緣由了。指針
下面說下怎麼解決這個問題,大概的思路有兩種:
第一種方法,就是不要在構造器中獲取依賴了,咱們能夠在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,處理數據:" + data); } }
我最後採用的是方法一,考慮的是隻須要修改一處便可,第二種方法須要修改三個子類,改動處較多。你們若是遇到這種問題,仍是根據本身的實際狀況來解決。
最後總結下,本身此次踩坑的緣由有兩點: