本章內(nèi)容:
- 聲明Bean
- 構(gòu)造器注入和Setter方法注入
- 裝配Bean
- 控制bean的創(chuàng)建和銷毀
任何一個成功的應(yīng)用都是由多個為了實現(xiàn)某一業(yè)務(wù)目標(biāo)而相互協(xié)作的組件構(gòu)成的。這些組件必須彼此了解,并且相互協(xié)作來完成工作。
但是,正如在第一章中所看到的,創(chuàng)建應(yīng)用對象之間關(guān)聯(lián)關(guān)系的傳統(tǒng)方法通常會導(dǎo)致結(jié)構(gòu)嚴(yán)重復(fù)雜的代碼,這些代碼很難被復(fù)用也很難進(jìn)行單元測試。
在Spring中,對象無需查找或創(chuàng)建與其關(guān)聯(lián)的其他對象。相反的,容器負(fù)責(zé)把需要相互協(xié)作的對象引用賦予各個對象。
創(chuàng)建應(yīng)用對象之間協(xié)作關(guān)系的行為通常稱為裝配,這也是依賴注入的本質(zhì)。
Spring配置的可選方案
Spring容器負(fù)責(zé)創(chuàng)建應(yīng)用程序中的bean并通過DI來協(xié)調(diào)這些對象之間的關(guān)系。但是開發(fā)人員需要告訴Spring要創(chuàng)建哪些bean并且如何將其裝配在一起。Spring中裝配bean最常見的有三種機(jī)制:
- 在XML中進(jìn)行顯示配置
- 在Java中進(jìn)行顯示配置
- 隱式的bean發(fā)現(xiàn)機(jī)制和自動裝配
最好盡可能地使用自動配置的機(jī)制。顯示配置越少越好。當(dāng)必須要顯示配置bean的時候,推薦使用類型安全并且比XML更加強(qiáng)大的JavaConfig。最后,只有當(dāng)想要使用便利的XML命名空間,并且在JavaConfig中沒有同樣的實現(xiàn)時,才應(yīng)該使用XML。
(自動配置 > 顯示Java配置 > 顯示XML配置)
自動化裝配bean
雖然顯示裝配技術(shù)非常有用,但是在便利性方面最強(qiáng)大的還是Spring的自動化配置。
Spring從兩個角度來實現(xiàn)自動化裝配:
- 組件掃描: Spring會自動發(fā)現(xiàn)應(yīng)用上下文中所創(chuàng)建的bean。
- 自動裝配:Spring自動滿足bean之間的依賴。
組件掃描和自動裝配組合在一起能夠?qū)@示配置降低到最少。
現(xiàn)在創(chuàng)建幾個bean,它們代表音響系統(tǒng)中的組件。
接下來為了闡述組件掃描和裝配,創(chuàng)建CompactDisc類,Spring會發(fā)現(xiàn)它并將其創(chuàng)建為一個bean。然后會創(chuàng)建一個CDPlayer類,讓Spring發(fā)現(xiàn)它,并將CompactDisc bean注入進(jìn)來。
創(chuàng)建可被發(fā)現(xiàn)的bean
首先在Java中建立CD的概念,定義CD的一個接口:
package soundsystem;
public interface CompactDisc {
void play();
}
作為接口,它定義了CD播放器對一盤CD所能進(jìn)行的操作。
現(xiàn)在還需要一個CompactDisc的實現(xiàn):
//帶有@component注解的CompactDisc實現(xiàn)類
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);
}
}
SgtPeppers類上使用了@Component注解。這個簡單的注解表明該類會作為組件類,并告知Spring要為這個類創(chuàng)建bean。沒有必要顯示配置SgtPeppers bean,因為這個類使用了@component注解。
組件掃描默認(rèn)是不啟用的,因此還需要顯示配置一下Spring,從而命令它去尋找?guī)в蠤Component注解的類,并為其創(chuàng)建bean:
package soundsystem;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class CDPlayerConfig {
}
類CDPlayerConfig通過Java代碼定義了Spring的裝配規(guī)則。CDPlayerConfig類并沒有顯示地聲明任何bean,但它使用了@ComponentScan注解,這個注解啟用了Spring的組件掃描。
如果沒有其他配置,@ComponentScan默認(rèn)會掃描與配置類相同的包。由于CDPlayerConfig類位于soundsystem包中,因此Spring會掃描這個包以及這個包下的所有子包,查找?guī)в蠤Component注解的類。這樣的話,就能發(fā)現(xiàn)CompactDisc并且會在Spring中自動創(chuàng)建一個bean。
也可以使用XML來啟用組件掃描,通過使用Spring Context命名空間的<context:component-scan>元素:
<?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>
為了測試組件掃描的功能,創(chuàng)建一個簡單的JUnit測試,它會創(chuàng)建Spring上下文,并判斷CompactDisc是不是真的創(chuàng)建出來了。
package soundsystem;
import static org.junit.Assert.*;
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創(chuàng)建的SpringJUnit4ClassRunner,在測試開始的時候自動創(chuàng)建Spring的應(yīng)用上下文。注解@ContextConfiguration會告訴它需要在CDPlayerConfig中加載配置。因為CDPlayerConfig類中包含了@ComponentScan,因此最終的應(yīng)用上下文中應(yīng)該包含CompactDisc bean。
測試代碼中有一個CompactDisc類型的屬性。并且這個屬性帶有@Autowired注解,以便于將CompactDisc bean注入到測試代碼之中。最后用一個簡單的測試方法斷言cd屬性不為null。如果它不為null,意味著Spring能夠發(fā)現(xiàn)CompactDisc類,自動在Spring上下文中將其創(chuàng)建為bean并將其注入到測試代碼之中。
為組件掃描的bean命名
Spring應(yīng)用上下文中所有的bean都會給定一個ID。前面的例子沒有明確地為SgtPeppers bean設(shè)置ID,但Spring會根據(jù)類名為其指定一個ID。前面的bean被默認(rèn)給定的ID為sgtPeppers(類名的第一個字母變?yōu)樾懀?/p>
如果想為這個bean設(shè)置不同的ID,則需要把期望的ID作為值傳遞給@Component注解:
package soundsystem;
import org.springframework.stereotype.Component;
@Component("lonelyHeartsClub")
public class SgtPeppers implements CompactDisc {
...
}
還有另外一種為bean命名的方式,這種方式不使用@Component注解,使用Java依賴注入規(guī)范中提供的@Name注解來為bean設(shè)置ID:
package soundsystem;
import javax.inject.Named;
@Named("lonelyHeartsClub")
public class SgtPeppers implements CompactDisc {
...
}
Spring支持將@Named作為@Component注解的替代方案。在大多數(shù)場景中它們可以互相替換。
設(shè)置組件掃描的基礎(chǔ)包
目前為止,沒有給@ComponentScan設(shè)置任何屬性。意味著它會按照默認(rèn)規(guī)則,以配置類所在的包作為基礎(chǔ)包來掃描組件。如果配置類被放在單獨的包里與應(yīng)用代碼區(qū)分開來,想掃描不同的包,或者掃描多個包。為了指定不同的基礎(chǔ)包,需要在@ComponentScan的value屬性中指定包的名稱:
@Configuration
@ComponentScan("soundsystem")
public class CDPlayerConfig {}
或
@Configuration
@ComponentScan(basePackages = "soundsystem")
public class CDPlayerConfig {}
注意basePackages屬性是復(fù)數(shù)形式。意味著可以設(shè)置多個基礎(chǔ)包,只需要將basePackages屬性設(shè)置為要掃描包的一個數(shù)組即可:
@Configuration
@ComponentScan(basePackages = {"soundsystem","video"})
public class CDPlayerConfig {}
上面的例子里,基礎(chǔ)包是以String類型表示的。但這種方法是類型不安全的。如果要重構(gòu)代碼,所指定的基礎(chǔ)包就可能會出現(xiàn)錯誤。
除了將包設(shè)置為簡單的String類型之外,@ComponentScan還提供了另外一種辦法 ,將其指定為包中所包含的類或接口:
@Configuration
@ComponentScan(basePackageClasses = {CDPlayer.class,DVDPlayer.class})
public class CDPlayerConfig {}
basePacakges
屬性被替換成了basePackageClasses
。同時不再使用String類型的名稱來指定包,為basePackageClasses指定的數(shù)組中包含了類。這些類所在的包將會作為組件掃描的基礎(chǔ)包。
在樣例中,為basePackageClasses設(shè)置的是組件類,但也可以在包中創(chuàng)建一個用來掃描的空標(biāo)記接口。通過標(biāo)記接口的方式,能夠保持對重構(gòu)友好的接口引用,避免引用任何實際的應(yīng)用程序代碼。
如果應(yīng)用程序中所有對象都是獨立的,彼此之之間沒有任何依賴,那么只需要組件掃描即可。但很多對象會依賴其他的對象才能完成任務(wù)。這樣就需要一種方法將組件掃描得到的bean和它們的依賴裝配到一起。這就需要Spring自動化配置的另一方面內(nèi)容,自動裝配。
通過為bean添加注解實現(xiàn)自動裝配
自動裝配是讓Spring自動滿足bean依賴的一種方法。在滿足依賴的過程中,會在Spring應(yīng)用上下文中尋找匹配某個bean需求的其他bean。借助@Autowired注解聲明進(jìn)行自動裝配。
比如下面的CDPlayer類。它的構(gòu)造器添加了Autowired注解,表明當(dāng)Spring創(chuàng)建CDPlayer bean的時候,會通過這個構(gòu)造器來進(jìn)行實例化并且會傳入一個可設(shè)置給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
注解不僅能用在構(gòu)造器上,還能用在屬性的Setter方法上。比如CDPlayer有一個SetCompactDisc()方法,可以采用如下的注解形式進(jìn)行自動裝配:
@Autowired
public void setCompaticDisc(CompaticDisc cd){
this.cd = cd;
}
在Spring初始化bean之后,它會盡可能滿足bean的依賴,本例的依賴是通過@Autowired注解的方法進(jìn)行聲明的,也就是setCompacticDisc()。
實際上,@Autowired注解可以用在類的任何方法上,不管是構(gòu)造器、Setter方法還是其他方法,Spring都會嘗試滿足方法參數(shù)上所聲明的依賴。假如只有一個bean匹配依賴需求,則這個bean將會被裝配進(jìn)來;如果沒有匹配的bean,則在應(yīng)用上下文創(chuàng)建的時候,Spring會拋出一個異常。如果要避免異常的出現(xiàn),可以將@Autowired的required屬性設(shè)置為false。
@Autowired(required = false)
public void setCompaticDisc(CompaticDisc cd){
this.cd = cd;
}
將required屬性設(shè)置為false時,Spring會嘗試執(zhí)行自動裝配,但是如果沒有匹配的bean的話,Spring會讓這個bean處于未裝配的狀態(tài)。但這樣如果在代碼中沒有進(jìn)行null檢查的話。未裝配狀態(tài)的屬性可能會引發(fā)NullPointerException。
如果有多個bean都滿足依賴關(guān)系的話,Spring會拋出一個異常,表明沒有明確指定要選擇哪個bean進(jìn)行自動裝配。自動裝配的歧義性會在下一章討論。
@Autowired是Spring特有的注解。如果不愿意在代碼中使用Spring的特定注解來完成自動裝配任務(wù),可以考慮將其替換為@Inject
:
package soundsystem;
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class CDPlayer {
...
Inject
public CDPlayer(CompactDisc cd){
this.cd = cd;
}
...
}
@Inject
注解來源于Java依賴注入規(guī)范,該規(guī)范同時還為我們定義了@Named
注解。在大多數(shù)場景下,@Inject
和@Autowired
是可以互相替換的。
驗證自動裝配
現(xiàn)在已經(jīng)在CDPlayer的構(gòu)造器中添加了@Autowired
注解,Spring將把一個可分配給CompactDisc類型的bean自動注入進(jìn)來。修改CDPlayerTest,使其能借助CDPlayer bean播放CD:
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 {
@Rule
public final StandardOutputStreamLog log = new StandardOutputStreamLog();
@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());
}
}
現(xiàn)在除了注入CompactDisc外,還將CDPlayer bean注入到測試代碼的player成員變量中。在play()測試方法中,調(diào)用CDPlayer的play()方法,并斷言它的行為與預(yù)期一致。
在測試代碼中使用System.out.println()是有點棘手的事情。因此,在樣例中使用了StandardOutputStreamLog,這來源于System Rules庫的一個JUnit規(guī)則,該規(guī)則能夠基于控制臺的輸出編寫斷言。在這里斷言SgtPeppers.play()方法的輸出被發(fā)送到了控制臺上。
通過Java代碼裝配bean
盡管很多場景下通過組件掃描和自動裝配實現(xiàn)Spring的自動化配置是更為推薦的方式,但有時候自動化配置的方案行不通,因此需要明確配合Spring。比如,要將第三方庫中的組件裝配到你的應(yīng)用中,在這種情況下無法在它的類上添加@Component和@Autowired注解。因此無法使用自動化裝配方案。
在這種情況下必須要采用顯式裝配的方式。像之前說的,顯示配置時,JavaConfig是更好的方案,因為它更為強(qiáng)大、類型安全并且對重構(gòu)友好。
JavaConfig與其他的Java代碼有區(qū)別,在概念上,它與應(yīng)用程序中的業(yè)務(wù)邏輯和領(lǐng)域代碼是不同的。盡管它與其他的組件一樣都使用相同的語言進(jìn)行表述,但JavaConfig是配置代碼。這意味著它不應(yīng)包含任何業(yè)務(wù)邏輯。JavaConfig也不應(yīng)該侵入到業(yè)務(wù)邏輯代碼之中。通常會將JavaConfig放到單獨的包里與其他應(yīng)用程序邏輯分離開來。
創(chuàng)建配置類
創(chuàng)建JavaConfig類的關(guān)鍵在于為其添加@Configuration注解,@Configuration注解表明這個類是一個配置類,該類應(yīng)該包含在Spring應(yīng)用上下文如何創(chuàng)建bean的細(xì)節(jié)。為了更加關(guān)注于顯示配置,將CDPlayer的@ComponentScan注解移除掉。
package soundsystem;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CDPlayerConfig {
}
聲明簡單的bean
要在JavaConfig中聲明bean, 需要編寫一個方法,這個方法會創(chuàng)建所需類型的實例,然后給這個方法添加@Bean
注解。比如下面的代碼聲明了CompaticDisc bean:
@Bean
public CompaticDisc sgtPeppers() {
return new SgtPeppers();
}
@Bean注解會告訴Sring這個方法將返回一個對象,該對象要注冊為Spring應(yīng)用上下文中的bean。方法體中包含了最終產(chǎn)生bean實例的邏輯。
默認(rèn)情況下,bean的ID與帶有@Bean注解的方法名是一樣的。
如果想要為其設(shè)置成一個不同的名字,則可以重命名該方法,也可以通過name屬性指定一個不同的名字:
@Bean(name="lonelyHeartsClubBand")
public CompaticDisc sgtPeppers() {
return new SgtPeppers();
}
借助JavaConfig實現(xiàn)注入
CDPlayer bean依賴于CompaticDisc,在JavaConfig中,要如何將它們裝配到一起?
JavaConfig中裝配bean最簡單的方式就是引用創(chuàng)建bean的方法(引用調(diào)用方法)。例如:
@Bean
public CDPlayer cdPlayer() {
return new CDPlayer(sgtPeppers);
}
cdPlayer()方法像sgtPeppers()方法一樣,同樣使用了@Bean
注解,這表明這個方法會創(chuàng)建一個bean實例并將其注冊到Spring應(yīng)用上下文中。創(chuàng)建的bean的ID為cdPlayer,與方法的名字相同。
cdPlayer()的方法體與sgtPeppers()有些區(qū)別,它并沒有使用默認(rèn)的構(gòu)造器創(chuàng)建實例,而是調(diào)用了需要傳入CompactDisc對象的構(gòu)造器來創(chuàng)建CDPlayer實例。
CompactDisc并非是調(diào)用sgtPeppers()得到的,由于sgtPeppers()方法上添加了@Bean注解,Spring將攔截所有對它的調(diào)用,并確保返回該方法所創(chuàng)建的bean,而不是每次都對其進(jìn)行實際的調(diào)用。
在默認(rèn)情況下,Spring的bean都是單例的,不需要為第二個CDPlayer bean創(chuàng)建完全相同的SgtPeppers實例。因此,**Spring會攔截對sgtPeppers()的調(diào)用并確保返回的是Spring所創(chuàng)建的bean。因此兩個CDPlayer bean會得到相同的SgtPeppers實例。
可以看到通過調(diào)用方法來引用bean的方式有點令人困惑。還有一種理解起來更為簡單的方式:
@Bean
Public CDPlayer cdPlayer(CompaticDisc comPaticDisc){
return new CDPlayer(compactDisc);
}
cdPlayer()方法請求一個CompaticDisc作為參數(shù)。當(dāng)Spring調(diào)用cdPlayer()創(chuàng)建CDPlayer bean的時候,會自動裝配一個Compact到配置方法中。然后,方法體按照合適的方式來使用它。借助這種技術(shù),cdPlayer()方法同樣也能將ComPaticDisc注入到CDPlayer的構(gòu)造器中,不用明確引用CompactDisc的@Bean方法。
使用這種方法引用其他的bean通常是最佳的選擇,因為它不要求將CompactDisc聲明到同一個配置類中。甚至不要求CompactDisc必須在JavaConfig中聲明。
前面使用CDPlayer的構(gòu)造器實現(xiàn)DI功能,同樣也可以采用其他風(fēng)格的DI配置。例如通過Setter方法注入CompactDisc:
@Bean
Public CDPlayer cdPlayer(CompaticDisc comPaticDisc){
CDPlayer cdPlayer = new CDPlayer(compactDisc);
cdPlayer.setCompactDisc(compactDisc)
return cdPlayer;
}
帶有@Bean注解的方法可以采用任何必要的Java功能來產(chǎn)生bean實例。構(gòu)造器和Setter方法只是@Bean方法的兩個簡單樣例。
通過XML裝配bean
XML是最初Spring剛出現(xiàn)的時候描述配置的主要方式,這種方式已經(jīng)不是最優(yōu)的或是唯一可選方案,本節(jié)的內(nèi)容主要是用來幫助維護(hù)已有的XML配置。
創(chuàng)建XML配置規(guī)范
在使用XML配置Spring裝配bean之前,需要創(chuàng)建一個新的配置規(guī)范(就像在JavaConfig的時候,要創(chuàng)建一個帶有@Configuration注解的類)。在XML配置中,要創(chuàng)建一個XML文件,并且要以<beans>元素為根。
最簡單的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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context>
......
</beans>
需要在配置文件的頂部聲明多個XML模式(XSD)文件,這些文件定義了配置Spring的XML元素。
可借助Spring Tool Suite創(chuàng)建XML配置文件來簡化創(chuàng)建和管理Spring XML配置文件的步驟。
裝配bean的最基本的XML元素包含在spring-beans模式之中,在上面這個XML文件中,它被定義為根命名空間。<beans>是該模式中的一個元素,是所有Spring配置文件的根元素。
聲明一個簡單的<beans>
在基于XML的Spring配置中聲明一個bean,要使用spring-beans模式中的另外一個元素<bean>。可以按照如下的方式生命CompactDisc bean:
<bean class="soundsystem.SgtPeppers" />
聲明了一個很簡單的bean,創(chuàng)建這個bean的類通過class屬性來指定。
由于沒有明確給定ID,所以將會根據(jù)全限定類名來進(jìn)行命名。上面的bean的ID將會是"soundsystem.Sgtpeppers#0"。“#0”是一個計數(shù)的形式,用來區(qū)分相同類型的其他bean。這樣的ID命名方式對稍后的引用會很不友好。因此通常要借助id屬性為每個bean設(shè)置一個自己選擇的名字:
<bean id="cdPlayer" class="soundsystem.CDPlayer" />
減少繁瑣
為了減少XML中配置的繁瑣,只對需要按名字引用的bean進(jìn)行明確地命名。
簡單bean聲明的特征
- 不再需要直接負(fù)責(zé)創(chuàng)建SgtPeppers的實例
在基于JavaConfig的配置中,需要直接負(fù)責(zé)創(chuàng)建SgtPeppers的實例。當(dāng)Spring發(fā)現(xiàn)這個<bean>元素時,會調(diào)用SgtPeppers的默認(rèn)構(gòu)造器來創(chuàng)建bean。這樣bean的創(chuàng)建顯得更加的被動。
- bean的類型以字符串的形式設(shè)置在了class屬性中,如果重命名那個類,XML配置文件也要修改。
借助構(gòu)造器注入初始化bean
Spring XML配置中只有一種聲明bean的方式。但是在XML中聲明DI時,有多重可選的配置方案和風(fēng)格。具體到構(gòu)造器注入,有兩種基本的配置方案可供選擇:
- <constructor-arg>元素
- 使用Spring 3.0引入的 c-命名空間
二者的區(qū)別在于是否冗長繁瑣。<constructor-arg>元素比使用 c-命名空間會更加冗長,從而導(dǎo)致XML難以讀懂。有些事情<constructor-arg>可以做到,但 c-命名空間卻無法實現(xiàn)。
構(gòu)造器注入bean引用
現(xiàn)在已經(jīng)聲明了SgtPeppers bean,且SgtPepepr類實現(xiàn)了CompactDisc接口,所以已經(jīng)有了一個可以注入到CDPlayer bean中的bean。在XML中聲明CDPlayer并通過ID引用SgtPeppers即可:
<bean id="cdPlayer" class="soundsystem.CDPlayer">
<constructor-arg ref="compactDisc" />
</bean>
當(dāng)Spring遇到這個<bean>元素時,會創(chuàng)建一個CDPlayer實例。<constructor-arg>元素會告知Spring要將ID為compactDisc的bean引用傳遞到CDPlayer的構(gòu)造器中。
作為替代方案,也可以使用Spring的 c-命名空間。它是在XML中更為簡潔地描述構(gòu)造器參數(shù)的方式。要使用它必須要在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:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
...
</beans>
聲明完 c-命名空間和模式之后,就可以使用它來聲明構(gòu)造器參數(shù)了:
<bean id="cdPlayer" class="soundsystem.CDPlayer"
c:cd-ref="compactDisc" />
c-命名空間作為<bean>元素的一個屬性。下圖描述了這個屬性名是如何組成的。
屬性名以“c:”開頭,也就是命名空間的前綴。接下來是要裝配的構(gòu)造器參數(shù)名,在此之后是“-ref”,這是一個命名約定,它會告訴Spring,正在裝配的是一個bean的引用,這個bean的名字是compactDisc,而不是字面量“compactDisc”。
c-命名空間直接使用了構(gòu)造器參數(shù)的名稱。引用參數(shù)的名字這需要在編譯代碼的時候,將調(diào)試標(biāo)志保存在類代碼中。如果優(yōu)化構(gòu)建過程將調(diào)試標(biāo)志移除掉,這種方式就可能無法正常執(zhí)行。
替代的方案是使用參數(shù)在整個參數(shù)列表中的位置信息:
<bean id="cdPlayer" class="soundsystem.CDPlayer"
c:_0-ref="compactDisc" />
將參數(shù)名稱替換成了“0”,即參數(shù)的索引。由于在XML中不允許數(shù)字作為屬性的第一個字符,要添加下畫線作為前綴。
使用索引識別構(gòu)造器參數(shù)會比使用名字好一些,這樣即便在構(gòu)建的時候移除掉了調(diào)試標(biāo)志,參數(shù)依然會保持相同的順序。當(dāng)只有一個構(gòu)造器參數(shù)的時候,可以不標(biāo)示參數(shù):
<bean id="cdPlayer" class="soundsystem.CDPlayer"
c:_-ref="compactDisc" />
現(xiàn)在已經(jīng)將引用裝配到了其他的bean中,接下來討論將字面量值裝配到構(gòu)造器之中。
將字面量注入到構(gòu)造器
到目前所做的DI通常指的是類型的裝配——將對象的引用裝配到依賴于它們的其他對象之中。而有時候,需要做的只是一個字面量值來配置對象。假設(shè)要創(chuàng)建一個CompactDisc的新實現(xiàn):
package soundsystem;
public class BlankDisc implements CompactDisc {
private String title;
private String artist;
public BlankDisc(String title, String artist) {
this.title = title;
this.artist = artist;
}
public void play() {
System.out.println("Playing " + title + " by " + artist);
}
}
將已有的SgtPeppers替換為這個類:
<bean id="compactDisc" class="soundsystem.BlankDisc">
<constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
<constructor-arg value="The Beatles" />
</bean>
再次使用<constructor-arg>元素進(jìn)行構(gòu)造器參數(shù)的注入。沒有使用ref屬性引用其他的bean,這次使用value屬性,通過該屬性表明給定的值要以字面量的形式注入到構(gòu)造器之中。
如果使用c-命名空間,引用構(gòu)造器參數(shù)的名字:
<bean id="compactDisc" class="soundsystem.BlankDisc"
c:_title="Sgt. Pepper's Lonely Hearts Club Band"
c:_artist="The Beatles" />
裝配自變量與裝配引用的區(qū)別在于屬性名去掉了-ref后綴。類似的,可以通過參數(shù)索引裝配字面量值:
<bean id="compactDisc" class="soundsystem.BlankDisc"
c:_0="Sgt. Pepper's Lonely Hearts Club Band"
c:_1="The Beatles" />
如果構(gòu)造器參數(shù)只有一個,同樣可以簡單的使用下畫線進(jìn)行標(biāo)示,如:
<bean id="compactDisc" class="soundsystem.BlankDisc"
c:_="Sgt. Pepper's Lonely Hearts Club Band"
裝配集合
有一種情況是<constructor-arg>能夠?qū)崿F(xiàn),c命名空間卻無法做到的。
真正的CD上面所承載的信息很多,大多數(shù)CD會包含十多個磁道,每個磁道包含了一首歌。
如果使用CompactDisc為真正的CD建模,它也應(yīng)該有磁道列表的概念:
package soundsystem.collections;
import java.util.List;
import soundsystem.CompactDisc;
public class BlankDisc implements CompactDisc {
private String title;
private String artist;
private List<String> tracks;
public BlankDisc(String title, String artist, List<String> tracks) {
this.title = title;
this.artist = artist;
this.tracks = tracks;
}
public void play() {
System.out.println("Playing " + title + " by " + artist);
for (String track : tracks) {
System.out.println("-Track: " + track);
}
}
}
這個變更使得在聲明bean的時候,必須要提供一個磁道列表。
不理想的方案是將列表設(shè)置為null:
<bean id="compactDisc" class="soundsystem.BlankDisc">
<constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
<constructor-arg value="The Beatles" />
<constructor-arg><null/></constructor-arg>
</bean>
注入期能夠正常執(zhí)行,當(dāng)調(diào)用play()方法時,會遇到NullPointerException異常。
更好的解決辦法是提供一個磁道名稱的列表。有多個可選的解決方案,其中之一,可以使用<list>元素將其聲明為一個列表:
<bean id="compactDisc"
class="soundsystem.properties.BlankDisc">
<constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
<constructor-arg name="artist" value="The Beatles" />
<constructor-arg name="tracks">
<list>
<value>Sgt. Pepper's Lonely Hearts Club Band</value>
<value>With a Little Help from My Friends</value>
<value>Lucy in the Sky with Diamonds</value>
<value>Getting Better</value>
<value>Fixing a Hole</value>
<value>She's Leaving Home</value>
<value>Being for the Benefit of Mr. Kite!</value>
<!-- ...others... -->
</list>
</property>
</bean>
其中<list>元素時<constructor-arg>的子元素,這表明一個包含值得列表會被傳遞到構(gòu)造器中。<value>元素用來指定列表中的每個元素。
類似的,也可以使用ref元素替代<value>,實現(xiàn)bean引用列表的裝配。
例如有一個Discography類,它的構(gòu)造器如下所示:
public Discography(String artist, List<CompactDisc> cds){ ... }
則可以采用如下的方式配置Discography bean:
<bean id="beatlesDiscography" class="soundsystem.Discography">
<constructor-arg value="The Beatles" />
<constructor-arg>
<List>
<ref bean="sgtPeppers" />
<ref bean="whiteAlbum" />
<ref bean="hardDaysNight" />
<ref bean="revolver" />
<!-- ...others... -->
<List>
</constructor-arg>
</bean>
當(dāng)構(gòu)造器參數(shù)的類型是 java.util.List時,使用<List>元素合情合理。但也可以按照同樣的方式使用<set>元素:
<bean id="compactDisc" class="soundsystem.BlankDisc">
<constructor-arg value="Sgt.Pepper's Lonely Hearts Club Band" />
<constructor-arg value="The Beatles" />
<constructor-arg>
<Set>
<value>Sgt. Pepper's Lonely Hearts Club Band</value>
<value>With a Little Help from My Friends</value>
<value>Lucy in the Sky with Diamonds</value>
<value>Getting Better</value>
<!-- ...others... -->
<List>
</constructor-arg>
</bean>
<set>和<list>元素的區(qū)別不大,最重要的不同在于當(dāng)Spring創(chuàng)建要裝配的集合時,所創(chuàng)建的是java.util.Set還是java.util.List。如果是Set的話,重復(fù)值會被忽略,存放的順序也不會得以保證。無論哪種情況下,<Set>或<List>都可以用來裝配List、Set甚至數(shù)組。
裝配集合方面,<constructor-arg>比 c-命名空間的屬性更有優(yōu)勢。目前,使用 c-命名空間的屬性無法實現(xiàn)裝配集合的功能。
設(shè)置屬性
到目前為止,CDPlayer和BlankDisc類完全是通過構(gòu)造器注入的,沒有使用屬性的Setter方法。接下來將討論如何使用Spring XML實現(xiàn)屬性注入。假設(shè)屬性注入的CDPlayer如下所示:
package soundsystem.properties;
import org.springframework.beans.factory.annotation.Autowired;
import soundsystem.CompactDisc;
import soundsystem.MediaPlayer;
public class CDPlayer implements MediaPlayer {
private CompactDisc compactDisc;
@Autowired
public void setCompactDisc(CompactDisc compactDisc) {
this.compactDisc = compactDisc;
}
public void play() {
compactDisc.play();
}
}
對于該選擇構(gòu)造器注入還是屬性注入。作為通用的規(guī)則,建議對強(qiáng)依賴使用構(gòu)造器注入,對可選性的依賴使用屬性注入。
現(xiàn)在CDPlayer沒有任何的構(gòu)造器。也沒有任何的強(qiáng)依賴。因此可以采用如下的方式將其聲明為Spring bean:
<bean id="cdPlayer" class="soundsystem.CDPlayer" />
Spring在創(chuàng)建bean時不會有問題,但在測試時會因為NullPointerException導(dǎo)致測試失敗,因為沒有注入CDPlayer的compactDisc屬性。按照如下方式修改XML:
<bean id="cdPlayer" class="soundsystem.CDPlayer">
<property name="compactDisc" ref="compactDisc">
</bean>
<property>元素為屬性的Setter方法所提供的功能等同于<constructor-arg>元素為構(gòu)造器所提供的功能。本例中,它引用了ID為compactDisc的bean,并將其注入到compactDisc屬性中(通過setCompactDisc()方法)。
Spring提供了更加簡潔的p命名空間作為<property>元素的替代方案。為了啟用 p-命名空間,必須要在XML文件中與其他的命名空間一起對其進(jìn)行生命:
<?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:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
...
</bean>
使用p-命名空間:
<bean id="cdPlayer"
class="soundsystem.properties.CDPlayer"
p:compactDisc-ref="compactDisc" />
p-命名空間中屬性所遵循的命名約定與c-命名空間類似。屬性名組成如下圖所示:
將字面量注入到屬性中
構(gòu)建一個新的完全通過屬性注入進(jìn)行配置的BlankDisc類:
package soundsystem.properties;
import java.util.List;
import soundsystem.CompactDisc;
public class BlankDisc implements CompactDisc {
private String title;
private String artist;
private List<String> tracks;
public void setTitle(String title) {
this.title = title;
}
public void setArtist(String artist) {
this.artist = artist;
}
public void setTracks(List<String> tracks) {
this.tracks = tracks;
}
public void play() {
System.out.println("Playing " + title + " by " + artist);
for (String track : tracks) {
System.out.println("-Track: " + track);
}
}
}
可以借助<property>元素的value屬性裝配屬性:
<bean id="compactDisc"
class="soundsystem.properties.BlankDisc">
<property name="title" value="Sgt. Pepper's Lonely Hearts Club Band" />
<property name="artist" value="The Beatles" />
<property name="tracks">
<list>
<value>Sgt. Pepper's Lonely Hearts Club Band</value>
<value>With a Little Help from My Friends</value>
<value>Lucy in the Sky with Diamonds</value>
<value>Getting Better</value>
<!-- ...others... -->
</list>
</property>
</bean>
這里除了使用<property>元素的value屬性來設(shè)置title和artist,還使用了內(nèi)嵌的<lists>元素來設(shè)置tracks屬性。
另一種可選方案是使用p命名空間的屬性來完成該功能:
<bean id="compactDisc"
class="soundsystem.properties.BlankDisc"
p:title="Sgt. Pepper's Lonely Hearts Club Band"
p:artist="The Beatles">
<property name="tracks">
<list>
<value>Sgt. Pepper's Lonely Hearts Club Band</value>
<value>With a Little Help from My Friends</value>
<value>Lucy in the Sky with Diamonds</value>
<value>Getting Better</value>
<!-- ...other tracks... -->
</list>
</property>
</bean>
沒有便利的方式使用 p-命名空間來指定一個值的列表。但是,可以使用Spring util-命名空間中的一些功能來簡化BlankDisc bean。
首先,在XML中聲明util-命名空間及其模式:
<?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:p="http://www.springframework.org/schema/p"
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">
...
</beans>
util-命名空間提供的功能之一就是<util:list>元素,它會創(chuàng)建一個列表的bean。借助<util:list>,可以將磁道列表轉(zhuǎn)移到BlankDisc bean之外。并將其聲明到單獨的bean中:
<util:list id="trackList">
<value>Sgt. Pepper's Lonely Hearts Club Band</value>
<value>With a Little Help from My Friends</value>
<value>Lucy in the Sky with Diamonds</value>
<value>Getting Better</value>
<!-- ...other tracks... -->
</util:list>
現(xiàn)在能夠像使用其他的bean那樣,將磁道列表bean注入到BlankDisc bean的tracks屬性中:
<bean id="compactDisc"
class="soundsystem.properties.BlankDisc"
p:title="Sgt. Pepper's Lonely Hearts Club Band"
p:artist="The Beatles"
p:tracks-ref="trackList" />
<util:list>只是util-命名空間中的多個元素之一。下表列出了 util-命名空間提供的所有元素:
元素 | 描述 |
---|---|
<util:constant> | 引用某個類型的public static域,并將其暴露為bean |
util:list | 創(chuàng)建一個java.util.List類型的bean,其中包含值或引用 |
util:map | 創(chuàng)建一個java.util.Map類型的bean,其中包含值或引用 |
util:properties | 創(chuàng)建一個java.util.Properties類型的bean |
util:property-path | 引用一個bean的屬性(或內(nèi)嵌屬性),并將其暴露為bean |
util:set | 創(chuàng)建一個java.util.Set,其中包含值或引用 |
導(dǎo)入和混合配置
Spring中,這些配置方案都不是互斥的。盡可以將JavaConfig的組建掃描和自動裝配和XML配置混合在一起。
混合配置第一件需要了解的事情就是在自動裝配時,自動裝配會將Spring容器中所有的bean都考慮到,不管是在JavaConfig或XML聲明的還是通過組建掃描獲取到的。
在JavaConfig中引用XMl配置
假設(shè)CDPlayerConfig已經(jīng)變得復(fù)雜,想要將其進(jìn)行拆分。實現(xiàn)的第一種方案就是將SgtPeppers從CDPlayerConfig拆分出來,定義到它自己的CDConfig中:
package soundsystem;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CDConfig {
@Bean
public CompactDisc compactDisc() {
return new SgtPeppers();
}
}
compactDisc()方法已經(jīng)從CDPlayerConfig中移除掉了,需要有一種方式將這兩個類組合在一起。一種方法是在CDPlayerConfig中使用@Import注解導(dǎo)入CDConfig:
package soundsystem;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(CDPlayerConfig.class)
public class CDPlayerConfig{
@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc) {
return new CDPlayer(compactDisc);
}
}
或者采用一個更好的辦法,也就是不在CDPlayerConfig中使用@Import,創(chuàng)建一個更高級別的SoundSystemConfig,在這個類中使用@Import將兩個配置組合在一起:
package soundsystem;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({CDPlayerConfig.class,CDConfig.class})
public class SoundSystemConfig {
}
不管使用哪種方法,都將CDPlayer的配置與SgtPeppers的配置分開了。現(xiàn)在假設(shè)希望通過XML來配置BlankDisc,如下所示:
<bean id="compactDisc" class="soundsystem.BlankDisc"
c:_0="Sgt. Pepper's Lonely Hearts Club Band"
c:_1="The Beatles" />
<constructor-arg>
<list>
<value>Sgt. Pepper's Lonely Hearts Club Band</value>
<value>With a Little Help from My Friends</value>
<value>Lucy in the Sky with Diamonds</value>
<value>Getting Better</value>
<!-- ...other tracks... -->
</list>
</constructor-arg>
</bean>
現(xiàn)在BlankDisc配置在了XML中,如何讓Spring同時加載它和其他基于Java的配置?
答案是@ImportResource注解,假設(shè)BlankDisc定義在名為cd-config.xml的文件中,該文件位于根類路徑下,那么可以修改SoundSystemConfig,讓它使用@ImportResource注解,如下所示:
package soundsystem;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;
@Configuration
@Import(CDPlayerConfig.class)
@ImportResource("classpath:cd-config.xml")
public class SoundSystemConfig{
}
兩個bean——配置在JavaConfig中的CDPlayer以及配置在XML中的BlankDisc都會被加載到Spring容器之中。因為CDPlayer中帶有@Bean注解的方法接受一個CompactDisc作為參數(shù),因此BlankDisc將會被裝配進(jìn)來。
在XMl配置中引用JavaConfig
假設(shè)正在使用Spring基于XMl的配置并且XML注解變得復(fù)雜。
在XML中,可以使用<import>元素來拆分XML配置。
假設(shè)將BlankDisc bean拆分到自己的配置文件中,該文件名為cd-config.xml。可以在XML配置文件中使用<import>元素來引用該文件:
<?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:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<import resource="cd-config.xml">
<bean id="cdPlayer" class="soundsystem.CDPlayer" c:cd-ref="compactDisc" />
</beans>
現(xiàn)在假設(shè)BlankDisc配置不在XML中,而是配置在JavaConfig中。
<import>元素只能導(dǎo)入其他的XML配置文件,并沒有XML元素可以導(dǎo)入JavaConfig類。
但有一個元素能夠用來將Java配置導(dǎo)入到XML配置中——<bean>元素:
<?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:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="soundsystem.CDConfig">
<bean id="cdPlayer" class="soundsystem.CDPlayer" c:cd-ref="compactDisc" />
</beans>
采用這樣的方式,兩種配置被組合在了一起。類似地,還可以創(chuàng)建一個更高層次的配置文件,這個文件不聲明任何bean,只負(fù)責(zé)將兩個或更多的配置組合起來。
例如,可以將CDConfig bean從之前的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:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="soundsystem.CDConfig">
<import resource="cdplayer-config.xml" />
</beans>
不管使用JavaXML還是XML進(jìn)行配置,通常都會創(chuàng)建一個根配置,這個配置會將兩個或更多的文件組合起來。也會在根配置中啟用組件掃描(通過<context:component-scan>或@componentScan)。
小結(jié)
Spring框架的核心是Spring容器。容器負(fù)責(zé)管理應(yīng)用中組件的生命周期與依賴關(guān)系。
建議盡可能使用自動化配置,避免顯示配置帶來的維護(hù)成本。如果需要顯示配置Spring,應(yīng)該優(yōu)先選擇基于Java的配置。