(二)《Spring實戰》——Spring核心
第二章:裝配Bean
在Spring中,對象無需自己查找或創建與其所關聯的其他對象。相反,容器負責把需要相互協作的對象引用賦予各個對象。例如,一個訂單管理組件需要信用卡認證組件,但它不需要自己創建信用卡認證組件。訂單管理組件只需要表明自己兩手空空,容器就會主動賦予它一個信用卡認證組件。
創建應用對象之間協作關系的行為通常稱為裝配(wiring),這也是依賴註入(DI)的本質。
1. Spring配置的可選方案
Spring容器負責創建應用程序中的bean並通過DI來協調這些對象之間的關系。但是,作為開發人員,你需要告訴Spring要創建哪些bean並且如何將其裝配在一起。當描述bean如何進行裝配時,Spring具有非常大的靈活性,它提供了三種主要的裝配機制
- 在XML中進行顯式配置。
- 在Java中進行顯式配置。
- 隱式的bean發現機制和自動裝配。
Spring有多種可選方案來配置bean,這是非常棒的,但有時候你必須要在其中做出選擇。
這方面,並沒有唯一的正確答案。你所做出的選擇必須要適合你和你的項目。而且,誰說我們只能選擇其中的一種方案呢?Spring的配置風格是可以互相搭配的,所以你可以選擇使用XML裝配一些bean,使用Spring基於Java的配置(JavaConfig)來裝配另一些bean,而將剩余的bean讓Spring去自動發現。
即便如此,我的建議是盡可能地使用自動配置的機制。顯式配置越少越好。當你必須要顯式配置bean的時候(比如,有些源碼不是由你來維護的,而當你需要為這些代碼配置bean的時候),我推薦使用類型安全並且比XML更加強大的JavaConfig。最後,只有當你想要使用便利的XML命名空間,並且在JavaConfig中沒有同樣的實現時,才應該使XML。
2. 自動化裝配bean
盡管顯式裝配技術(借助Java和XML來進行Spring裝配)非常有用,但是在便利性方面,最強大的還是Spring的自動化配置。
Spring從兩個角度來實現自動化裝配:
- 組件掃描(component scanning):Spring會自動發現應用上下文中所創建的bean。
- 自動裝配(autowiring):Spring自動滿足bean之間的依賴。
為了闡述組件掃描和裝配,我們需要創建幾個bean,它們代表了一個音響系統中的組件。首先,要創建CompactDisc 類,Spring會發現它並將其創建為一個bean。然後,會創建一個CDPlayer
2.1 創建可被發現的bean
CompactDisc接口在Java中定義了CD的概念
package soundsystem; public interface CompactDisc { void play(); }
CompactDisc 的具體內容並不重要,重要的是你將其定義為一個接口。作為接口,它定義了CD播放器對一盤CD所能進行的操作。它將CD播放器的任意實現與CD本身的耦合降低到了最小的程度。
帶有@Component註解的CompactDisc實現類SgtPeppers
package soundsystem; import org.springframework.stereotype.Component; @Component public class SgtPeppers implements CompactDisc { private String title = "Sgt. Pepper‘s Lonely Hearts Club Band"; private String artist = "The Beatles"; public void play() { System.out.println("Playing " + title + " by " + artist); } }
和CompactDisc 接口一樣,SgtPeppers 的具體內容並不重要。你需要註意的就是SgtPeppers 類上使用了@Component 註解。這個簡單的註解表明該類會作為組件類,並告知Spring要為這個類創建bean。沒有必要顯式配置SgtPeppers bean,因為這個類使用了@Component 註解,所以Spring會為你把事情處理妥當。
不過,組件掃描默認是不啟用的。我們還需要顯式配置一下Spring,從而命令它去尋找帶有@Component 註解的類,並為其創建bean。
@ComponentScan註解啟用了組件掃描
package soundsystem; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan public class CDPlayerConfig { }
如果沒有其他配置的話,@ComponentScan 默認會掃描與配置類相同的包。因為CDPlayerConfig 類位於soundsystem 包中,因此Spring將會掃描這個包以及這個包下的所有子包,查找帶有@Component 註解的類。這樣的話,就能發現CompactDisc ,並且會在Spring中自動為其創建一個bean。
如果你更傾向於使用XML來啟用組件掃描的話,那麽可以使用Spring context 命名空間的<context:component-scan> 元素。
通過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:context="http://www.springframework.org/schema/context" xmlns:c="http://www.springframework.org/schema/c" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="soundsystem" /> </beans>
測試組件掃描能夠發現CompactDisc
package soundsystem; import static org.junit.Assert.*; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes=CDPlayerConfig.class) public class CDPlayerTest { @Autowired private CompactDisc cd; @Test public void cdShouldNotBeNull() { assertNotNull(cd); } }
CDPlayerTest 使用了Spring的SpringJUnit4ClassRunner ,以便在測試開始的時候自動創建Spring的應用上下文。註解@ContextConfiguration 會告訴它需要在CDPlayerConfig 中加載配置。因為CDPlayerConfig 類中包含了@ComponentScan ,因此最終的應用上下文中應該包含CompactDisc bean。
測試代碼中有一個CompactDisc 類型的屬性,並且這個屬性帶有@Autowired 註解,以便於將CompactDisc bean註入到測試代碼之中。
2.2 為組件掃描的bean命名
Spring應用上下文中所有的bean都會給定一個ID。在前面的例子中,盡管我們沒有明確地為SgtPeppers bean設置ID,但Spring會根據類名為其指定一個ID。具體來講,這個bean所給定的ID為sgtPeppers ,也就是將類名的第一個字母變為小寫。
如果想為這個bean設置不同的ID,你所要做的就是將期望的ID作為值傳遞給@Component 註解。
還有一種方式是使用Java依賴註入規範(Java Dependency Injection)中所提供的@Named 註解來為bean設置ID。(不常用)
2.3 設置組件掃描的基礎包
為了指定不同的基礎包,你所需要做的就是在@ComponentScan 的value屬性中指明包的名稱:
要表明你所設置的是基礎包,那麽你可以通過basePackages屬性進行配置:
要設置多個基礎包的話,只需要將basePackages 屬性設置為要掃描包的一個數組即可:
除了將包設置為簡單的String類型(缺點類型不安全【not type-safe】)之外,@ComponentScan 還提供了另外一種方法,那就是將其指定為包中所包含的類或接口:
可以看到,basePackages 屬性被替換成了basePackageClasses 。同時,我們不是再使用String 類型的名稱來指定包,為basePackageClasses 屬性所設置的數組中包含了類。這些類所在的包將會作為組件掃描的基礎包。
2.4 通過為bean添加註解實現自動裝配
簡單來說,自動裝配就是讓Spring自動滿足bean依賴的一種方法,在滿足依賴的過程中,會在Spring應用上下文中尋找匹配某個bean需求的其他bean。為了聲明要進行自動裝配,我們可以借助Spring的@Autowired 註解。
如下,構造器上添加了@Autowired 註解,這表明當Spring創建CDPlayer bean的時候,會通過這個構造器來進行實例化並且會傳入一個可設置給CompactDisc 類型的bean。
package soundsystem; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class CDPlayer implements MediaPlayer { private CompactDisc cd; @Autowired public CDPlayer(CompactDisc cd) { this.cd = cd; } public void play() { cd.play(); } }
@Autowired 註解不僅能夠用在構造器上,還能用在屬性的Setter方法上。
@Autowired public setCompactDisc(CompactDisc cd) { this.cd = cd; }
@Autowired 註解可以用在類的任何方法上。
@Autowired public insertCompactDisc(CompactDisc cd) { this.cd = cd; }
不管是構造器、Setter方法還是其他的方法,Spring都會嘗試滿足方法參數上所聲明的依賴。假如有且只有一個bean匹配依賴需求的話,那麽這個bean將會被裝配進來。
如果沒有匹配的bean,那麽在應用上下文創建的時候,Spring會拋出一個異常。為了避免異常的出現,你可以將@Autowired 的required 屬性設置為false :
@Autowired(required=false) public CDPlayer(CompactDisc cd) { this.cd = cd; }
將required 屬性設置為false 時,Spring會嘗試執行自動裝配,但是如果沒有匹配的bean的話,Spring將會讓這個bean處於未裝配的狀態。但是,把required 屬性設置為false 時,你需要謹慎對待。如果在你的代碼中沒有進行null檢查的話,這個處於未裝配狀態的屬性有可能會出現NullPointerException 。
@Autowired 是Spring特有的註解。如果你不願意在代碼中到處使用Spring的特定註解來完成自動裝配任務的話,那麽你可以考慮將其替換為@Inject :
@Inject 註解來源於Java依賴註入規範,該規範同時還為我們定義了@Named 註解。在自動裝配中,Spring同時支持@Inject 和@Autowired 。盡管@Inject 和@Autowired 之間有著一些細微的差別,但是在大多數場景下,它們都是可以互相替換的。
2.5 驗證自動裝配
package soundsystem; import static org.junit.Assert.*; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.StandardOutputStreamLog; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes=CDPlayerConfig.class) public class CDPlayerTest { //測試代碼使用StandardOutputStreamLog 基於控制臺的輸出編寫斷言 @Rule public final StandardOutputStreamLog log = new StandardOutputStreamLog(); //將CDPlayer bean註入到測試代碼的player 成員變量之中(它是更為通用的MediaPlayer 類型) @Autowired private MediaPlayer player; @Autowired private CompactDisc cd; @Test public void cdShouldNotBeNull() { assertNotNull(cd); } @Test public void play() { player.play(); assertEquals( "Playing Sgt. Pepper‘s Lonely Hearts Club Band by The Beatles\n", log.getLog()); } }
3. 通過Java代碼裝配bean
在進行顯式配置時,JavaConfig是更好的方案,因為它更為強大、類型安全並且對重構友好。因為它就是Java代碼,就像應用程序中的其他Java代碼一樣。
同時,JavaConfig與其他的Java代碼又有所區別,在概念上,它與應用程序中的業務邏輯和領域代碼是不同的。盡管它與其他的組件一樣都使用相同的語言進行表述,但JavaConfig是配置代碼。這意味著它不應該包含任何業務邏輯,JavaConfig也不應該侵入到業務邏輯代碼之中。盡管不是必須的,但通常會將JavaConfig放到單獨的包中,使它與其他的應用程序邏輯分離開來,這樣對於它的意圖就不會產生困惑了。
3.1 創建配置類
package soundsystem;import org.springframework.context.annotation.Configuration; @Configuration public class CDPlayerConfig { }
創建JavaConfig類的關鍵在於為其添加@Configuration 註解,@Configuration 註解表明這個類是一個配置類,該類應該包含在Spring應用上下文中如何創建bean的細節。
3.2 聲明簡單的bean
要在JavaConfig中聲明bean ,我們需要編寫一個方法,這個方法會創建所需類型的實例,然後給這個方法添加@Bean 註解。
@Bean public CompactDisc compactDisc() { return new SgtPeppers(); }
默認情況下,bean的ID與帶有@Bean 註解的方法名是一樣的。當然也可通過name屬性自定義。
@Bean(name="lonelyHeartsClubBand") public CompactDisc compactDisc() { return new SgtPeppers(); }
3.3 借助JavaConfig實現註入
可以使用上面的代碼,通過調用方法來引用bean,但最常用的方法是下面這種:
@Bean public CDPlayer cdPlayer(CompactDisc compactDisc) { return new CDPlayer(compactDisc); }
在這裏,cdPlayer() 方法請求一個CompactDisc 作為參數。當Spring調用cdPlayer() 創建CDPlayer bean的時候,它會自動裝配一個CompactDisc 到配置方法之中。然後,方法體就可以按照合適的方式來使用它。借助這種技術,cdPlayer() 方法也能夠將CompactDisc 註入到CDPlayer 的構造器中,而且不用明確引用CompactDisc 的@Bean 方法。
4. 通過xml裝配bean
4.1 創建xml配置規範
創建新的配置規範。在使用JavaConfig的時候,這意味著要創建一個帶有@Configuration 註解的類,而在XML配置中,這意味著要創建一個XML文件,並且要以<beans> 元素為根。
<?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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> </beans>
4.2 聲明一個簡單的bean
<bean id="compactDisc" class="soundsystem.SgtPeppers" />
4.3 借助構造器註入初始化bean
構造器註入,有兩種基本的配置方案可供選擇:
- <constructor-arg> 元素
- 使用Spring 3.0所引入的c-命名空間
4.3.1 構造器註入bean引用
<constructor-arg >使用ref 來引入其他bean
c-命名空間
4.3.2 將字面量註入到構造器中
<constructor-arg>使用value 屬性表明給定的值要以字面量的形式註入到構造器之中。
c-命名空間
①引用構造器參數的名字
②通過參數索引裝配相同的字面量值
4.3.3 裝配集合
4.4 設置屬性
引用bean
<property >通過ref引用bean
p-命名空間
將字面量註入到屬性中
<property> 使用value 屬性設置屬性值
p-命名空間
不能使用p-命名空間來裝配集合,裝配集合可使用<util:list>
現在,我們能夠像使用其他的bean那樣,將磁道列表bean註入到BlankDisc bean的tracks 屬性中
5. 導入和混合配置
在自動裝配時,它並不在意要裝配的bean來自哪裏。自動裝配的時候會考慮到Spring容器中所有的bean,不管它是在JavaConfig或XML中聲明的還是通過組件掃描獲取到的。
5.1 在JavaConfig中引用XML配置
假設BlankDisc 定義在名為cd-config.xml 的文件中,該文件位於根類路徑下,那麽可以修改SoundSystemConfig ,讓它使用@ImportResource 註解,如下所示:
兩個bean——配置在JavaConfig中的CDPlayer 以及配置在XML中BlankDisc ——都會被加載到Spring容器之中。因為CDPlayer 中帶有@Bean 註解的方法接受一個CompactDisc 作為參數,因此BlankDisc 將會裝配進來,此時與它是通過XML配置的沒有任何關系。
5.2 在XML配置中引用JavaConfig
在XML中,我們可以使用<import >元素來拆分XML配置。
(二)《Spring實戰》——Spring核心