依賴注入(DI)是一個處理對象的依賴的過程,換句話說就是處理與某個對象一起工作的其它對象。所依賴的對象可以通過構造參數、工廠方法參數以及設置對象實例的屬性來定義。當一個對象被創建時,IOC 容器會注入它的依賴,這個過程與傳統創建對象的方式是相反的故稱為控制反轉(IOC)。
 
使用DI后代碼十分的簡潔,并且在定義依賴時也能有效的解耦。對象不需要過多的去關注它依賴的對象,所以類更容易測試,特別是依賴于接口或者是抽象類時,可以在單元測試中使用stub或者mock實現。
 
DI存在兩種主要的形式:基于構造器注入和Setter方法注入。
* * * * *
 
### **基于構造器注入**
構造注入通過容器提供指定的參數調用構造方法來完成,每個參數都稱為依賴。這和通過提供給靜態的工廠方法的參數來創建對象幾乎是等價的。下面的樣例展示了一個類通過構造方法來完成依賴注入的過程。
~~~
public class SimpleMovieLister {
// SimpleMovieLister依賴于MovieFinder
private MovieFinder movieFinder;
// Spring通過構造器注入MovieFinder實例
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
//使用被注入MovieFinder實例來完成的業務邏輯已被忽略
}
~~~
注意:這個類是普通的Java對象,不依賴于容器相關的接口、基類或者注解。
### **構造參數的識別**
構造參數的識別是通過類型匹配來完成的,如果沒有模糊不清的構造參數,那么構造參數的定義順序就是容器實例化Bean時提供參數給構造器的順序。參考如下代碼:
~~~
package x.y;
public class Foo {
public Foo(Bar bar, Baz baz) {
// ...
}
}
~~~
現在,參數都是明確的。假定`Bar`和`Baz`沒有繼承關系,因此下面的配置沒有任何問題,你也不需要在`<constructor-arg/>`元素中指定參數的索引和類型。
~~~
<beans>
<bean id="foo" class="x.y.Foo">
<constructor-arg ref="bar"/>
<constructor-arg ref="baz"/>
</bean>
<bean id="bar" class="x.y.Bar"/>
<bean id="baz" class="x.y.Baz"/>
</beans>
~~~
當引用的Bean的類型是確定的,這能很好的進行參數匹配。如果使用的是簡單類型,比如:`<value>true</value>`,Spring就不能判斷其類型了,所以不能獨自完成類型匹配。參考如下代碼:
~~~
package examples;
public class ExampleBean {
//計算最終答案的年份
private int years;
//生活、宇宙、萬物的答案
private String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
~~~
##### 1.構造參數類型匹配
在上面的例子中,如果你通過`type`屬性指定參數的類型,那么容器可以很好的進行簡單參數類型的類型匹配。參考如下代碼:
~~~
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
~~~
##### 2.構造參數索引
使用`index`屬性指定參數在構造器中的索引,參考如下代碼:
~~~
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>
~~~
> 如果不指定index的值,則默認以<constructor-arg/>出現的順序為參數索引。
為了解決多個簡單類型間的歧義性,可以通過指定值在構造參數中的索引。注意:索引從0開始。
##### *3.構造參數名*
你也可以指定值所對應構造器中的參數名來消除歧義:
~~~
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
~~~
注意:要使上述樣例正常工作,必須啟用調試標記來編譯代碼,這樣Spring才能從構造器中查找到參數名。否則你必須使用`@ConstructorProperties` JDK注解來顯式指定參數名,參考如下代碼:
~~~
package examples;
public class ExampleBean {
// 忽略屬性
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
~~~
*****
 
### **基于Setter方法注入**
容器通過無參構造方法或者無參靜態工廠方法實例化Bean之后,再通過調用Setter方法來完成依賴注入。
下面的樣例展示了一個類通過Setter方法來完成依賴注入。注意:這個類是普通的Java對象,不依賴于容器相關的接口、基類或者注解。
~~~
public class SimpleMovieLister {
// SimpleMovieLister依賴于MovieFinder
private MovieFinder movieFinder;
// Spring通過Setter方法注入MovieFinder實例
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
//使用被注入MovieFinder實例來完成的業務邏輯已被忽略
}
~~~
`ApplicationContext`支持構造注入和Setter方法注入,也支持通過構造參數注入某些依賴后再通過Setter注入其它依賴。配置的依賴存在于`BeanDefinition`中,可以結合`PropertyEditor`完成屬性的轉換。然而,大多開發者并不會直接使用這些類(即以編程的方式)而是使用XML、被注解的組件(即被`@Component`、 `@Controller`注解的類)以及帶有`@Configuration`的類中`@Bean`注解的方法來定義Bean,這些配置在內部被轉換為`BeanDefinition`實例并被用來加載整個Spring IOC容器。
<p style="margin-bottom:0px"> </p>
>### :-: **使用構造注入還是Setter方法注入?**
>
> 盡管可以使用混合注入,但是通過**構造方法完成必要依賴的注入**,通過**Setter方法完成可選依賴的注入**是一個好的編程方式。注意:在Setter方法上標注`@Required`([1.9.1.@Required](1.9.1.Required.md))注解使得屬性為必需依賴。不過,使用構造注入(帶有編程式參數驗證)更可取。
> **Spring團隊提倡使用構造注入**,原因如下:
> 1.該方式可以使得依賴為不可變對象并且不會為空;
> 2.通過構造注入的依賴的對象被客戶端調用時都是完全初始化好的;
> 3.它還可以作為啟示:過多的構造參數是一種不好的編程方式,意味著此類擁有過多的職責,最好重構代碼,將多余的職責分離開來。
>  
> Setter注入主要用于可選依賴,但是在類中也應該分配合理的默認值,否則在客戶端使用此依賴時需要做非空檢查。使用Setter注入有一個好處:可以重新配置或者重新注入依賴。通過JMX mbean進行管理是Setter注入的一個引人注目的用例。
> DI只在大多場景下是有效的,當處理沒有源碼的第三方類時,可以選擇其它注入方式。比如:第三方類沒有暴露任何Setter方法,那只能通過構造注入了。
*****
 
### **依賴處理過程**
容器處理依賴的的細節如下:
* ` ApplicationContext`通過配置元數據來創建和初始化,配置元數據可以通過XML、Java代碼、注解來指定。
* 每個Bean的依賴都表示為:屬性、構造參數、靜態工廠方法參數。當Bean創建后容器會注入其依賴。
* 每個屬性、構造參數都是需要設置的值或者需要引用容器中的其它Bean。
* 每個屬性、構造參數的值都被從指定的類型轉換成實際類型。默認情況下,Spring可以將字符串類型的值轉換為`int`,`long`,`String`,`boolean`等。
Spring容器被創建時會驗證每個Bean的配置,然而Bean的屬性在Bean沒有創建之前不會設置。Bean默認是單例的并且會在容器創建時初始化(關于Bean的作用域將在[1.5.Bean的范圍](1.5.Bean的范圍.md)詳細解釋),非單例的Bean將會在被客戶請求獲取時創建。Bean的創建可能會一系列Bean(Bean的依賴的依賴......的依賴)被創建。注意,這些依賴項之間的不匹配解析可能在較晚的時候才出現,例如在第一次創建受影響的Bean時。
<p style="margin-bottom:0px"> </p>
>### :-: **循環依賴**
>如果通過構造器完成依賴注入,這可能會出現循環依賴的情況。
>
>比如:類A通過構造注入類B的實例,而類B也通過構造注入類A的實例。容器在運行時會檢查出循環引用并拋出`BeanCurrentlyInCreationException`。
> 
>一個可行的解決方案:編輯源碼將一些類的構造注入替換成Setter注入。此外,還可以避免使用構造注入而只使用Setter注入。換而言之,可以通過Setter注入配置循環依賴,雖然這是不推薦的。
> 
>和典型的場景(沒有循環依賴)不同,Bean A和Bean B之間的循環依賴關系迫使在完全初始化之前將其中一個Bean注入到另一個Bean中,這是一個矛盾的問題(這就像一個經典的問題:先有雞還是先有蛋)。
>
開發者可以信任Spring做的事都是正確的。在容器加載時會檢查不存在依賴和循環依賴等問題。當一個Bean被創建時,Spring會盡可能晚的設置屬性和解析依賴。這意味著Spring容器被正確的加載后,當你請求一個對象時,如果創建該對象或者它的一項依賴出現問題,生成異常也會更晚。例如:當Bean的屬性缺失或者無效時會拋出異常。所以一些配置問題可能延遲可見,這也就是為什么`ApplicationContext`的實現默認提前實例化單例Bean的原因。在容器創建之時花費一些時間和內存創建Bean可以提前發現問題。你仍然可以覆蓋提前初始化的行為,將單例Bean的提前加載轉變為延遲加載。
 
如果沒有循環依賴存在,一個Bean在注入到依賴Bean之前就已經完成初始化好了。這意味著,如果Bean A依賴Bean B,在調用Bean A 的Setter方法之前,Bean B已是完全可用的。換而言之,當一個Bean初始化完成時,它的依賴已經設置好了,相關的生命周期方法(比如:初始化回調方法,詳情參見:[1.6.1.生命周期回調函數](1.6.1.生命周期回調函數.md))也調用完畢。
*****
 
### **依賴注入的例子**
下面的樣例展示了基于XML配置的Setter注入。
~~~
<bean id="exampleBean" class="examples.ExampleBean">
<!-- 使用ref元素完成Setter注入-->
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>
<!-- 使用更簡潔的ref屬性完成Setter注入 -->
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
~~~
~~~
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
public void setBeanOne(AnotherBean beanOne) {
this.beanOne = beanOne;
}
public void setBeanTwo(YetAnotherBean beanTwo) {
this.beanTwo = beanTwo;
}
public void setIntegerProperty(int i) {
this.i = i;
}
}
~~~
在上面的樣例中,Setter方法和和`<property/>`對應。下面的樣例采用構造注入。
~~~
<bean id="exampleBean" class="examples.ExampleBean">
<!-- 使用ref元素完成Setter注入-->
<constructor-arg>
<ref bean="anotherExampleBean"/>
</constructor-arg>
<!-- 使用更簡潔的ref屬性完成Setter注入 -->
<constructor-arg ref="yetAnotherBean"/>
<constructor-arg type="int" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
~~~
~~~
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
public ExampleBean(
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
this.beanOne = anotherBean;
this.beanTwo = yetAnotherBean;
this.i = i;
}
}
~~~
Bean定義指定的構造器參數會作為ExampleBean構造器中的參數。
現在請參考另一個例子,Spring通過靜態工廠來代替構造器創建實例。
~~~
<bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
<constructor-arg ref="anotherExampleBean"/>
<constructor-arg ref="yetAnotherBean"/>
<constructor-arg value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
~~~
~~~
public class ExampleBean {
// 私有構造器
private ExampleBean(...) {
...
}
/**
* 靜態工廠方法;
* 這個方法的參數可以被認為是待創建Bean的依賴項,
* 不管這些參數實際是如何使用的的。
*/
public static ExampleBean createInstance (AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
ExampleBean eb = new ExampleBean (...);
// 其它操作......
return eb;
}
}
~~~
`<constructor-arg/>`提供給靜態工廠方法的參數和構造器實際使用的參數是一致的。從工廠方法返回的對象類型可以和包含靜態工廠方法的類不一致,盡管在本例子中是一致的。實例(非靜態)工廠方法的用法和靜態工廠方法本質上是一樣(使用`factory-bean` 代替`class`屬性),故不再此詳述。
- ---- 閱讀說明
- 一、核心技術
- 1.IOC容器
- 1.1.IOC 容器和 Bean 簡介
- 1.2.容器概覽
- 1.2.1.配置元數據
- 1.2.2.實例化容器
- 1.2.3.使用容器
- 1.3.Bean概覽
- 1.3.1.Bean的命名
- 1.3.2.實例化Bean
- 1.4.依賴
- 1.4.1.依賴注入
- 1.4.2.依賴配置詳情
- 1.4.5.注入合作者
- 1.4.4.Bean的懶加載
- 1.5.Bean的范圍
- 1.6.自定義Bean的特性
- 1.6.1.生命周期回調函數
- 1.7.繼承Bean定義
- 1.9.基于注解的容器配置
- 1.9.1.@Required
- 1.12.基于Java的容器配置
- 1.12.3.使用@Bean注解
- 1.15.ApplicationContext的附加功能
- 1.15.4.Web 應用中便捷的 ApplicationContext 實例
- 1.16.BeanFactory
- 2.資源
- 2.7.應用上下文和資源路徑
- 3.驗證、數據綁定以及類型轉換
- 3.5.Spring類型轉換
- 3.5.4.ConversionService API
- 5.基于 Spring 的面向切面編程
- 5.8.在 Spring 中使用 AspectJ
- 5.8.1.在 Spring 中使用 AspectJ 注入領域對象
- 二、測試
- 三、數據訪問
- 四、Web應用
- 五、Web響應式編程
- 六、集成
- 4.1.介紹
- 七、編程語言