前言
提起Spring
,大家肯定不陌生,它是每一個Java開發者繞不過去的坎。Spring
框架爲基於 java
的企業應用程序提供了一整套解決方案,方便開發人員在框架基礎快速進行業務開發。
在官網中,我們發現它的核心技術之一:Dependency Injection
,簡稱:DI
,翻譯過來就是依賴注入。今天我們就來盤一盤它。
在本文中,我們將深入研究 Spring
框架 DI
背後的故事,包括 Spring Inversion of Control
(控制反轉)、 DI
和 ApplicationContext
接口。 基於這些基本概念,我們將研究如何使用基於 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();
}
}
通過上面的一頓騷操作,我們成功的解決了我們引擎的問題。如果是一個日常需求,我們已經可以成功交工了。但是這顯然不是我寫這篇文章的目的。
從設計的角度來說,目前的代碼是糟糕的,有以下兩點原因:
- 在兩個不同的類中,存在重複的
start()
方法; - 我們需要爲每個新的
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();
}
}
那麼我們該如何解決我們提出的第二個問題那?
其實這個問題我們可以換個角度看:爲什麼我們要去關注CombustionCar
和ElectricCar
,我們現在將關注點回到我們的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
不需要參數,因爲在它的構造函數裏面已經爲我們new
了Engine
對象。使用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兩點思想創建對象的機制
通過反射,我們可以查看 Car
類的構造函數,並且知道它需要一個 Engine
參數。因此爲了創建Car對象,我們必須創建至少一個Engine
接口的實現類用作依賴項來使用。在這裏,我們創建一個CombustionEngine
對象(爲了方便,暫時當做只有一個實現類,bean衝突問題待會再說)來聲明它作爲依賴項來使用,就滿足Car
對象創建時的需求.
其實,這個過程是遞歸的,因爲CombustionEngine
依賴於其他對象,我們需要不斷重複第一個過程,直到把所有依賴對象聲明完畢,然後註冊創建這些依賴對象所需要的類。
第三點其實就是將前面兩點思想付諸實施,從而形成一種創建對象的機制
舉個例子:比如我們需要一個Car
對象,我們必須遍歷依賴關係樹並檢查是否存在至少一個符合條件的類來滿足所有依賴關係。 例如,聲明CombustionEngine
類可滿足Engine
節點要求。 如果存在這種依賴關係,我們將實例化該依賴關係,然後移至下一個節點。
如果有一個以上的類滿足所需的依賴關係,那麼我們必須顯式聲明應該選擇哪一種依賴關係。 稍後我們將討論 Spring 是如何做到這一點的。
一旦我們確定所有的依賴關係都準備好了,我們就可以從終端節點開始創建依賴對象。 對於 Car
對象,我們首先實例化 Camshaft
和Crankshaft
ーー因爲這些對象沒有依賴關係ーー然後將這些對象傳遞給 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
的配置方式
- 基本配置
- 自動配置
3.1 基於 java 的基本配置
基於java
的基本配置的核心,其實是下面兩個註解:
@Configuration
: 定義配置類@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
提供了額外的註解。 雖然我們平時可能加過很多這種類型的註解,但是有三個最基本的註解:
@Component
: 註冊爲由 Spring 管理的類@Autowired
: 指示 Spring 注入一個依賴對象@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
,它允許我們將包名稱指定爲一個 String
,Spring
將通過遞歸搜索來查找@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
的自動配置方法有兩個主要優點:
- 所需的配置要簡潔得多
- 註解直接應用於類,而不是在配置類
所以無特殊情況,自動配置是首選
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
。 結合 bean
和 constructor-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 元素中,我們必須指定兩個屬性:
id
: bean 的唯一 ID (相當於帶有@Bean
註解方法名)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
,並正確地將所有依賴關係注入到我們的應用程序中,但是我們必須處理兩個棘手的問題:
- 依賴對象衝突
- 依賴對象間存在循環依賴
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
接口的兩個類ーー即 CombustionEngine
和ElectricEngine
ーー 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
註釋添加到類中。 例如,我們可以將我們的 Combusttionengine
、 ElectricEngine
和 Car
類更改爲:
@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
依賴性,即 CombustionEngine
和 Electricengine
,但 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,從而打破循環依賴鏈。 理解了這一點,我們可以改變 Foo
和 Bar
類:
@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
的基礎知識,包括 IoC
、 DI
和 Spring ApplicationContext
。 然後,我們介紹了使用基於 java
的配置和基於 xml
的配置創建 Spring
應用程序的基本知識,同時研究了使用 Spring DI
時可能遇到的一些常見問題。 雖然這些概念一開始可能晦澀難懂,與 Spring
代碼脫節,但是我們可以從基底層認識Spirng
,希望對大家有所幫助,謝謝大家。