《Spring實戰(第4版)》——2.2 自動化裝配bean
本節書摘來自異步社區《Spring實戰(第4版)》一書中的第2章,第2.2節,作者: 【美】Craig Walls(沃爾斯)著,更多章節內容可以訪問雲棲社區“異步社區”公眾號查看
2.2 自動化裝配bean
在本章稍後的內容中,你會看到如何借助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的概念
CompactDisc的具體內容並不重要,重要的是你將其定義為一個接口。作為接口,它定義了CD播放器對一盤CD所能進行的操作。它將CD播放器的任意實現與CD本身的耦合降低到了最小的程度。
我們還需要一個CompactDisc的實現,實際上,我們可以有CompactDisc接口的多個實現。在本例中,我們首先會創建其中的一個實現,也就是程序清單2.2所示的SgtPeppers類。
程序清單2.2 帶有@Component注解的CompactDisc實現類SgtPeppers
和CompactDisc接口一樣,SgtPeppers的具體內容並不重要。你需要注意的就是SgtPeppers類上使用了@Component注解。這個簡單的注解表明該類會作為組件類,並告知Spring要為這個類創建bean。沒有必要顯式配置SgtPeppersbean,因為這個類使用了@Component注解,所以Spring會為你把事情處理妥當。
不過,組件掃描默認是不啟用的。我們還需要顯式配置一下Spring,從而命令它去尋找帶有@Component注解的類,並為其創建bean。程序清單2.3的配置類展現了完成這項任務的最簡潔配置。
程序清單2.3 @ComponentScan注解啟用了組件掃描
類CDPlayerConfig通過Java代碼定義了Spring的裝配規則。在2.3節中,我們還會更為詳細地介紹基於Java的Spring配置。不過,現在我們隻需觀察一下CDPlayerConfig類並沒有顯式地聲明任何bean,隻不過它使用了@ComponentScan注解,這個注解能夠在Spring中啟用組件掃描。
如果沒有其他配置的話,@ComponentScan默認會掃描與配置類相同的包。因為CDPlayerConfig類位於soundsystem包中,因此Spring將會掃描這個包以及這個包下的所有子包,查找帶有@Component注解的類。這樣的話,就能發現CompactDisc,並且會在Spring中自動為其創建一個bean。
如果你更傾向於使用XML來啟用組件掃描的話,那麼可以使用Spring context命名空間的context:component-scan元素。程序清單2.4展示了啟用組件掃描的最簡潔XML配置。
程序清單2.4 通過XML啟用組件掃描
盡管我們可以通過XML的方案來啟用組件掃描,但是在後麵的討論中,我更多的還是會使用基於Java的配置。如果你更喜歡XML的話,context:component-scan元素會有與@ComponentScan注解相對應的屬性和子元素。
可能有點讓人難以置信,我們隻創建了兩個類,就能對功能進行一番嚐試了。為了測試組件掃描的功能,我們創建一個簡單的JUnit測試,它會創建Spring上下文,並判斷CompactDisc是不是真的創建出來了。程序清單2.5中的CDPlayerTest就是用來完成這項任務的。
程序清單2.5 測試組件掃描能夠發現CompactDisc
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注解配置為如下所示:
還有另外一種為bean命名的方式,這種方式不使用@Component注解,而是使用Java依賴注入規範(Java Dependency Injection)中所提供的@Named注解來為bean設置ID:
Spring支持將@Named作為@Component注解的替代方案。兩者之間有一些細微的差異,但是在大多數場景中,它們是可以互相替換的。
話雖如此,我更加強烈地喜歡@Component注解,而對於@Named……怎麼說呢,我感覺它的名字起得很不好。它並沒有像@Component那樣清楚地表明它是做什麼的。因此在本書及其示例代碼中,我不會再使用@Named。
2.2.3 設置組件掃描的基礎包
到現在為止,我們沒有為@ComponentScan設置任何屬性。這意味著,按照默認規則,它會以配置類所在的包作為基礎包(base package)來掃描組件。但是,如果你想掃描不同的包,那該怎麼辦呢?或者,如果你想掃描多個基礎包,那又該怎麼辦呢?
有一個原因會促使我們明確地設置基礎包,那就是我們想要將配置類放在單獨的包中,使其與其他的應用代碼區分開來。如果是這樣的話,那默認的基礎包就不能滿足要求了。
要滿足這樣的需求其實也完全沒有問題!為了指定不同的基礎包,你所需要做的就是在@ComponentScan的value屬性中指明包的名稱:
如果你想更加清晰地表明你所設置的是基礎包,那麼你可以通過basePackages屬性進行配置:
可能你已經注意到了basePackages屬性使用的是複數形式。如果你揣測這是不是意味著可以設置多個基礎包,那麼恭喜你猜對了。如果想要這麼做的話,隻需要將basePackages屬性設置為要掃描包的一個數組即可:
在上麵的例子中,所設置的基礎包是以String類型表示的。我認為這是可以的,但這種方法是類型不安全(not type-safe)的。如果你重構代碼的話,那麼所指定的基礎包可能就會出現錯誤了。
除了將包設置為簡單的String類型之外,@ComponentScan還提供了另外一種方法,那就是將其指定為包中所包含的類或接口:
可以看到,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之中
@Autowired注解不僅能夠用在構造器上,還能用在屬性的Setter方法上。比如說,如果CDPlayer有一個setCompactDisc()方法,那麼可以采用如下的注解形式進行自動裝配:
在Spring初始化bean之後,它會盡可能得去滿足bean的依賴,在本例中,依賴是通過帶有@Autowired注解的方法進行聲明的,也就是setCompactDisc()。
實際上,Setter方法並沒有什麼特殊之處。@Autowired注解可以用在類的任何方法上。假設CDPlayer類有一個insertDisc()方法,那麼@Autowired能夠像在setCompactDisc()上那樣,發揮完全相同的作用:
不管是構造器、Setter方法還是其他的方法,Spring都會嚐試滿足方法參數上所聲明的依賴。假如有且隻有一個bean匹配依賴需求的話,那麼這個bean將會被裝配進來。
如果沒有匹配的bean,那麼在應用上下文創建的時候,Spring會拋出一個異常。為了避免異常的出現,你可以將@Autowired的required屬性設置為false:
將required屬性設置為false時,Spring會嚐試執行自動裝配,但是如果沒有匹配的bean的話,Spring將會讓這個bean處於未裝配的狀態。但是,把required屬性設置為false時,你需要謹慎對待。如果在你的代碼中沒有進行null檢查的話,這個處於未裝配狀態的屬性有可能會出現NullPointerException。
如果有多個bean都能滿足依賴關係的話,Spring將會拋出一個異常,表明沒有明確指定要選擇哪個bean進行自動裝配。在第3章中,我們會進一步討論自動裝配中的歧義性。
@Autowired是Spring特有的注解。如果你不願意在代碼中到處使用Spring的特定注解來完成自動裝配任務的話,那麼你可以考慮將其替換為@Inject:
@Inject注解來源於Java依賴注入規範,該規範同時還為我們定義了@Named注解。在自動裝配中,Spring同時支持@Inject和@Autowired。盡管@Inject和@Autowired之間有著一些細微的差別,但是在大多數場景下,它們都是可以互相替換的。
在@Inject和@Autowired中,我沒有特別強烈的偏向性。實際上,在有的項目中,我會發現我同時使用了這兩個注解。不過在本書的樣例中,我會一直使用@Autowired,而你可以根據自己的情況,選擇其中的任意一個。
2.2.5 驗證自動裝配
現在,我們已經在CDPlayer的構造器中添加了@Autowired注解,Spring將把一個可分配給CompactDisc類型的bean自動注入進來。為了驗證這一點,讓我們修改一下CDPlayerTest,使其能夠借助CDPlayer bean播放CD:
現在,除了注入CompactDisc,我們還將CDPlayerbean注入到測試代碼的player成員變量之中(它是更為通用的MediaPlayer類型)。在play()測試方法中,我們可以調用CDPlayer的play()方法,並斷言它的行為與你的預期一致。
在測試代碼中使用System.out.println()是稍微有點棘手的事情。因此,該樣例中使用了StandardOutputStreamLog,這是來源於System Rules庫的一個JUnit規則,該規則能夠基於控製台的輸出編寫斷言。在這裏,我們斷言SgtPeppers.play()方法的輸出被發送到了控製台上。
現在,你已經了解了組件掃描和自動裝配的基礎知識,在第3章中,當我們介紹如何處理自動裝配的歧義性時,還會繼續研究組件掃描。
但是現在,我們先將組件掃描和自動裝配放在一邊,看一下在Spring中如何顯式地裝配bean,首先從通過Java代碼編寫配置開始。
最後更新:2017-05-31 11:03:08