Spring實戰2:裝配bean—依賴注入的本質

主要內容

  • Spring的配置方法概覽
  • 自動裝配bean
  • 基于Java配置文件裝配bean
  • 控制bean的創建和銷毀

任何一個成功的應用都是由多個為了實現某個業務目標而相互協作的組件構成的,這些組件必須相互了解、能夠相互協作完成工作。例如,在一個在線購物系統中,訂單管理組件需要與產品管理組件以及信用卡認證組件協作;這些組件還需要跟數據庫組件協作從而進行數據庫讀寫操作。

在Spring應用中,對象無需自己負責查找或者創建與其關聯的其他對象,由容器負責將創建各個對象,并創建各個對象之間的依賴關系。例如,一個訂單管理組件需要使用信用卡認證組件,它不需要自己創建信用卡認證組件,只需要定義它需要使用信用卡認證組件即可,容器會創建信用卡認證組件然后將該組件的引用注入給訂單管理組件。

創建各個對象之間協作關系的行為通常被稱為裝配(wiring),這就是依賴注入(DI)的本質。

2.1 Spring的配置方法概覽

正如在Spring初探一文中提到的,Spring容器負責創建應用中的bean,并通過DI維護這些bean之間的協作關系。作為開發人員,你應該負責告訴Spring容器需要創建哪些bean以及如何將各個bean裝配到一起。Spring提供三種裝配bean的方式:

  • 基于XML文件的顯式裝配
  • 基于Java文件的顯式裝配
  • 隱式bean發現機制和自動裝配

絕大多數情況下,開發人員可以根據個人品味選擇這三種裝配方式中的一種。Spring也支持在同一個項目中混合使用不同的裝配方式。

我的建議是:盡可能使用自動裝配,越少寫顯式的配置文件越好;當你必須使用顯式配置時(例如,你要配置一個bean,但是該bean的源碼不是由你維護),盡可能使用類型安全、功能更強大的基于Java文件的裝配方式;最后,在某些情況下只有XML文件中才又你需要使用的名字空間時,再選擇使用基于XML文件的裝配方式。

2.2 自動裝配bean

Spring通過兩個特性實現自動裝配:

  • Component scanning——Spring自動掃描和創建應用上下文中的beans;
  • Autowiring——Spring自動建立bean之間的依賴關系;

這里用一個例子來說明:假設你需要實現一個音響系統,該系統中包含CDPlayer和CompactDisc兩個組件,Spring將自動發現這兩個bean,并將CompactDisc的引用注入到CDPlayer中。

2.2.1 創建可發現的beans

首先創建CD的概念——CompactDisc接口,如下所示:

package com.spring.sample.soundsystem;

public interface CompactDisc {
    void play();
}

CompactDisc接口的作用是將CDPlayer與具體的CD實現解耦合,即面向接口編程。這里還需定義一個具體的CD實現,如下所示:

package com.spring.sample.soundsystem;

import org.springframework.stereotype.Component;

@Component
public class SgtPeppers implements CompactDisc {
    private String title = "Sgt. Perppers' Lonely Hearts Club Band";
    private String artist = "The Beatles";

    public void play() {
        System.out.println("Playing " + title + " by " + artist);
    }
}

這里最重要的是@Component注解,它告訴Spring需要創建SgtPeppers bean。除此之外,還需要啟動自動掃描機制,有兩種方法:基于XML配置文件;基于Java配置文件,代碼如下(二選一):

  • 創建soundsystem.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"
       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="com.spring.sample.soundsystem" />
</beans>

在這個XML配置文件中,使用<context:component-scan>標簽啟動Component掃描功能,并可設置base-package屬性。

  • 創建Java配置文件
package com.spring.sample.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = "com.spring.sample.soundsystem")
public class SoundSystemConfig {
}

在這個Java配置文件中有兩個注解值得注意:@Configuration表示這個.java文件是一個配置文件;@ComponentScan表示開啟Component掃描,并且可以設置basePackages屬性——Spring將會設置該目錄以及子目錄下所有被@Component注解修飾的類。

  • 自動配置的另一個關鍵注解是@Autowired,基于之前的兩個類和一個Java配置文件,可以寫個測試
package com.spring.sample.soundsystem;

import com.spring.sample.config.SoundSystemConfig;
import 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 = SoundSystemConfig.class)
public class SoundSystemTest {
    @Autowired
    private CompactDisc cd;

    @Test
    public void cdShouldNotBeNull() {
        Assert.assertNotNull(cd);
    }
}

運行測試,測試通過,說明@Autowired注解起作用了:自動將掃描機制創建的CompactDisc類型的bean注入到SoundSystemTest這個bean中。

2.2.2 給被掃描的bean命名

在Spring上下文中,每個bean都有自己的ID。在上一個小節的例子中并沒有提到這一點,但Spring在掃描到SgtPeppers這個組件并創建對應的bean時,默認給它設置的ID為sgtPeppers——是的,這個ID就是將類名稱的首字母小寫。

如果你需要給某個類對應的bean一個特別的名字,則可以給@Component注解傳入指定的參數,例如:

@Component("lonelyHeartsClub")
public class SgtPeppers implements CompactDisc {
  ...
}

2.2.3 設置需要掃描的目標basepackage

在之前的例子中,我們通過給@Component注解傳入字符串形式的包路徑,來設置需要掃描指定目錄下的類并為之創建bean。

可以看出,basePackages是復數,意味著你可以設置多個目標目錄,例如:

@Configuration
@ComponentScan(basePackages = {"com.spring.sample.soundsystem", "com.spring.sample.video"})
public class SoundSystemConfig {
}

這種字符串形式的表示雖然可以,但是不具備“類型安全”,因此Spring也提供了更加類型安全的機制,即通過類或者接口來設置掃描機制的目標目錄,例如:

@Configuration
@ComponentScan(basePackageClasses = {CDPlayer.class, DVDPlayer.class})
public class SoundSystemConfig {
}

通過如上設置,會將CDPlayer和DVDPlayer各自所在的目錄作為掃描機制的目標根目錄。

如果應用中的對象是孤立的,并且互相之間沒有依賴關系,例如SgtPeppersbean,那么這就夠了。

2.2.4 自動裝配bean

簡單得說,自動裝配的意思是讓Spring從應用上下文中找到對應的bean的引用,并將它們注入到指定的bean。通過@Autowired注解可以完成自動裝配。

例如,考慮下面代碼中的CDPlayer類,它的構造函數被@Autowired修飾,表明當Spring創建CDPlayer的bean時,會給這個構造函數傳入一個CompactDisc的bean對應的引用。

package com.spring.sample.soundsystem;

import org.springframework.beans.factory.annotation.Autowired;

@Component
public class CDPlayer implements MediaPlayer {
    private CompactDisc cd;

    @Autowired
    public CDPlayer(CompactDisc cd) {
        this.cd = cd;
    }
    public void play() {
        cd.play();
    }
}

還有別的實現方法,例如將@Autowired注解作用在setCompactDisc()方法上:

@Autowired
public void setCd(CompactDisc cd) {
    this.cd = cd;
}

或者是其他名字的方法上,例如:

@Autowired
public void insertCD(CompactDisc cd) {
    this.cd = cd;
}

更簡單的用法是,可以將@Autowired注解直接作用在成員變量之上,例如:

@Autowired
private CompactDisc cd;

只要對應類型的bean有且只有一個,則會自動裝配到該屬性上。如果沒有找到對應的bean,應用會拋出對應的異常,如果想避免拋出這個異常,則需要設置@Autowired(required=false)。不過,在應用程序設計中,應該謹慎設置這個屬性,因為這會使得你必須面對NullPointerException的問題。

如果存在多個同一類型的bean,則Spring會拋出異常,表示裝配有歧義,解決辦法有兩個:(1)通過@Qualifier注解指定需要的bean的ID;(2)通過@Resource注解指定注入特定ID的bean;

2.2.5 驗證自動配置

通過下列代碼,可以驗證:CompactDisc的bean已經注入到CDPlayer的bean中,同時在測試用例中是將CDPlayer的bean注入到當前測試用例。

package com.spring.sample.soundsystem;

import com.spring.sample.config.SoundSystemConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;import org.slf4j.LoggerFactory;
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 = SoundSystemConfig.class)
public class CDPlayerTest {
    public final Logger log = LoggerFactory.getLogger(CDPlayerTest.class);
    @Autowired
    private MediaPlayer player;

    @Test
    public void playTest() {
        player.play();
    }
}

2.3 基于Java配置文件裝配bean

Java配置文件不同于其他用于實現業務邏輯的Java代碼,因此不能將Java配置文件業務邏輯代碼混在一起。一般都會給Java配置文件新建一個單獨的package。

2.3.1 創建配置類

實際上在之前的例子中我們已經實踐過基于Java的配置文件,看如下代碼:

@Configuration
@ComponentScan(basePackageClasses = {CDPlayer.class, DVDPlayer.class})
public class SoundSystemConfig {
}

@Configuration注解表示這個類是配置類,之前我們是通過@ComponentScan注解實現bean的自動掃描和創建,這里我們重點是學習如何顯式創建bean,因此首先將@ComponentScan(basePackageClasses = {CDPlayer.class, DVDPlayer.class})這行代碼去掉。

2.3.2 定義bean

通過@Bean注解創建一個Spring bean,該bean的默認ID和函數的方法名相同,即sgtPeppers。例如:

@Bean
public CompactDisc sgtPeppers() {
    return new SgtPeppers();
}

同樣,可以指定bean的ID,例如:

@Bean(name = "lonelyHeartsClub")
public CompactDisc sgtPeppers() {
    return new SgtPeppers();
}

可以利用Java語言的表達能力,實現類似工廠模式的代碼如下:

@Bean
public CompactDisc randomBeatlesCD() {
    int choice = (int)Math.floor(Math.random() * 4);

    if (choice == 0) {
        return new SgtPeppers();
    } else if (choice == 1) {
        return new WhiteAlbum();
    } else if (choice == 2) {
        return new HardDaysNight();
    } else if (choice == 3) {
        return new Revolover();
    }
}

2.3.3 JavaConfig中的屬性注入

最簡單的辦法是將被引用的bean的生成函數傳入到構造函數或者set函數中,例如:

@Bean
public CDPlayer cdPlayer() {
    return new CDPlayer(sgtPeppers());
}

看起來是函數調用,實際上不是:由于sgtPeppers()方法被@Bean注解修飾,所以Spring會攔截這個函數調用,并返回之前已經創建好的bean——確保該SgtPeppers bean為單例。

假如有下列代碼:

@Bean
public CDPlayer cdPlayer() {
    return new CDPlayer(sgtPeppers());
}

@Bean
public CDPlayer anotherCDPlayer() {
    return new CDPlayer(sgtPeppers());
}

如果把sgtPeppers()方法當作普通Java方法對待,則cdPlayerbean和anotherCDPlayerbean會持有不同的SgtPeppers實例——結合CDPlayer的業務場景看:就相當于將一片CD同時裝入兩個CD播放機中,顯然這不可能。

默認情況下,Spring中所有的bean都是單例模式,因此cdPlayeranotherCDPlayer這倆bean持有相同的SgtPeppers實例。

當然,還有一種更清楚的寫法:

@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc) {
    return new CDPlayer(compactDisc);
}

@Bean
public CDPlayer anotherCDPlayer() {
    return new CDPlayer(sgtPeppers());
}

這種情況下,cdPlayeranotherCDPlayer這倆bean持有相同的SgtPeppers實例,該實例的ID為lonelyHeartsClub。這種方法最值得使用,因為它不要求CompactDisc bean在同一個配置文件中定義——只要在應用上下文容器中即可(不管是基于自動掃描發現還是基于XML配置文件定義)。

2.4 基于XML配置文件裝配bean

這種是Spring中最原始的定義方式,在此不再詳述。

2.5 混合使用多種配置方法

通常,可能在一個Spring項目中同時使用自動配置和顯式配置,而且,即使你更喜歡JavaConfig,也有很多場景下更適合使用XML配置。幸運的是,這些配置方法可以混合使用。

首先明確一點:對于自動配置,它從整個容器上下文中查找合適的bean,無論這個bean是來自JavaConfig還是XML配置。

2.5.1 在JavaConfig中解析XML配置

  • 通過@Import注解導入其他的JavaConfig,并且支持同時導入多個配置文件;
@Configuration
@Import({CDPlayerConfig.class, CDConfig.class})
public class SoundSystemConfig {
}
  • 通過@ImportResource注解導入XML配置文件;
@Configuration
@Import(CDPlayerConfig.class)
@ImportResource("classpath: cd-config.xml")
public class SoundSystemConfig {
}

2.5.2 在XML配置文件中應用JavaConfig

  • 通過<import>標簽引入其他的XML配置文件;
  • 通過<bean>標簽導入Java配置文件到XML配置文件,例如
<bean class="soundsystem.CDConfig" />

通常的做法是:無論使用JavaConfig或者XML裝配,都要創建一個root configuration,即模塊化配置定義;并且在這個配置文件中開啟自動掃描機制:<context:component-scan>或者@ComponentScan。

2.6 總結

這一章中學習了Spring 裝配bean的三種方式:自動裝配、基于Java文件裝配和基于XML文件裝配。

由于自動裝配幾乎不需要手動定義bean,建議優先選擇自動裝配;如何必須使用顯式配置,則優先選擇基于Java文件裝配這種方式,因為相比于XML文件,Java文件具備更多的能力、類型安全等特點;但是也有一種情況必須使用XML配置文件,即你需要使用某個名字空間(name space),該名字空間只在XML文件中可以使用。

參考資料

  1. http://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html

本號專注于后端技術、JVM問題排查和優化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發者的工作和成長經驗,期待你能在這里有所收獲。


javaadu
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,480評論 2 379

推薦閱讀更多精彩內容