天天看點

《Spring實戰(第4版)》——2.2 自動化裝配bean

本節書摘來自異步社群《spring實戰(第4版)》一書中的第2章,第2.2節,作者: 【美】craig walls(沃爾斯)著,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

在本章稍後的内容中,你會看到如何借助java和xml來進行spring裝配。盡管你會發現這些顯式裝配技術非常有用,但是在便利性方面,最強大的還是spring的自動化配置。如果spring能夠進行自動化裝配的話,那何苦還要顯式地将這些bean裝配在一起呢?

spring從兩個角度來實作自動化裝配:

元件掃描(component scanning):spring會自動發現應用上下文中所建立的bean。

自動裝配(autowiring):spring自動滿足bean之間的依賴。

元件掃描和自動裝配組合在一起就能發揮出強大的威力,它們能夠将你的顯式配置降低到最少。

為了闡述元件掃描和裝配,我們需要建立幾個bean,它們代表了一個音響系統中的元件。首先,要建立compactdisc類,spring會發現它并将其建立為一個bean。然後,會建立一個cdplayer類,讓spring發現它,并将compactdiscbean注入進來。

2.2.1 建立可被發現的bean

在這個mp3和流式媒體音樂的時代,cd(compact disc)顯得有點典雅甚至陳舊。它不像卡帶機、八軌錄音帶、塑膠唱片那麼普遍,随着以實體載體進行音樂傳遞的方式越來越少,cd也變得越來越稀少了。

盡管如此,cd為我們闡述di如何運作提供了一個很好的樣例。如果你不将cd插入(注入)到cd播放器中,那麼cd播放器其實是沒有太大用處的。是以,可以這樣說,cd播放器依賴于cd才能完成它的使命。

為了在spring中闡述這個例子,讓我們首先在java中建立cd的概念。程式清單2.1展現了compactdisc,它是定義cd的一個接口:

程式清單2.1 compactdisc接口在java中定義了cd的概念

《Spring實戰(第4版)》——2.2 自動化裝配bean

compactdisc的具體内容并不重要,重要的是你将其定義為一個接口。作為接口,它定義了cd播放器對一盤cd所能進行的操作。它将cd播放器的任意實作與cd本身的耦合降低到了最小的程度。

我們還需要一個compactdisc的實作,實際上,我們可以有compactdisc接口的多個實作。在本例中,我們首先會建立其中的一個實作,也就是程式清單2.2所示的sgtpeppers類。

程式清單2.2 帶有@component注解的compactdisc實作類sgtpeppers

《Spring實戰(第4版)》——2.2 自動化裝配bean

和compactdisc接口一樣,sgtpeppers的具體内容并不重要。你需要注意的就是sgtpeppers類上使用了@component注解。這個簡單的注解表明該類會作為元件類,并告知spring要為這個類建立bean。沒有必要顯式配置sgtpeppersbean,因為這個類使用了@component注解,是以spring會為你把事情處理妥當。

不過,元件掃描預設是不啟用的。我們還需要顯式配置一下spring,進而指令它去尋找帶有@component注解的類,并為其建立bean。程式清單2.3的配置類展現了完成這項任務的最簡潔配置。

程式清單2.3 @componentscan注解啟用了元件掃描

《Spring實戰(第4版)》——2.2 自動化裝配bean

類cdplayerconfig通過java代碼定義了spring的裝配規則。在2.3節中,我們還會更為詳細地介紹基于java的spring配置。不過,現在我們隻需觀察一下cdplayerconfig類并沒有顯式地聲明任何bean,隻不過它使用了@componentscan注解,這個注解能夠在spring中啟用元件掃描。

如果沒有其他配置的話,@componentscan預設會掃描與配置類相同的包。因為cdplayerconfig類位于soundsystem包中,是以spring将會掃描這個包以及這個包下的所有子包,查找帶有@component注解的類。這樣的話,就能發現compactdisc,并且會在spring中自動為其建立一個bean。

如果你更傾向于使用xml來啟用元件掃描的話,那麼可以使用spring context命名空間的元素。程式清單2.4展示了啟用元件掃描的最簡潔xml配置。

程式清單2.4 通過xml啟用元件掃描

《Spring實戰(第4版)》——2.2 自動化裝配bean

盡管我們可以通過xml的方案來啟用元件掃描,但是在後面的讨論中,我更多的還是會使用基于java的配置。如果你更喜歡xml的話,元素會有與@componentscan注解相對應的屬性和子元素。

可能有點讓人難以置信,我們隻建立了兩個類,就能對功能進行一番嘗試了。為了測試元件掃描的功能,我們建立一個簡單的junit測試,它會建立spring上下文,并判斷compactdisc是不是真的建立出來了。程式清單2.5中的cdplayertest就是用來完成這項任務的。

程式清單2.5 測試元件掃描能夠發現compactdisc

《Spring實戰(第4版)》——2.2 自動化裝配bean

cdplayertest使用了spring的springjunit4classrunner,以便在測試開始的時候自動建立spring的應用上下文。注解@contextconfiguration會告訴它需要在cdplayerconfig中加載配置。因為cdplayerconfig類中包含了@componentscan,是以最終的應用上下文中應該包含compactdiscbean。

為了證明這一點,在測試代碼中有一個compactdisc類型的屬性,并且這個屬性帶有@autowired注解,以便于将compactdiscbean注入到測試代碼之中(稍後,我會讨論@autowired)。最後,會有一個簡單的測試方法斷言cd屬性不為null。如果它不為null的話,就意味着spring能夠發現compactdisc類,自動在spring上下文中将其建立為bean并将其注入到測試代碼之中。

這個代碼應該能夠通過測試,并以測試成功的顔色顯示(在你的測試運作器中,或許會希望出現綠色)。你第一個簡單的元件掃描練習就成功了!盡管我們隻用它建立了一個bean,但同樣是這麼少的配置能夠用來發現和建立任意數量的bean。在soundsystem包及其子包中,所有帶有@component注解的類都會建立為bean。隻添加一行@componentscan注解就能自動建立無數個bean,這種權衡還是很劃算的。

現在,我們會更加深入地探讨@componentscan和@component,看一下使用元件掃描還能做些什麼。

2.2.2 為元件掃描的bean命名

spring應用上下文中所有的bean都會給定一個id。在前面的例子中,盡管我們沒有明确地為sgtpeppersbean設定id,但spring會根據類名為其指定一個id。具體來講,這個bean所給定的id為sgtpeppers,也就是将類名的第一個字母變為小寫。

如果想為這個bean設定不同的id,你所要做的就是将期望的id作為值傳遞給@component注解。比如說,如果想将這個bean辨別為lonelyheartsclub,那麼你需要将sgtpeppers類的@component注解配置為如下所示:

《Spring實戰(第4版)》——2.2 自動化裝配bean

還有另外一種為bean命名的方式,這種方式不使用@component注解,而是使用java依賴注入規範(java dependency injection)中所提供的@named注解來為bean設定id:

《Spring實戰(第4版)》——2.2 自動化裝配bean

spring支援将@named作為@component注解的替代方案。兩者之間有一些細微的差異,但是在大多數場景中,它們是可以互相替換的。

話雖如此,我更加強烈地喜歡@component注解,而對于@named……怎麼說呢,我感覺它的名字起得很不好。它并沒有像@component那樣清楚地表明它是做什麼的。是以在本書及其示例代碼中,我不會再使用@named。

2.2.3 設定元件掃描的基礎包

到現在為止,我們沒有為@componentscan設定任何屬性。這意味着,按照預設規則,它會以配置類所在的包作為基礎包(base package)來掃描元件。但是,如果你想掃描不同的包,那該怎麼辦呢?或者,如果你想掃描多個基礎包,那又該怎麼辦呢?

有一個原因會促使我們明确地設定基礎包,那就是我們想要将配置類放在單獨的包中,使其與其他的應用代碼區分開來。如果是這樣的話,那預設的基礎包就不能滿足要求了。

要滿足這樣的需求其實也完全沒有問題!為了指定不同的基礎包,你所需要做的就是在@componentscan的value屬性中指明包的名稱:

《Spring實戰(第4版)》——2.2 自動化裝配bean

如果你想更加清晰地表明你所設定的是基礎包,那麼你可以通過basepackages屬性進行配置:

《Spring實戰(第4版)》——2.2 自動化裝配bean

可能你已經注意到了basepackages屬性使用的是複數形式。如果你揣測這是不是意味着可以設定多個基礎包,那麼恭喜你猜對了。如果想要這麼做的話,隻需要将basepackages屬性設定為要掃描包的一個數組即可:

《Spring實戰(第4版)》——2.2 自動化裝配bean

在上面的例子中,所設定的基礎包是以string類型表示的。我認為這是可以的,但這種方法是類型不安全(not type-safe)的。如果你重構代碼的話,那麼所指定的基礎包可能就會出現錯誤了。

除了将包設定為簡單的string類型之外,@componentscan還提供了另外一種方法,那就是将其指定為包中所包含的類或接口:

《Spring實戰(第4版)》——2.2 自動化裝配bean

可以看到,basepackages屬性被替換成了basepackageclasses。同時,我們不是再使用string類型的名稱來指定包,為basepackageclasses屬性所設定的數組中包含了類。這些類所在的包将會作為元件掃描的基礎包。

盡管在樣例中,我為basepackageclasses設定的是元件類,但是你可以考慮在包中建立一個用來進行掃描的空标記接口(marker interface)。通過标記接口的方式,你依然能夠保持對重構友好的接口引用,但是可以避免引用任何實際的應用程式代碼(在稍後重構中,這些應用代碼有可能會從想要掃描的包中移除掉)。

在你的應用程式中,如果所有的對象都是獨立的,彼此之間沒有任何依賴,就像sgtpeppersbean這樣,那麼你所需要的可能就是元件掃描而已。但是,很多對象會依賴其他的對象才能完成任務。這樣的話,我們就需要有一種方法能夠将元件掃描得到的bean和它們的依賴裝配在一起。要完成這項任務,我們需要了解一下spring自動化配置的另外一方面内容,那就是自動裝配。

2.2.4 通過為bean添加注解實作自動裝配

簡單來說,自動裝配就是讓spring自動滿足bean依賴的一種方法,在滿足依賴的過程中,會在spring應用上下文中尋找比對某個bean需求的其他bean。為了聲明要進行自動裝配,我們可以借助spring的@autowired注解。

比方說,考慮程式清單2.6中的cdplayer類。它的構造器上添加了@autowired注解,這表明當spring建立cdplayerbean的時候,會通過這個構造器來進行執行個體化并且會傳入一個可設定給compactdisc類型的bean。

程式清單2.6 通過自動裝配,将一個compactdisc注入到cdplayer之中

《Spring實戰(第4版)》——2.2 自動化裝配bean

@autowired注解不僅能夠用在構造器上,還能用在屬性的setter方法上。比如說,如果cdplayer有一個setcompactdisc()方法,那麼可以采用如下的注解形式進行自動裝配:

《Spring實戰(第4版)》——2.2 自動化裝配bean

在spring初始化bean之後,它會盡可能得去滿足bean的依賴,在本例中,依賴是通過帶有@autowired注解的方法進行聲明的,也就是setcompactdisc()。

實際上,setter方法并沒有什麼特殊之處。@autowired注解可以用在類的任何方法上。假設cdplayer類有一個insertdisc()方法,那麼@autowired能夠像在setcompactdisc()上那樣,發揮完全相同的作用:

《Spring實戰(第4版)》——2.2 自動化裝配bean

不管是構造器、setter方法還是其他的方法,spring都會嘗試滿足方法參數上所聲明的依賴。假如有且隻有一個bean比對依賴需求的話,那麼這個bean将會被裝配進來。

如果沒有比對的bean,那麼在應用上下文建立的時候,spring會抛出一個異常。為了避免異常的出現,你可以将@autowired的required屬性設定為false:

《Spring實戰(第4版)》——2.2 自動化裝配bean

将required屬性設定為false時,spring會嘗試執行自動裝配,但是如果沒有比對的bean的話,spring将會讓這個bean處于未裝配的狀态。但是,把required屬性設定為false時,你需要謹慎對待。如果在你的代碼中沒有進行null檢查的話,這個處于未裝配狀态的屬性有可能會出現nullpointerexception。

如果有多個bean都能滿足依賴關系的話,spring将會抛出一個異常,表明沒有明确指定要選擇哪個bean進行自動裝配。在第3章中,我們會進一步讨論自動裝配中的歧義性。

@autowired是spring特有的注解。如果你不願意在代碼中到處使用spring的特定注解來完成自動裝配任務的話,那麼你可以考慮将其替換為@inject:

《Spring實戰(第4版)》——2.2 自動化裝配bean

@inject注解來源于java依賴注入規範,該規範同時還為我們定義了@named注解。在自動裝配中,spring同時支援@inject和@autowired。盡管@inject和@autowired之間有着一些細微的差别,但是在大多數場景下,它們都是可以互相替換的。

在@inject和@autowired中,我沒有特别強烈的偏向性。實際上,在有的項目中,我會發現我同時使用了這兩個注解。不過在本書的樣例中,我會一直使用@autowired,而你可以根據自己的情況,選擇其中的任意一個。

2.2.5 驗證自動裝配

現在,我們已經在cdplayer的構造器中添加了@autowired注解,spring将把一個可配置設定給compactdisc類型的bean自動注入進來。為了驗證這一點,讓我們修改一下cdplayertest,使其能夠借助cdplayer bean播放cd:

《Spring實戰(第4版)》——2.2 自動化裝配bean

現在,除了注入compactdisc,我們還将cdplayerbean注入到測試代碼的player成員變量之中(它是更為通用的mediaplayer類型)。在play()測試方法中,我們可以調用cdplayer的play()方法,并斷言它的行為與你的預期一緻。

在測試代碼中使用system.out.println()是稍微有點棘手的事情。是以,該樣例中使用了standardoutputstreamlog,這是來源于system rules庫的一個junit規則,該規則能夠基于控制台的輸出編寫斷言。在這裡,我們斷言sgtpeppers.play()方法的輸出被發送到了控制台上。

現在,你已經了解了元件掃描和自動裝配的基礎知識,在第3章中,當我們介紹如何處理自動裝配的歧義性時,還會繼續研究元件掃描。

但是現在,我們先将元件掃描和自動裝配放在一邊,看一下在spring中如何顯式地裝配bean,首先從通過java代碼編寫配置開始。