1. 程式人生 > >Spring學習筆記之自動化裝配Bean

Spring學習筆記之自動化裝配Bean

在Spring中可以使用Java程式碼、XML和自動化裝配三種方式來裝配Bean。從便利性角度來說,最強大的還是Spring的自動化配置,如果Spring能夠進行自動化裝配的話,那何苦還要顯式的將這些Bean裝配在一起呢?

Spring從兩個角度來實現自動化裝配:

元件掃描:Spring會自動發現應用上下文中所建立的Bean;

自動裝配:Spring自動滿足bean之間的依賴。

為了闡述元件掃描和裝配,我們需要建立幾個Bean,它們代表了一個音響系統中的元件。

一、建立可被發現的bean

定義CD的一個介面:
package cn.javacodes.spring.beans.soundsystem;
public interface CompactDisc {
    void play();
}
CompactDisc介面定義了CD播放器對一盤CD所能進行的操作。它將CD播放器的任意實現與CD本身的耦合降低到了最小的程度。下面建立一個CompactDisc的實現:
package cn.javacodes.spring.beans.soundsystem;
import org.springframework.stereotype.Component;
@Component
public class Transfer implements CompactDisc {
    private String title = "transfer";
    private String artist = "周傳雄/小剛";
    public void play() {
        System.out.println("正在播放"+artist+"的專輯:" + title);
    }
}
這裡需要注意的是該類使用了@Component註解,表明該類會作為元件類,並告知Spring要為這個元件建立bean。但是在這之前,由於預設元件掃描是不啟用的。我們還需要顯式配置一下Spring,從而命令它去尋找帶有@Component註解的類,併為其建立bean。下面的這個類展現了完成這件事情的最簡介配置方式:
package cn.javacodes.spring.beans.soundsystem;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class CDPlayerConfig {
}

如果沒有其他配置的話,@ComponentScan預設會掃描與配置類相同的包。因為CDPlayerConfig類位於cn.javacodes.spring.beans.soundsystem包中,因此Spring將會掃描這個包以及這個包下的所有子包,查詢帶有@Component註解標示的類,並在Spring中自動為其建立一個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:configurator="http://www.springframework.org/schema/c"
       xmlns:avalon="http://www.springframework.org/schema/p"
       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="cn.javacodes.spring.beans.soundsystem"/>
</beans>


儘管我們可以使用XML的方案來啟用元件掃描,但在後面的討論中,更多的還是會使用基於Java的配置。下面我們建立一個簡單的JUnit測試,它會建立Spring上下文,並判斷CompactDisc是不是真的創建出來了。
package cn.javacodes.spring.beans.soundsystem;
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;
import static org.junit.Assert.assertNotNull;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = CDPlayerConfig.class)
public class CDPlayerTest {
    @Autowired
    private CompactDisc cd;
    @Test
    public void cdShouldNotBeNull(){
        assertNotNull(cd);
    }
}


該類使用了Spring的SpringJUnit4ClassRunner,以便在測試開始的時候自動建立Spring的上下文。註解@ContextConfiguration會告訴它需要在CDPlayerConfig中載入配置。因為CDPlayerConfig類中包含了@ComponentScan註解,因此最終的應用上下文中應該包含CompactDisc bean。為了證明這一點,在測試程式碼中有一個CompactDisc屬性,並且這個屬性帶有@Autowired註解,以便於將CompactDisc bean注入到測試程式碼中,有關與@Autowired註解的更多內容將在後面講述。最後,有一個簡單的測試方法斷言cd屬性不為null。如果它不為null的話,就意味著Spring能夠發現CompactDisc類,自動在Spring上下文中將其建立為bean並將其注入到了測試程式碼中。這個程式碼應該能夠通過測試,並以測試成功的顏色顯示。

二、為元件掃描的bean命名

Spring上下文中所有的bean都有一個id。在前面的例子中,即使我們並沒有明確的給定Transfer bean一個id,但Spring會根據類名為其給定一個id。具體來講,Spring會預設給定一個將類名首字母變為小寫的id,例如上例中將給定的id為transfer。如果想為這個bean給定不同的id,你需要做的就是將你所想要給定的id作為引數傳遞給@Component註解。例如:
package cn.javacodes.spring.beans.soundsystem;
import org.springframework.stereotype.Component;
@Component("transfer")
public class Transfer implements CompactDisc {
    private String title = "transfer";
    private String artist = "周傳雄/小剛";
    public void play() {
        System.out.println("正在播放"+artist+"的專輯:" + title);
    }
}


還有另外一種為bean命名的方式,使用Java依賴注入規範中提供的@Named註解來為bean設定id:
package cn.javacodes.spring.beans.soundsystem;
import javax.inject.Named;
@Named("transfer")
public class Transfer implements CompactDisc {
    private String title = "transfer";
    private String artist = "周傳雄/小剛";
    public void play() {
        System.out.println("正在播放"+artist+"的專輯:" + title);
    }
}


Spring支援將@Named作為@Component註解的替代方案。兩者之間有一些細微的差別,不過大多數場景種它們使可以相互替換的。但是推薦使用@Component而不是@Named,因為@Component註解看起來更加能夠知道它是幹什麼的。

三、設定元件掃描的基礎包

現在我們已經知道,預設情況下@ComponentScan註解會掃描當前配置類所在的包及其子包,但我們可能更希望將配置類與其它類放在不同的包中,那麼為了指定不同的基礎包,可以將指定的包名作為引數傳遞給@ComponentScan註解即可:
@Configuration
@ComponentScan("cn.javacodes.spring.beans.soundsystem")
public class CDPlayerConfig {
}


當然也可以更加清晰的指明其是基礎包,使用basePackages屬性:
@Configuration
@ComponentScan(basePackages = "cn.javacodes.spring.beans.soundsystem")
public class CDPlayerConfig {
}


這裡我們發現basePackages屬性是複數形式,我們猜測它是否可以指定多個基礎包呢?答案是正確的,如果想要指定多個包,那麼只需要將要掃描的包放到一個數組中即可:
@Configuration
@ComponentScan(basePackages = {"cn.javacodes.spring.beans.soundsystem", "cn.javacodes.spring.beans.video"})
public class CDPlayerConfig {
}


上面的方式中,包名以簡單的字串進行表示,當然這是可以的。但是如果我們日後對程式碼進行重構,很有可能就會出現問題,所以這種通過簡單的字串來配置基礎包的方式是不安全的。為了解決這個問題,我們可以將其指定為包中所包含的類或介面:注意:這裡不再使用basePackages屬性,取而代之的是basePackageClasses屬性。我們不再使用String型別的包名來指定包,而是為basePackageClasses屬性設定的陣列中包含了類。這些類所在的包會作為元件掃描的基礎包。當然,使用元件類直接給basePackageClasses屬性並不是很好的方式,我們可以考慮在包中建立一個用來進行掃描的空標記介面。通過標記介面的方式,你依然能夠保持對重構友好的介面引用,但是可以避免引用任何實際的應用程式程式碼。

四、通過為bean添加註解實現自動裝配

簡單來說,自動裝配就是讓Spring自動滿足bean依賴的一種方法,在滿足依賴的過程中,會在Spring應用上下文中尋找匹配某個bean需求的其它bean。為了宣告要進行自動裝配,我們可以考慮使用Spring的@Autowired註解。比如下面的CDPlayer類,它的構造器使用了@Autowired註解,表明當Spring建立CDPlayer bean的時候,會通過這個構造器來進行例項化並會傳入一個可以設定給CompactDisc型別的bean:
package cn.javacodes.spring.beans.soundsystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class CDPlayer {
    private CompactDisc cd;
    @Autowired
    public CDPlayer(CompactDisc cd) {
        this.cd = cd;
    }
    
    public void play(){
        cd.play();
    }
}


@Autowired屬性不僅可以用在構造器上,還可以用在屬性的Setter方法上。比如說,如果CDPlayer有一個setCompactDisc()方法,那麼可以採用下面的方式來進行自動裝配:
    @Autowired
    public void setCompactDisc(CompactDisc cd){
        this.cd = cd;
    }


在Spring完成初始化bean之後,它會盡可能的去滿足bean的依賴。實際上,Setter方法並沒有什麼特殊之處,@Autowired可以出現在任何方法上。假如有且只有一個bean匹配依賴需求的話,那麼這個bean將會被封裝起來。如果沒有匹配的bean,那麼在應用上下文建立的時候,Spring將會丟擲一個異常。為了避免異常,可以將@Autowired的required屬性設定為false,Spring會嘗試執行自動匹配,但是如果沒有匹配的bean的話,Spring會讓這個bean處於未裝配的狀態:
    @Autowired(required = false)
    public CDPlayer(CompactDisc cd) {
        this.cd = cd;
    }


但是,把required屬性設定為false的時候你需要注意,如果你的程式碼中沒有null檢查的話,這個處於未裝配狀態的屬性有可能會出現空指標異常(NullPointerException)。如果有多個bean都能滿足依賴關係的話,Spring會丟擲一個異常,表明沒有明確指定要選擇哪個bean進行裝配,有關於Spring自動化裝配的歧義性的問題,我會在後續的文章中進行說明。@Autowired是Spring特有的註解,如果你不希望在程式碼中到處使用Spring特有的註解的話,那麼可以考慮使用@Inject註解對其進行替換,例如:
package cn.javacodes.spring.beans.soundsystem;
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class CDPlayer {
    private CompactDisc cd;
    @Inject
    public CDPlayer(CompactDisc cd) {
        this.cd = cd;
    }
    public void play(){
        cd.play();
    }
}


@Inject註解來源於Java依賴注入規範,同@Named註解一樣,@Inject註解與@Autowired註解存在一些細微的差別,但大多數情況下它們可以進行相互替換。

五、驗證自動裝配

我們修改一下測試類CDPlayerTest,使其能夠藉助CDPlayer bean播放CD:
package cn.javacodes.spring.beans.soundsystem;
import cn.javacodes.spring.beans.MediaPlayer;
import cn.javacodes.spring.configuration.CDPlayerConfig;
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;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@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("正在播放周傳雄/小剛的專輯:transfern", log.getLog());
    }
}


該類中,除了注入CompactDisc,還將CDPlayer bean注入到了測試程式碼中(更為通用的MediaPlayer型別)。在play()方法中,我們可以呼叫CDPlayer的play()方法並斷言它的行為與你的預期是否一致。自動化裝配Bean還有更多的細節,我會在後續的文章中進行闡述。  

本文為博主獨立部落格(https://javacodes.cn)同步發表,轉載請註明出處。
檢視原文:https://javacodes.cn/327.htm