記一次循環依賴踩坑

草捏以前寫過一篇《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和操做系統有關,因此兩個環境的操做系統不一樣可能會致使加載順序不一樣。這也就是本地環境和測試環境運行結果不一樣的緣由了。指針

下面說下怎麼解決這個問題,大概的思路有兩種:

  1. 去除構造器依賴;
  2. 控制加載順序。

第一種方法,就是不要在構造器中獲取依賴了,咱們能夠在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);
 }
}

我最後採用的是方法一,考慮的是隻須要修改一處便可,第二種方法須要修改三個子類,改動處較多。你們若是遇到這種問題,仍是根據本身的實際狀況來解決。

最後總結下,本身此次踩坑的緣由有兩點:

  1. 在學習循環依賴時,只考慮到了X和Y都用屬性注入或構造器注入,沒思考過X使用屬性注入、Y使用構造器注入是否會發生循環依賴問題。
  2. 對Bean的加載順序缺少關注。爲了保證程序的正確運行,Bean的加載順序須要保證正確。
相關文章
相關標籤/搜索