Spring 詳解(二):IOC 和DI
1 IOC 理解
Ioc—Inversion of Control,即“控制反轉”,不是什麼技術,而是一種設計思想。在Java開發中,Ioc意味著將你設計好的物件交給容器控制,而不是傳統的在你的物件內部直接控制。
IoC不是一種技術,只是一種思想,一個重要的面向物件程式設計的法則,它能指導我們如何設計出鬆耦合、更優良的程式。傳統應用程式都是由我們在類內部主動建立依賴物件,從而導致類與類之間高耦合,難於測試;有了IoC容器後,把建立和查詢依賴物件的控制權交給了容器,由容器進行注入組合物件,所以物件與物件之間是鬆散耦合,這樣也方便測試,利於功能複用,更重要的是使得程式的整個體系結構變得非常靈活。
- 誰控制誰,控制什麼:傳統Java SE程式設計,我們直接在物件內部通過new進行建立物件,是程式主動去建立依賴物件;而IoC是有專門一個容器來建立這些物件,即由Ioc容器來控制對 象的建立;誰控制誰?當然是IoC 容器控制了物件;控制什麼?那就是主要控制了外部資源獲取(不只是物件包括比如檔案等)。
- 為何是反轉,哪些方面反轉了:有反轉就有正轉,傳統應用程式是由我們自己在物件中主動控制去直接獲取依賴物件,也就是正轉;而反轉則是由容器來幫忙建立及注入依賴物件;為何是反轉?因為由容器幫我們查詢及注入依賴物件,物件只是被動的接受依賴物件,所以是反轉;哪些方面反轉了?依賴物件的獲取被反轉了。
用一句話來講解什麼是IOC:IOC中文名稱是控制反轉。主要目的是為了實現程式的解耦,由程式設計師主動例項化物件的過程轉交給Spring容器。IOC將本來是由程式設計師建立物件以及物件的管理和銷燬的控制權交給了Spring IOC容器中,Spring IOC將這一塊操作從應用程式中解耦出來,降低程式的耦合程度,簡化程式設計。
2. DI 理解
Dependency Injection,即“依賴注入”。DI是元件之間依賴關係由容器在執行期決定,形象的說,即由容器動態的將某個依賴關係注入到元件之中。當一個類(A)需要依賴另一個類物件(B)時,我們把另一個物件賦值給一個物件的過程。依賴注入的目的並非為軟體系統帶來更多功能,而是為了提升元件重用的頻率,併為系統搭建一個靈活、可擴充套件的平臺。通過依賴注入機制,我們只需要通過簡單的配置,而無需任何程式碼就可指定目標需要的資源,完成自身的業務邏輯,而不需要關心具體的資源來自何處,由誰實現。
理解DI的關鍵是:“誰依賴誰,為什麼需要依賴,誰注入誰,注入了什麼”,那我們來深入分析一下:
- 誰依賴於誰:當然是應用程式依賴於IoC容器;
- 為什麼需要依賴:應用程式需要IoC容器來提供物件需要的外部資源;
- 誰注入誰:很明顯是IoC容器注入應用程式某個物件,應用程式依賴的物件;
- 注入了什麼:就是注入某個物件所需要的外部資源(包括物件、資源、常量資料)。
用一句話來講解什麼是依賴注入:當我們一個類需要依賴於另一個物件,我們把另一個物件例項化後注入到這個物件的過程,我們就稱為DI。由於Spring IOC掌控物件的建立、管理以及銷燬工作,如果一個類需要依賴另外一個物件,Spring IOC容器需要將另外一個物件注入到應用程式某個物件中來,應用程式需要依賴某個外部物件。所有依賴注入提升元件重用的頻率,併為系統搭建一個靈活、可擴充套件的平臺。
3. Spring Bean裝配
在Spring中,物件無需自己查詢或者建立與其所關聯的其他物件。相反容器負責把需要相互協作的物件引用賦予各個物件。建立應用物件之間協作關係的行為通常被稱為裝配(wiring),這也是依賴注入(DI)的本質。
3.1 Spring Bean裝配的可選方案
當描述bean如何裝配時,Spring具有非常大的靈活性,它提供了三種主要的裝配機制:
- 在XML中進行顯示配置
- 在Java中進行顯示配置
- 隱式的Bean發現機制和自動裝配
我們需要儘可能使用在Java中進行顯示配置和Bean自動裝配機制,極少推薦使用XML中進行顯示配置,因為XML中配置需要大量的XML配置工作。
3.2 隱式的Bean發現機制和自動裝配
Spring從兩個角度來實現自動化裝配:
- 元件掃描(component scanning):Spring會發現應用上下文中所建立的Bean
- 元件裝配(autowiring):Spring自動滿足Bean之間的依賴
實現自動化裝配有兩種方式:(元件掃描預設是不啟動的)
1)通過XML啟用元件掃描
首先在mvc-config.xml中啟動元件掃描功能,並用base-package屬性指定掃描範圍
<context:component-scan base-package="com.hust.edu"/>
再通過在需要註解的class上新增@Controller、@Service、@Repository、@Component等註解,比如:
@Controller
public class UserController {// ......}
2)通過@ComponentScan註解啟用元件掃描
首先在class上使用@ComponentScan啟用元件掃描,例如:
@ComponentScan
public class AppConfig {// ......}
此外:@ComponentScan(basePackages=“conf”)等同於@ComponentScan(“conf”),然後通過在需要註解的class上新增@Controller、@Service、@Repository、@Component等註解,例如:
對於@ComponentScan可以通過basePackages或者basePackageClasses指定掃描範圍,等同於XML註解中的base-package屬性;如果不指定掃描範圍,則預設掃描當前類所在包以及子包的所有類。當然Craig Walls建議使用basePackageClasses,因為如果程式碼重構的話這種方式會立馬發現錯誤,下面是basePackageClasses的使用方式(指定基礎掃描類):
@ComponentScan(basePackageClasses={
UserController.class
// ......
})
public class AppConfig {}
使用@Autowired可以為bean實現自動裝配,@Autowired可以使用在建構函式、Setter方法、普通方法和成員變數上。比如下面的用法:
// 用法一:
@Autowired
MessageSend messageSend;
// 用法二:(建構函式也一樣,主要是函式引數的依賴)
@Autowired
public void setMessageSend(MessageSend messageSend) {
this.messageSend = messageSend;
}
設定@Autowired(required=false)時,Spring嘗試執行自動裝配,但是如果沒有匹配的bean則忽略,但是這種情況故意出現空指標異常NullPointerException。@Autowired註解可以使用@Inject替換,@component可以使用@Named註解替換,後者是源於Java依賴注入規範。
3.3. 通過Java程式碼裝配
大部分的場景自動化裝配Bean是滿足要求的,但是在一些特殊場景下自動化裝配Bean是無法滿足要求的,比如說要將第三方的元件裝配到自己的應用中,因為沒有方法將@component或者@Autowired註解放置在它們的類上。但是你仍然可以採用顯示裝配方式:Java程式碼裝配和XML配置。
首先需要建立配置類,建立配置類的關鍵是使用@Configuration註解來表明這個類是一個配置類,該類包涵Spring上下文中如何建立Bean的細節。
宣告一個簡單Bean:在Java的配置類中編寫一個帶有@Bean註解的方法,比如下面:
@Bean
public UserServiceImpl userService(){
return new UserServiceImpl();
}
預設情況下Bean的ID是方法名,也可以指定Bean的ID:@Bean(name=“userService”),如果有依賴可以使用下面這些的方式來實現:
@Bean
public UserServiceImpl userService(){
return new UserServiceImpl(userDao());
}
@Bean
public UserDaoImpl userDao(){
return new UserDaoImpl();
}
上面看起來是呼叫UserDaoImpl(),其實在配置類中Spring會攔截對這個方法的引用,並返回該方法所建立的bean,而不是每次都對其進行實際呼叫。當然下面這種方式也是可以的,userService()方法需要userDao作為引數,Spring建立Bean的時候會自動裝配一個UserDaoImpl到方法中(我猜測應該和@Autowired意思差不多,當Spring Context下只有一個UserDaoImpl就可以通過匹配原則進行裝配),這種方式是被推薦的,如果UserDaoImpl不是在本配置類下配置,任然可以正常使用(比如XML預設的元件掃描等)。
@Bean
public UserServiceImpl userService(UserDaoImpl userDao){
return new UserServiceImpl(userDao);
}
上面我們通過建構函式的方式實現依賴注入(DI),當然我們也可以用一種更好的方式來實現依賴注入,就是用Setter方法注入,如下所示:
@Bean
public UserServiceImpl userService(UserDaoImpl userDao){
UserServiceImpl userService = new UserServiceImpl();
userService.setDao(userDao);
return userService;
}
3.4 通過XML裝配Bean
XML裝配Bean的方式,雖然已經不推薦了,但是還是我們最早使用的一種Bean裝配的方式,還是需要學習的(很多古老的專案還在用,我目前的公司也是)。下面是一個簡單Bean的宣告:
<!--index表示獲取到物件的標識 clsss建立哪個物件-->
<bean id="peo" class="com.hust.edu.pojo.People">
<!--ref表示引用另一個bean vaule表示基本資料型別或者String-->
<constructor-arg index="1" type="int" value="1"></constructor-arg>
<constructor-arg index="0" type="java.lang.String" value="AAA"></constructor-arg>
</bean>
通過構造器建立Bean
- 通過無引數構造建立:預設情況
- 有引數構造建立:需要明確配置
在applicationContext.xml中設定呼叫哪個構造方法建立物件
- 如果設定的條件匹配多個構造方法執行最後的構造方法
- index:引數的索引,從0開始
- name:引數名字
- type:型別(區分關鍵字和非封裝類 int 和 Integer)
public class People {
private int id;
private String name;
public People(int id, String name) {
this.id = id;
this.name = name;
System.out.println("有引數構造方法1 name:"+name+" id:"+id);
}
public People(String name, int id) {
this.id = id;
this.name = name;
System.out.println("有引數構造方法2 name:"+name+" id:"+id);
}
public People() {
System.out.println("執行預設的構造方法");
}
public int getId() {
return id;
}
public void setId(int id) {
System.out.println("setter id: "+ id);
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
System.out.println("setter name: "+ name);
this.name = name;
}
@Override
public String toString() {
return "People{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
<!--index表示獲取到物件的標識 clsss建立哪個物件-->
<bean id="peo" class="com.hust.edu.pojo.People">
<!--ref表示引用另一個bean vaule表示基本資料型別或者String-->
<constructor-arg index="1" type="int" value="1"></constructor-arg>
<constructor-arg index="0" type="java.lang.String" value="AAA"></constructor-arg>
</bean>
通過例項工廠方法建立Bean
工廠設計模式:幫助建立物件,一個工廠可以引數多個物件。
例項工廠:需要建立工廠,才能建立物件。
實現步驟:
- 必須要有一個例項工廠
- 在applicationContext.xml 中配置工廠物件和需要建立的物件
public class PeopleFactory {
public People createPeople(String type){
switch (type){
case "A":
return new PeopleA();
case "B":
return new PeopleB();
default:
return null;
}
}
}
public class PeopleA extends People{
@Override
public String toString() {
System.out.println("A");
return super.toString();
}
}
public class PeopleB extends People{
@Override
public String toString() {
System.out.println("B");
return super.toString();
}
}
<bean id="factory" class="com.hust.edu.constructor.factory.PeopleFactory"></bean>
<bean id="peo1" factory-bean="factory" factory-method="createPeople" >
<constructor-arg name="type" value="A"/>
</bean>
<bean id="peo2" factory-bean="factory" factory-method="createPeople" >
<constructor-arg name="type" value="B"/>
</bean>
通過靜態工廠建立Bean
靜態工廠:不需要建立工廠, 可以快速建立物件
實現步驟:
- 必須需要有一個靜態工廠,在方法上新增static
- 在applicationContext.xml 中建立物件
public class PeopleStaticFactory {
public static People getPeopleAInstance(){
return new PeopleA();
}
public static People getPeopleBInstance(){
return new PeopleB();
}
}
<bean id="peo3" class="com.hust.edu.constructor.factory.PeopleStaticFactory" factory-method="getPeopleAInstance"></bean>
<bean id="peo4" class="com.hust.edu.constructor.factory.PeopleStaticFactory" factory-method="getPeopleBInstance"></bean>
設定屬性
- 如果屬性是基本資料型別或者String型別,注入就比較簡單
- 如果屬性是Set、List、Map、Array, 注入不同的資料型別
<bean id="peo" class="com.hust.edu.pojo.People">
<property name="id" value="1"></property>
<property name="name" value="zhangsan"></property>
</bean>
<bean id="peoSet" class="com.hust.edu.pojo.PeopleSet">
<property name="id" value="1"></property>
<property name="name" value="zhangsan"></property>
<property name="set" >
<set>
<value>1</value>
<value>2</value>
<value>3</value>
<value>4</value>
</set>
</property>
</bean>
<bean id="peoList" class="com.hust.edu.pojo.PeopleList">
<property name="id" value="1"></property>
<property name="name" value="zhangsan"></property>
<property name="list" >
<set>
<value>1</value>
<value>2</value>
<value>3</value>
<value>4</value>
</set>
</property>
</bean>
<bean id="peoArray" class="com.hust.edu.pojo.PeopleArray">
<property name="id" value="1"></property>
<property name="name" value="zhangsan"></property>
<property name="strings" >
<array>
<value>1</value>
<value>2</value>
<value>3</value>
<value>4</value>
</array>
</property>
</bean>
<bean id="peoMap" class="com.hust.edu.pojo.PeopleMap">
<property name="id" value="1"></property>
<property name="name" value="zhangsan"></property>
<property name="map" >
<map>
<entry key="a" value="123"></entry>
<entry key="b" value="456"></entry>
<entry key="c" value="789"></entry>
</map>
</property>
</bean>
4 處理自動裝配的歧義性
雖然在實際編寫程式碼中,很少有情況會遇到Bean裝配的歧義性,更多的情況是給定的類只有一個實現,這樣自動裝配就會很好的實現。但是當發生歧義性的時候,Spring提供了多種的可選解決方案。比如People父類,有三個實現類分別是PeopleA、PeopleB和PeopleC。那麼在自動裝配的時候具體是裝配哪個類呢?所以Spring必須提供表選自動裝配的Bean。
public class PeopleAutowired {
@Autowired
private People people;
@Override
public String toString() {
return "PeopleAutowired{" +
"people=" + people +
'}';
}
}
@Component
public class PeopleA extends People {
@Override
public String toString() {
System.out.println("A");
return super.toString();
}
}
@Component
public class PeopleB extends People {
@Override
public String toString() {
System.out.println("B");
return super.toString();
}
}
@Component
public class PeopleC extends People {
@Override
public String toString() {
System.out.println("C");
return super.toString();
}
}
4.1 表示首選的Bean
注意,對於多可選擇項,只能有一個可以加上@Primary。通過@Primary
註解來標識首選Bean。
@Component
@Primary
public class PeopleA extends People {
@Override
public String toString() {
System.out.println("A");
return super.toString();
}
}
4.2 限定自動裝配的Bean
@Qualifier(“peopleA”)指向的是掃描元件時建立的Bean,並且這個Bean是IceCream類的例項。事實上如果所有的Bean都沒有自己指定一個限定符(Qualifier),則會有一個預設的限定符(與Bean ID相同),我們可以在Bean的類上新增@Qualifier註解來自定義限定符,如下所示:
@Component
@Primary
@Qualifier("peopleA")
public class PeopleA extends People {
@Override
public String toString() {
System.out.println("A");
return super.toString();
}
}
@Component
public class PeopleAutowired {
@Autowired
@Qualifier("peopleB")
private People people;
@Override
public String toString() {
return "PeopleAutowired{" +
"people=" + people +
'}';
}
}
5. Bean作用域
預設情況下,Spring應用上下文中所有的Bean都是單例模式。在大多數情況下單例模式都是非常理想的方案。但是如果,你要注入或者裝配的Bean是易變的,他們會有一些特有的狀態。這種情況下單例模式就會容易被汙染。Spring為此定義了很多作用域,可以基於這些作用域建立Bean,包括:
- 單例(Singleton):在整個應用中,只建立Bean的一個例項
- 原型(Prototype):每次注入或者通過Spring應用上下文獲取的時候,都會建立一個新的Bean例項。這個相當於new的操作
- 會話(Session):在Web應用中,為每個會話建立一個Bean例項。對於同一個介面的請求,如果使用不同的瀏覽器,將會得到不同的例項(Session不同)
- 請求(Request):在Web應用中,為每個請求建立一個Bean例項
@Component
@Scope("prototype")
public class Car {
// 。。。。。。
}
// ——————或者——————
@Bean
@Scope("prototype")
public LinuxConfig getLinux() {
LinuxConfig config = new LinuxConfig();
return config;
}
5.1 使用會話和請求作用域
我們常用@Scope來定義Bean的作用域。如使用者的購物車資訊,如果將購物車類宣告為單例(Singleton),那麼每個使用者都向同一個購物車中新增商品,這樣勢必會造成混亂;你也許會想到使用原型模式宣告購物車,但這樣同一使用者在不同請求時,所獲得的購物車資訊是不同的,這也是一種混亂。如下所示:
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,proxyMode = ScopedProxyMode.INTERFACES)
public class ShoppingCart {
}
在這裡我們要注意一下,屬性proxyMode。這個屬性解決了將會話或者請求作用域的Bean注入到單例Bean中所遇到的問題。假設我們要將Cart bean注入到單例StoreService bean的Setter方法中:
StoreService是一個單例bean,會在Spring應用上下文載入的時候建立。 當它建立的時候, Spring會試圖將Cart bean注入到setCart()方法中。 但是Cart bean是會話作用域的, 此時並不存在。 直到某個使用者進入系統,建立了會話之後,才會出現Cart例項。系統中將會有多個Cart例項: 每個使用者一個。 我們並不想讓Spring注入某個固定的Cart例項到StoreService中。 我們希望的是當StoreService處理購物車功能時, 它所使用的Cart例項恰好是當前會話所對應的那一個。Spring並不會將實際的Cart bean注入到StoreService中,Spring會注入一個到Cart bean的代理。這