Spring實(shí)戰(zhàn)2:裝配bean—依賴注入的本質(zhì)

主要內(nèi)容

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

任何一個成功的應(yīng)用都是由多個為了實(shí)現(xiàn)某個業(yè)務(wù)目標(biāo)而相互協(xié)作的組件構(gòu)成的,這些組件必須相互了解、能夠相互協(xié)作完成工作。例如,在一個在線購物系統(tǒng)中,訂單管理組件需要與產(chǎn)品管理組件以及信用卡認(rèn)證組件協(xié)作;這些組件還需要跟數(shù)據(jù)庫組件協(xié)作從而進(jìn)行數(shù)據(jù)庫讀寫操作。

在Spring應(yīng)用中,對象無需自己負(fù)責(zé)查找或者創(chuàng)建與其關(guān)聯(lián)的其他對象,由容器負(fù)責(zé)將創(chuàng)建各個對象,并創(chuàng)建各個對象之間的依賴關(guān)系。例如,一個訂單管理組件需要使用信用卡認(rèn)證組件,它不需要自己創(chuàng)建信用卡認(rèn)證組件,只需要定義它需要使用信用卡認(rèn)證組件即可,容器會創(chuàng)建信用卡認(rèn)證組件然后將該組件的引用注入給訂單管理組件。

創(chuàng)建各個對象之間協(xié)作關(guān)系的行為通常被稱為裝配(wiring),這就是依賴注入(DI)的本質(zhì)。

2.1 Spring的配置方法概覽

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

  • 基于XML文件的顯式裝配
  • 基于Java文件的顯式裝配
  • 隱式bean發(fā)現(xiàn)機(jī)制和自動裝配

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

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

2.2 自動裝配bean

Spring通過兩個特性實(shí)現(xiàn)自動裝配:

  • Component scanning——Spring自動掃描和創(chuàng)建應(yīng)用上下文中的beans;
  • Autowiring——Spring自動建立bean之間的依賴關(guān)系;

這里用一個例子來說明:假設(shè)你需要實(shí)現(xiàn)一個音響系統(tǒng),該系統(tǒng)中包含CDPlayer和CompactDisc兩個組件,Spring將自動發(fā)現(xiàn)這兩個bean,并將CompactDisc的引用注入到CDPlayer中。

2.2.1 創(chuàng)建可發(fā)現(xiàn)的beans

首先創(chuàng)建CD的概念——CompactDisc接口,如下所示:

package com.spring.sample.soundsystem;

public interface CompactDisc {
    void play();
}

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

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需要創(chuàng)建SgtPeppers bean。除此之外,還需要啟動自動掃描機(jī)制,有兩種方法:基于XML配置文件;基于Java配置文件,代碼如下(二選一):

  • 創(chuàng)建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>標(biāo)簽啟動Component掃描功能,并可設(shè)置base-package屬性。

  • 創(chuàng)建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掃描,并且可以設(shè)置basePackages屬性——Spring將會設(shè)置該目錄以及子目錄下所有被@Component注解修飾的類。

  • 自動配置的另一個關(guān)鍵注解是@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);
    }
}

運(yùn)行測試,測試通過,說明@Autowired注解起作用了:自動將掃描機(jī)制創(chuàng)建的CompactDisc類型的bean注入到SoundSystemTest這個bean中。

2.2.2 給被掃描的bean命名

在Spring上下文中,每個bean都有自己的ID。在上一個小節(jié)的例子中并沒有提到這一點(diǎn),但Spring在掃描到SgtPeppers這個組件并創(chuàng)建對應(yīng)的bean時,默認(rèn)給它設(shè)置的ID為sgtPeppers——是的,這個ID就是將類名稱的首字母小寫。

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

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

2.2.3 設(shè)置需要掃描的目標(biāo)basepackage

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

可以看出,basePackages是復(fù)數(shù),意味著你可以設(shè)置多個目標(biāo)目錄,例如:

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

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

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

通過如上設(shè)置,會將CDPlayer和DVDPlayer各自所在的目錄作為掃描機(jī)制的目標(biāo)根目錄。

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

2.2.4 自動裝配bean

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

例如,考慮下面代碼中的CDPlayer類,它的構(gòu)造函數(shù)被@Autowired修飾,表明當(dāng)Spring創(chuàng)建CDPlayer的bean時,會給這個構(gòu)造函數(shù)傳入一個CompactDisc的bean對應(yīng)的引用。

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();
    }
}

還有別的實(shí)現(xiàn)方法,例如將@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;

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

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

2.2.5 驗(yàn)證自動配置

通過下列代碼,可以驗(yàn)證:CompactDisc的bean已經(jīng)注入到CDPlayer的bean中,同時在測試用例中是將CDPlayer的bean注入到當(dāng)前測試用例。

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配置文件不同于其他用于實(shí)現(xiàn)業(yè)務(wù)邏輯的Java代碼,因此不能將Java配置文件業(yè)務(wù)邏輯代碼混在一起。一般都會給Java配置文件新建一個單獨(dú)的package。

2.3.1 創(chuàng)建配置類

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

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

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

2.3.2 定義bean

通過@Bean注解創(chuàng)建一個Spring bean,該bean的默認(rèn)ID和函數(shù)的方法名相同,即sgtPeppers。例如:

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

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

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

可以利用Java語言的表達(dá)能力,實(shí)現(xiàn)類似工廠模式的代碼如下:

@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的生成函數(shù)傳入到構(gòu)造函數(shù)或者set函數(shù)中,例如:

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

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

假如有下列代碼:

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

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

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

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

當(dāng)然,還有一種更清楚的寫法:

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

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

這種情況下,cdPlayeranotherCDPlayer這倆bean持有相同的SgtPeppers實(shí)例,該實(shí)例的ID為lonelyHeartsClub。這種方法最值得使用,因?yàn)樗灰驝ompactDisc bean在同一個配置文件中定義——只要在應(yīng)用上下文容器中即可(不管是基于自動掃描發(fā)現(xiàn)還是基于XML配置文件定義)。

2.4 基于XML配置文件裝配bean

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

2.5 混合使用多種配置方法

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

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

2.5.1 在JavaConfig中解析XML配置

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

2.5.2 在XML配置文件中應(yīng)用JavaConfig

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

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

2.6 總結(jié)

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

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

參考資料

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

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


javaadu
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容