一文搞懂Spring依賴注入

在這裏插入圖片描述

前言

提起Spring,大家肯定不陌生,它是每一個Java開發者繞不過去的坎。Spring 框架爲基於 java 的企業應用程序提供了一整套解決方案,方便開發人員在框架基礎快速進行業務開發。

在官網中,我們發現它的核心技術之一:Dependency Injection,簡稱:DI ,翻譯過來就是依賴注入。今天我們就來盤一盤它。

在本文中,我們將深入研究 Spring 框架 DI背後的故事,包括 Spring Inversion of Control(控制反轉)、 DIApplicationContext 接口。 基於這些基本概念,我們將研究如何使用基於 java 和基於 XML 的配置來 創建Spring 應用程序。 最後,我們將探討在創建 Spring 應用程序時遇到的一些常見問題,包括 bean衝突循環依賴性

一 控制反轉(Inversion of Control)

在學習DI之前,我們先學習一下 IoC控制反轉),接下來的一段可能讀起來會讓你感覺比較囉嗦,但是要細細體會每一次改變的意圖,和我們的解決方案,對於理解控制反轉非常重要。

首先來了解下我們通常實例化一個對象的方式。 在 平時,我們使用 new 關鍵字實例化一個對象。 例如,如果有一個 Car 類,我們可以使用以下方法實例化一個對象 Car

Car car = new Car();

因爲汽車有很多零部件組成,我們定義Engine接口來模擬汽車引擎,然後將engine對象作爲成員變量放在Car

public interface Engine {
     void turnOn();
}

public class Car {

    private Engine engine;
    
    public Car() {}
    
    public void start() {

        engine.turnOn();

    }

}

現在,我們可以調用start()方法嗎?顯然是不行的,一眼可以看出會報NullPointerException (NPE),因爲我們沒有在Car的構造函數中初始化engine。通常我們採用的方案就是在Car的構造函數中覺得使用Engine接口的哪個實現,並直接將該實現分配給engine字段;

現在,我們來首先創建Engine接口的實現類

public class ElectricEngine implements Engine {
    @Override
    public void turnOn() {
        System.out.println("電動引擎啓動");
    }
}

public class CombustionEngine implements Engine {
    @Override
    public void turnOn() {
        System.out.println("燃油引擎啓動");
    }
}

我們修改Car的構造函數,使用ElectricEngine實現,將我們的engine字段分配給一個實例化的ElectricEngine對象

public class Car {

    private Engine engine;

    public Car() {
        this.engine = new ElectricEngine();
    }

    public void start() {

        engine.turnOn();

    }

    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}

現在我們執行start()方法,我們會看到如下輸出:

在這裏插入圖片描述

大功告成,我們成功解決了 NPE(空指針)問題,但是我們勝利了嗎?哈哈哈,顯然沒有!

在解決問題的同時,我們又引入了另一個問題。儘管我們通過抽象Engine接口,然後通過不同的Engine實現類來負責不同類型引擎的業務邏輯,的確是很好的設計策略。但是細心的夥伴可能已經發現了,我們Car類的構造函數中將engine聲明爲CombustionEngine,這將導致所有車都有一個燃油引擎。假如我們現在要創建不同的汽車對象,它有一個電動引擎,我們將不得不改變我們的設計。比較常見的方法是創建兩個獨立裏的類,各司其職,在他們的構造函數中將engine分配給Engine接口的不同實現;

例如:

public class CombustionCar {
    
    private Engine engine;
    
    public CombustionCar() {
        this.engine = new CombustionEngine();
    }
    
    public void start() {
        engine.turnOn();
    }

}

public class ElectricCar {
    private Engine engine;

    public ElectricCar() {
        this.engine = new ElectricEngine();
    }
    
    public void start() {
        engine.turnOn();
    }
    
}

通過上面的一頓操作,我們成功的解決了我們引擎的問題。如果是一個日常需求,我們已經可以成功交工了。但是這顯然不是我寫這篇文章的目的。

從設計的角度來說,目前的代碼是糟糕的,有以下兩點原因:

  1. 在兩個不同的類中,存在重複的start()方法;
  2. 我們需要爲每個新的Engine 實現類創建一個新的類;

尤其後一個問題更加難以解決,因爲我們不控制Engine的實現,隨着開發人員不斷的創建自己的實現類,這個問題會更加惡化;

帶着上面的問題,我們繼續思考…

我們可以創建一個父類Car,將公共代碼抽取到父類中,可以輕鬆解決第一個問題。由於Engine字段是私有的,我們在父類Car的構造函數中接收Engine對象,並且進行賦值。

public class Car {

    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.turnOn();
    }
}

public class CombustionCar extends Car{

    public CombustionCar() {
        super(new CombustionEngine());
    }

}

public class ElectricCar extends Car {

    public ElectricCar() {
        super(new ElectricEngine());
    }

}

通過這種方法,我們成功的解決了代碼重複的問題,我們來測試一下:

public class Car {

    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.turnOn();
    }

    public static void main(String[] args) {

        CombustionCar combustionCar1 = new CombustionCar();
        combustionCar1.start();
        ElectricCar electricCar1 = new ElectricCar();
        electricCar1.start();
    }
}

在這裏插入圖片描述

那麼我們該如何解決我們提出的第二個問題那?

其實這個問題我們可以換個角度看:爲什麼我們要去關注CombustionCarElectricCar,我們現在將關注點回到我們的Car,我們現在已經允許客戶端實例化Car對象時候將Engine對象作爲構造函數的參數傳入,其實已經消除了爲每個Engine對象創建新Car的問題。因爲現在Car類依賴於Engine接口,並不知道任何Engine的實現;

通過帶有Engine參數的構造函數,我們已將要使用哪個Engine實現的決定從Car類本身(最初由CombustionEngine決定)更改爲實例化Car類的客戶端。 決策過程的這種逆轉稱爲IoC原則。 現在,由客戶端控制使用哪種實現,而不是由Car類本身控制使用哪種Engine實現。

有點繞,大家結合下面的示例代碼,細細琢磨

public class Car {

    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.turnOn();
    }

    public static void main(String[] args) {

        /**
         * 老法子
         * 爲每一類型發送機的車創建類,然後實現父類car,然後在構造函數傳入自己的引擎,然後調用start()
         */
        CombustionCar combustionCar1 = new CombustionCar();
        combustionCar1.start();
        ElectricCar electricCar1 = new ElectricCar();
        electricCar1.start();

        /**
         * 控制反轉思想
         * 把自己看作實例化car的客戶端,需要什麼引擎,直接傳入相關對象
         */
        CombustionEngine combustionEngine = new CombustionEngine();
        Car combustionCar = new Car(combustionEngine);
        combustionCar.start();
        ElectricEngine electricEngine = new ElectricEngine();
        Car electricCar = new Car(electricEngine);
        electricCar.start();
    }
}

執行上面的代碼,我們發現都可以獲得我們想要的結果:

在這裏插入圖片描述

從上面的例子我們可以看到,實例化Car類的客戶端可以控制所使用的Engine實現,並且取決於將哪個Engine實現傳遞給Car構造函數,Car對象的行爲發生巨大變化。爲什麼這麼說,接着看下面

二 依賴注入(Dependency Injection)

在上面控制反轉的知識點,我們已經解決了由誰決定使用哪種Engine實現的問題,但是不可避免,我們也更改了實例化一個Car對象的步驟;

最開始,我們實例化Car不需要參數,因爲在它的構造函數裏面已經爲我們newEngine對象。使用IoC方法之後,我們要求在實例化一個Car之前,我們需要先創建一個Engine對象,並作爲參數傳遞給Car構造對象。換句話說,最初,我們首先實例化Car對象,然後實例化Engine對象。但是,使用IoC之後,我們首先實例化Engine對象,然後實例化Car對象;

因此,我們在上面的過程中創建了一個依賴關係。不過這種依賴關係不是指編譯時候Car類對Engine接口的依賴關係,相反,我們引入了一個運行時依賴關係。在運行時,實例化Car對象之前,必須首先實例化Engine對象。

2.1 依賴關係樹

某一個具體的依賴對象大家可以理解爲Spring中的bean,對於兩個有依賴關係的bean,其中被依賴的那個bean,我們把它稱爲依賴對象

我們用圖形化的方式來看看它們之間的依賴關係,其中圖形的節點代表對象,箭頭代表依賴關係(箭頭指向依賴對象)。對於我們我的Car類,依賴關係樹非常簡單:

如果依賴關係樹的終端結點還有自己的附加依賴關係,那麼這個依賴關係樹將變得更加複雜。現在再看我們上面的例子,如果CombustionEngine 還有其他依賴對象,我們首先需要創建CombustionEngine的依賴對象,然後才能實例化一個CombustionEngine對象。這樣在創建Car對象時候,才能將CombustionEngine傳遞給Car的構造函數;

//凸輪軸		
public class Camshaft {}
//機軸
public class Crankshaft {}

public class CombustionEngine implements Engine {

  //凸輪軸
  private Camshaft camshaft;

  //機軸
  private Crankshaft crankshaft;

  public CombustionEngine(Camshaft camshaft, Crankshaft crankshaft) {

      this.camshaft = camshaft;

      this.crankshaft = crankshaft;
  }

  @Override

  public void turnOn() {

      System.out.println("燃油引擎啓動");

  }

}

經過我們改造,我們現在的依賴關係樹變爲下面的樣子

2.2 依賴注入框架

隨着我們不斷引入更多的依賴關係,這種複雜性將繼續增長。爲了解決這個複雜問題,我們需要基於依賴關係樹抽取對象的創建過程。這就是依賴注入框架

一般來說,我們可以把這個過程分爲三個部分:

  1. 聲明需要創建的對象需要哪些依賴對象
  2. 註冊創建這些依賴對象所需要的類
  3. 提供一種使用1和2兩點思想創建對象的機制

通過反射,我們可以查看 Car 類的構造函數,並且知道它需要一個 Engine 參數。因此爲了創建Car對象,我們必須創建至少一個Engine接口的實現類用作依賴項來使用。在這裏,我們創建一個CombustionEngine 對象(爲了方便,暫時當做只有一個實現類,bean衝突問題待會再說)來聲明它作爲依賴項來使用,就滿足Car對象創建時的需求.

其實,這個過程是遞歸的,因爲CombustionEngine 依賴於其他對象,我們需要不斷重複第一個過程,直到把所有依賴對象聲明完畢,然後註冊創建這些依賴對象所需要的類。

第三點其實就是將前面兩點思想付諸實施,從而形成一種創建對象的機制

舉個例子:比如我們需要一個Car對象,我們必須遍歷依賴關係樹並檢查是否存在至少一個符合條件的類來滿足所有依賴關係。 例如,聲明CombustionEngine類可滿足Engine節點要求。 如果存在這種依賴關係,我們將實例化該依賴關係,然後移至下一個節點。

如果有一個以上的類滿足所需的依賴關係,那麼我們必須顯式聲明應該選擇哪一種依賴關係。 稍後我們將討論 Spring 是如何做到這一點的。

一旦我們確定所有的依賴關係都準備好了,我們就可以從終端節點開始創建依賴對象。 對於 Car 對象,我們首先實例化 CamshaftCrankshaftーー因爲這些對象沒有依賴關係ーー然後將這些對象傳遞給 CombustionEngine 構造函數,以實例化 CombunstionEngine 對象。 最後,我們將 CombunstionEngine 對象傳遞給 Car 構造函數,以實例化所需的 Car 對象。

瞭解了 DI 的基本原理之後,我們現在可以繼續討論 Spring 如何執行 DI

2.3 Spring的依賴注入

Spring的核心是一個DI框架,它可以將DI配置轉換爲Java應用程序。

在這裏我們要闡述一個問題:那就是庫和框架的區別。庫只是類定義的集合。背後的原因僅僅是代碼重用,即獲取其他開發人員已經編寫的代碼。這些類和方法通常在域特定區域中定義特定操作。例如,有一些數學庫可讓開發人員僅調用函數而無需重做算法工作原理的實現。

框架通常被認爲是一個骨架,我們在其中插入代碼以創建應用程序。 許多框架保留了特定於應用程序的部分,並要求我們開發人員提供適合框架的代碼。 在實踐中,這意味着編寫接口的實現,然後在框架中註冊實現。

2.4 ApplicationContext

Spring 中,框架圍繞 ApplicationContext 接口實現上一節中概述的三個 DI 職責。通常這個接口代表了一個上下文。 因此,我們通過基於 java 或基於 xml 的配置向 ApplicationContext 註冊合適的類,並從 ApplicationContext 請求創建 bean 對象。 然後 ApplicationContext 構建一個依賴關係樹並遍歷它以創建所需的 bean對象

Applicationcontext 中包含的邏輯通常被稱爲 Spring 容器。 通常,一個 Spring 應用程序可以有多個 ApplicationContext,每個 ApplicationContext 可以有單獨的配置。 例如,一個 ApplicationContext 可能被配置爲使用 CombustionEngine 作爲其引擎實現,而另一個容器可能被配置爲使用 ElectricEngine 作爲其實現。

在本文中,我們將重點討論每個應用程序的單個 ApplicationContext,但是下面描述的概念即使在一個應用程序有多個 ApplicationContext 實例時也適用。

三 基於 java 的配置

Spring爲我們提供了兩種基於 java 的配置方式

  1. 基本配置
  2. 自動配置

3.1 基於 java 的基本配置

基於java的基本配置的核心,其實是下面兩個註解:

  1. @Configuration: 定義配置類
  2. @Bean: 創建一個bean

例如,給出我們之前定義的 Car, CombustionEngine, Camshaft, 和Crankshaft 類,我們可以創建一個下面 的配置類:

/**
 * @author milogenius
 * @date 2020/5/17 20:52
 */
@Configuration
public class AnnotationConfig {
    
    @Bean
    public Car car(Engine engine) {
        return new Car(engine);
    }

    @Bean
    public Engine engine(Camshaft camshaft, Crankshaft crankshaft) {
        return new CombustionEngine(camshaft, crankshaft);
    }

    @Bean
    public Camshaft camshaft() {
        return new Camshaft();
    }

    @Bean
    public Crankshaft crankshaft() {
        return new Crankshaft();
    }
}

接下來,我們創建一個 ApplicationContext 對象,從 ApplicationContext 對象獲取一個 Car 對象,然後在創建的 Car 對象上調用 start 方法:

ApplicationContext context = 

    new AnnotationConfigApplicationContext(AnnotationConfig.class);

Car car = context.getBean(Car.class);

car.start();

執行結果如下:

Started combustion engine

雖然@Configuration@Bean 註解的組合爲 Spring 提供了足夠的信息來執行依賴注入,但我們仍然需要手動手動定義每個將被注入的 bean,並顯式地聲明它們的依賴關係。 爲了減少配置 DI 框架所需的開銷,Spring 提供了基於java的自動配置。

3.2 基於 java 的自動配置

爲了支持基於 java 的自動配置,Spring 提供了額外的註解。 雖然我們平時可能加過很多這種類型的註解,但是有三個最基本的註解:

  1. @Component: 註冊爲由 Spring 管理的類
  2. @Autowired: 指示 Spring 注入一個依賴對象
  3. @ComponentScan: 指示Spring在何處查找帶有@Component註解的類

3.2.1 構造函數注入

@Autowired註解用來指導 Spring ,我們打算在使用註解的位置注入一個依賴對象。 例如,在 Car 構造函數中,我們期望注入一個 Engine 對象,因此,我們給 Car 構造函數添加@Autowired註解。 通過使用@Component@Autowired註解改造我們Car類,如下所示:

@Component
public class Car {

  private Engine engine;

 
  @Autowired

  public Car(Engine engine) {

      this.engine = engine;
  }

  public void start() {

      engine.turnOn();

  }

}

我們可以在其他類中重複這個過程:

@Component

public class Camshaft {}

@Component
public class Crankshaft {}


@Component

public class CombustionEngine implements Engine {

 

 private Camshaft camshaft;

 private Crankshaft crankshaft;


 @Autowired

 public CombustionEngine(Camshaft camshaft, Crankshaft crankshaft) {

     this.camshaft = camshaft;

     this.crankshaft = crankshaft;

 }


 @Override

 public void turnOn() {

     System.out.println("Started combustion engine");

 }

}

改造完成相關類之後,我們需要創建一個@Configuration 類來指導 Spring 如何自動配置我們的應用程序。 對於基於 java 的基本配置,我們明確指示 Spring 如何使用@Bean 註解創建每個 bean,但在自動配置中,我們已經通過@Component@Autowired 註解提供了足夠的信息,說明如何創建所需的所有 bean。 唯一缺少的信息是 Spring 應該在哪裏尋找我們的帶有@Component註解的 類,並把它註冊爲對應的bean。

@ Componentscan 註釋包含一個參數 basePackages,它允許我們將包名稱指定爲一個 StringSpring 將通過遞歸搜索來查找@Component 類。 在我們的示例中,包是 com.milo.domain,因此,我們得到的配置類是:

@Configuration
@ComponentScan(basePackages = "com.milo.domain")
public class AutomatedAnnotationConfig {}

ApplicationContext context = 

    new AnnotationConfigApplicationContext(AutomatedAnnotationConfig.class);

Car car = context.getBean(Car.class);   

car.start();

執行結果:

Started combustion engine

通過和基於java的基礎配置比較,我們發現基於 java 的自動配置方法有兩個主要優點:

  1. 所需的配置要簡潔得多
  2. 註解直接應用於類,而不是在配置類

所以無特殊情況,自動配置是首選

3.2.2 字段注入

除了構造函數注入,我們還可以通過字段直接注入。 我們可以將@Autowired 註解應用到所需的字段來實現這一點:

@Component
public class Car {


  @Autowired

  private Engine engine;

  

  public void start() {

      engine.turnOn();

  }

}

這種方法極大地減少了我們的編碼壓力,但是它也有一個缺點,就是在使用字段之前,我們將無法檢查自動注入的對象是否爲空。

3.2.3 Setter注入

構造函數注入的最後一種替代方法是 setter 注入,其中@Autowired 註解應用於與字段關聯的 setter。 例如,我們可以改變 Car 類,通過 setter 注入獲得 Engine 對象,方法是用@Autowired註解 setEngine 方法:

@Component
public class Car {


  private Engine engine;

  

  public void start() {

      engine.turnOn();

  }


  public Engine getEngine() {

      return engine;

  }


  @Autowired

  public void setEngine(Engine engine) {

      this.engine = engine;

  }

}

Setter 注入類似於字段注入,但它允許我們與 注入對象交互。 在有些情況下,setter 注入可能特別有用,例如具有循環依賴關係,但 setter 注入可能是三種注入技術中最不常見的,儘可能優先使用構造函數注入

四 基於 xml 的配置

另一種配置方法是基於 xml 的配置。 我們在 XML 配置文件中定義 bean 以及它們之間的關係,然後指示 Spring 在哪裏找到我們的配置文件。

第一步是定義 bean。 我們基本遵循與基於 java 的基本配置相同的步驟,但使用 xmlbean 元素代替。 在 XML 的情況下,我們還必須顯式地聲明我們打算使用 constructor-arg 元素注入到其他構造函數中的 bean。 結合 beanconstructor-arg 元素,我們得到以下 XML 配置:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xmlns:util="http://www.springframework.org/schema/util"

  xsi:schemaLocation="

      http://www.springframework.org/schema/beans

      http://www.springframework.org/schema/beans/spring-beans.xsd

      http://www.springframework.org/schema/util

      http://www.springframework.org/schema/util/spring-util.xsd">


  <bean id="car" class="com.milo.domain.Car">

      <constructor-arg ref="engine" />

  </bean>

  

  <bean id="engine" class="com.milo.CombustionEngine">

      <constructor-arg ref="camshaft" />

      <constructor-arg ref="crankshaft" />

  </bean>

  

  <bean id="camshaft" class="com.milo.Camshaft" />

  <bean id="crankshaft" class="com.milo.Crankshaft" />


</beans>

在 bean 元素中,我們必須指定兩個屬性:

  1. id : bean 的唯一 ID (相當於帶有@Bean 註解方法名)
  2. class : 類的全路徑(包括包名)

對於 constructor-arg 元素,我們只需要指定 ref 屬性,它是對現有 bean ID 的引用。 例如,元素構造函數 <constructor-arg ref="engine" /> 規定,具有 ID engine(直接定義在 car bean 之下)的 bean 應該被用作注入 car bean 構造函數的 bean。

構造函數參數的順序由 constructor-arg 元素的順序決定。 例如,在定義 engine bean 時,傳遞給 CombustionEngine 構造函數的第一個構造函數參數是 camshaft bean,而第二個參數是 crankshaft bean。

獲取ApplicationContext對象,我們只需修改 ApplicationContext 實現類型。 因爲我們將 XML 配置文件放在類路徑上,所以我們使用 ClassPathXmlApplicationContext:

ApplicationContext context = 

    new ClassPathXmlApplicationContext("basic-config.xml");

Car car = context.getBean(Car.class);

car.start();

執行結果:

Started combustion engine

五 常見問題

現在,我們已經摸清了Spring框架如何進行DI,並正確地將所有依賴關係注入到我們的應用程序中,但是我們必須處理兩個棘手的問題:

  1. 依賴對象衝突
  2. 依賴對象間存在循環依賴

5.1 具有多個符合條件的依賴對象

在基於 java 和基於 xml 的方法中,我們已經指示 Spring 只使用 CombustionEngine 作爲我們的Engine實現。 如果我們將ElectricEngine註冊爲符合 di 標準的部件會發生什麼? 爲了測試結果,我們將修改基於 java 的自動配置示例,並用@Component 註解 ElectricEngine 類:

@Component
public class ElectricEngine implements Engine {


  @Override

  public void turnOn() {

      System.out.println("Started electric engine");

  }

}

如果我們重新運行基於 java 的自動配置應用程序,我們會看到以下錯誤:

No qualifying bean of type 'com.dzone.albanoj2.spring.di.domain.Engine' available: expected single matching bean but found 2: combustionEngine,electricEngine

由於我們已經註釋了用@Component 實現 Engine 接口的兩個類ーー即 CombustionEngineElectricEngine ーー spring 現在無法確定在實例化 Car 對象時應該使用這兩個類中的哪一個來滿足 Engine 依賴性。 爲了解決這個問題,我們必須明確地指示 Spring 使用這兩個 bean 中的哪一個。

5.1.1 @ Qualifier 註解

一種方法是給我們的依賴對象命名,並在應用@Autowired註解的地方使用@Qualifier註解來確定注入哪一個依賴對象。 所以,@Qualifier 註解限定了自動注入的 bean,從而將滿足需求的 bean 數量減少到一個。 例如,我們可以命名我們的CombustionEngine依賴對象:

@Component("defaultEngine")
public class CombustionEngine implements Engine {

    

    // ...代碼省略,未改變

}

然後我們可以添加@Qualifier 註解,其名稱和我們想要注入的依賴對象的名稱保持一致,這樣,我們Engine 對象在 Car 構造函數中被自動注入

@Component
public class Car {

  

  @Autowired

  public Car(@Qualifier("defaultEngine") Engine engine) {

      this.engine = engine;

  }

  

  // ...existing implementation unchanged...

}

如果我們重新運行我們的應用程序,我們不再報以前的錯誤:

Started combustion engine

注意,如果沒有顯式申明bean名稱的類都有一個默認名稱,該默認名稱就是類名首字母小寫。 例如,我們的 Combusttionengine 類的默認名稱是 combusttionengine

5.1.2 @ Primary 註解

如果我們知道默認情況下我們更喜歡一個實現,那麼我們可以放棄@Qualifier 註釋,直接將@Primary 註釋添加到類中。 例如,我們可以將我們的 CombusttionengineElectricEngineCar 類更改爲:

@Component
@Primary

public class CombustionEngine implements Engine {

  

   // ...existing implementation unchanged...

}

@Component
public class ElectricEngine implements Engine {

  

    // ...existing implementation unchanged...

}


@Component
public class Car {

 

 @Autowired

 public Car(Engine engine) {

     this.engine = engine;

 }

 

 // ...existing implementation unchanged...

}

我們重新運行我們的應用程序,我們會得到以下輸出:

Started combustion engine

這證明,雖然有兩種可能性滿足 Engine 依賴性,即 CombustionEngineElectricengine,但 Spring 能夠根據@Primary 註釋決定兩種實現中哪一種應該優先使用。

5.2 循環依賴

雖然我們已經深入討論了 Spring DI 的基礎知識,但是還有一個主要問題沒有解決: 如果依賴關係樹有一個循環引用會發生什麼? 例如,假設我們創建了一個 Foo 類,它的構造函數需要一個 Bar 對象,但是 Bar 構造函數需要一個 Foo 對象。

我們可以使用代碼實現上面問題:

@Component
public class Foo {



  private Bar bar;


  @Autowired

  public Foo(Bar bar) {

      this.bar = bar;

  }

}


@Component
public class Bar {



 private Foo foo;


 @Autowired

 public Bar(Foo foo) {

     this.foo = foo;

 }

}

然後我們可以定義以下配置:

@Configuration
@ComponentScan(basePackageClasses = Foo.class)
public class Config {}

最後,我們可以創建我們的 ApplicationContext:

ApplicationContext context = 

    new AnnotationConfigApplicationContext(Config.class);

Foo foo = context.getBean(Foo.class);

當我們執行這個代碼片段時,我們看到以下錯誤:

Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'bar': Requested bean is currently in creation: Is there an unresolvable circular reference?

首先,Spring 嘗試創建 Foo 對象。 在這個過程中,Spring 認識到需要一個 Bar 對象。 爲了構造 Bar 對象,需要一個 Foo 對象。 由於 Foo 對象目前正在構建中(這也是創建 Bar 對象的原因) ,spring 認識到可能發生了循環引用。

這個問題最簡單的解決方案之一是在一個類和注入點上使用@Lazy註解。 這指示 Spring 推遲帶註解的 bean 和帶註釋的@Autowired 位置的初始化。 這允許成功地初始化其中一個 bean,從而打破循環依賴鏈。 理解了這一點,我們可以改變 FooBar 類:

@Component
public class Foo {

  

  private Bar bar;


  @Autowired

  public Foo(@Lazy Bar bar) {

      this.bar = bar;

  }

}


@Component

@Lazy

public class Bar {


  @Autowired

  public Bar(Foo foo) {}

}

如果使用@Lazy 註解後重新運行應用程序,沒有發現報告任何錯誤。

六 總結

在本文中,我們探討了 Spring 的基礎知識,包括 IoCDISpring ApplicationContext。 然後,我們介紹了使用基於 java 的配置和基於 xml 的配置創建 Spring 應用程序的基本知識,同時研究了使用 Spring DI 時可能遇到的一些常見問題。 雖然這些概念一開始可能晦澀難懂,與 Spring 代碼脫節,但是我們可以從基底層認識Spirng,希望對大家有所幫助,謝謝大家。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章