Spring依賴注入淺析

什麼是依賴注入?


我們都知道Spring的兩大特性,以來注入(DI)和麪向切面編程(AOP),那麼什麼是依賴注入呢?我們舉個例子說明一下。
假設要寫一個簡單的音樂播放器,我們通常會這麼寫:
首先創建一個CDPlayer類,如下:

public class CDPlayer {
    private CD cd;

    public CDPlayer() {
        this.cd = new CD();
    }

    public void play() {
    this.cd.play();
    }
}

然後再創建一個表示唱片的CD類,如下:

public class CD {
    private String artist;
    private String cdName;
    private List<Song> songs;

    public CD() {
        this.artist = "Bruno Mars";
        this.cdName = "Uptown Funk";
        //this.songs = ... 
    }

    public CD(String artist, String cdName, List<Song> songs){
        this.artist = artist;
        this.cdName = cdName;
        this.songs = songs;
    }

    public void play(){
        //play songs...
    }
}

class Song在這裏省去

上面的例子有什麼問題呢,它當然可以工作,但在播放音樂時,你需要在CDPlayer類中new一個CD對象,這樣兩個class就緊密的耦合在一起。並且當你想要播放其它唱片時,就需要修改java代碼。

有了依賴注入,我們就不需要這樣了,我們可以給class CDPlayer增加一個方法,讓我們可以設置要播放的唱片:

public void SetCd(CD cd) {
    this.cd = cd;
}

有了這個方法,我們就不需要手動的在CDPlayer中new一個CD實例了。

當然,Spring提供地依賴注入不止這麼簡單,下面就來仔細的瞭解下Spring的依賴注入(DI)功能。

Spring DI

有了Spring的依賴注入,依賴類不再需要手動實例化,而是由Spring容器幫我們實例化並注入到需要的對象中,依賴類的生命週期也無需程序員關心,Spring容器會照顧它的一生。
依賴注入的幾種方式:
- Setter方法注入
- Constructor 方法注入

Setter方法注入

Setter方法注入是最簡單的一種注入方法,以上面的CDPlayer爲例,它需要一個CD class的實例,就在class CDPlayer中定義一個CD類的實例,然後設置它的Setter方法:

public class CDPlayer {
    private CD cd;
    public void setCd(CD cd) {
        this.cd = cd;
    }
}

然後編寫Spring的xml配置文件,指定依賴注入的配置
cdplayer.xml

<!-- 創建一個CD bean-->
<bean class="demo.CD" id="cd"/>
<!--創建CDplayer bean 在property中指定ref參數爲CD bean 的id-->
<bean class="demo.CDPlayer" id="player">
    <property name="cd" ref="cd"/>
</bean>

有了上面的代碼,Spring容器就可以創建兩個Bean實例並自動將class CD的實例注入到class CDplayer的實例中。

如何執行程序呢?需要以下代碼:
MainApp.java

public class MainApp {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("cdplayer.xml");
        CDPlayer player = (CDPlayer) context.getBean("player");
        player.play();
    }
}

xml文件中定義的bean的id屬性是每一個bean的唯一標識,ApplicationContext實例通過這個id屬性就可以獲取Spring容器創建的Bean的實例。
需要注意的是,當使用以上的xml配置時,class CD 必須有一個無參的默認構造函數,Spring容器需要這個構造函數創建cd bean。
class CD的實現可以爲:

public class CD {
    private String artist;
    private String name;

    public CD(String artist, String name){
        this.artist = artist;
        this.name = name;
    }

    public CD() {
        this.artist = "BM";
        this.name = "UF";
    }

    public void play() {
        System.out.println("Play songs in " + name + " by " + artist);
    }
}

Constructor方法注入

Constructor方法注入顧名思義就是通過構造函數注入依賴像,還是上面的例子,將CDPlayer修改爲:

public class CDPlayer {
    private CD cd;
    public CDPlayer(CD cd) {
        this.cd = cd;
    }
}

然後修改xml配置文件爲:

<!-- 創建一個CD bean-->
<bean class="demo.CD" id="cd"/>
<!--創建CDplayer bean 在property中指定ref參數爲CD bean 的id-->
<bean class="demo.CDPlayer" id="player">
    <constructor-arg ref="cd"/>
</bean>

上面的配置文件只是將player Bean的property屬性改成了<constructor-arg>屬性,就完成了Constructor注入方式。
關於構造函數注入必須指出的是,在上面的例子中,我們在<constructor-arg>中並沒有指定要注入的參數是哪一個,但因爲CDPlayer的構造函數只有一個參數,所以Spring默認是傳遞給其中的cd屬性的。那麼,當構造函數不止一個參數,尤其當多個參數具有相同類型時,怎麼確定要傳遞給哪個參數呢?
我們通過class CD來說明,它有兩個屬性,CD的名字和歌手名字,我們把無參的構造函數去掉,改成下面的形式:

public class CD {
    private String artist;
    private String name;

    public CD(String artist, String name){
        this.artist = artist;
        this.name = name;
    }

    public void play() {
        System.out.println("Play songs in " + name + " by " + artist);
    }
}

class CD現在有一個構造函數,都是String類型,我們通過xml來配置它具體的值,如下:

 <bean id="cd" class="demo.CD">
        <constructor-arg index="0" value="BM"/>
        <constructor-arg index="1" value="UF"/>
 </bean>

可以看到我們使用了<constructor-arg>標籤的index屬性來指明是哪一個參數。通過上面的配置,我們把“BM”字符串傳遞給了CD 的artist屬性,把“UF”字符串傳遞給了它的name屬性。
通過上面的例子還可以看到ref 屬性指定一個bean,而value 屬性制定一個值,如一個字符串常量。這對 <property> 標籤同樣適用。

三種配置方式

上面我們使用了xml文件的方式配置bean之間的依賴注入關係,但是,當我們的bean變得足夠多時,使用xml文件的配置方式會使xml文件過於臃腫。還好,Spring爲我們提供了其他的配置方式,分別是基於註解的自動發現,基於java代碼的配置。下面介紹其他兩種配置方式:
- 基於註解的自動發現和注入
- 基於java的配置

基於註解的自動發現和注入 自動裝配主要使用了`@Component`和`@Autowaired`註解,首先更改class CD 的代碼如下:

@Component
public class CD {
    private String artist;
    private String name;

    public CD(String artist, String name){
        this.artist = artist;
        this.name = name;
    }

    public CD() {
        this.artist = "bruno mars";
        this.name = "uptown funk";
    }


    public void play() {
        System.out.println("Play songs in " + name + " by " + artist);
    }
}
可以看到,在class CD的上面添加了`@Component`,這樣Spring就會把它當作一個bean創建它的實例。 再修改class CDPlayer的代碼如下:
@Component
public class CDPlayer {
    @Autowired
    private CD cd;


    public void play(){
        cd.play();
    }
CDPlayer 有一個CD的實例作爲屬性,這裏,我們既沒有給cd設置setter方法,也沒有給CDPlayer編寫構造函數,而是在cd的頭上添加了一個`@Autowired`註解。這樣,Spring就知道,cd的實例由添加了@Component的class CD提供,並將cd的實例自動注入到CDPlayer的實例中。 當然,使用這種方式,同樣要求CD class有一個無參的構造函數,Spring容器使用這個無參的構造函數初始化CD實例。 當然,只有這樣還不夠,還需要一個配置類,我們創建一個配置class,代碼如下:
@Configuration
@ComponentScan(basePackageClasses = CDPlayer.class)
public class ContextConfigurationDemo {
}
class ContextConfigurationDemo 中沒有任何代碼。只是在其上添加了`@Configuration` 表示這是一個Spring 配置類,`@ComponentScan` 通過其basePackageClasses屬性指明瞭Component組件所在的包。Spring會將這個包下的帶有`@Component` 註解的class初始化爲bean並自動注入到帶有`@Autowired` 的屬性上去。 爲了測試上面的自動發現和注入的例子,我們編寫了如下的測試代碼:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ContextConfigurationDemo.class)
public class DemoTest {
    @Autowired
    private CDPlayer player;

    @Test
    public void test() {
        player.play();
    }
}
`@RunWith` 表示這是一個測試類,`@ContextConfiguration` 註解指明瞭配置class,`@Test` 指明瞭test()是一個測試方法,執行它,將會看到正確的輸出。 仔細考慮一下,將會看到這種方式的侷限性,自動發現和注入方式在代碼完全由自己編寫時能夠很好的工作,但如果我們使用了其他人編寫的代碼,如導入的jar包,我們是不可能在這些代碼的class類頭上添加註解的,這時,基於java的配置方案就很有用了。

基於java的配置

基於java的配置和基於自動發現和注入的方式非常相似,兩者經常配合使用,彌補自動發現和注入的不足。
基於java的方式,只是將class頭上的 @Component 註解去掉,而在配置class中使用 @Bean 手動添加實例。接着上面的例子,我們去掉class CD 上的 @Component 註解,在class ContextConfigurationDemo中添加如下代碼:

@Configuration
@ComponentScan(basePackageClasses = CDPlayer.class)
public class ContextConfigurationDemo {
    @Bean
    public CD cd() {
        return new CD();
    }
}

在這裏,我們編寫了一個普通的方法,在其上添加一個 @Bean 註解。這個方法返回一個CD類型的對象,在這裏我們只是簡單的new了一個CD對象,你也可以任意修改它,設置artist和name都可以。

再執行測試,發現結果和使用自動發現是一樣的。

高級特性

profile

有時候,我們需要根據不同的場景決定一些bean是否應該被創建,比如,在你的project中,一些bean是爲測試創建的,當部署到生產環境中時,這些爲測試而定義的bean就不需要創建了。那麼,就需要一種機制,讓開發者決定一些bean什麼時候被創建,什麼時候不創建。Spring提供的方式就是 profiles

@Profile 註解完成這一功能,假設我你們有兩個場景分別是開發場景和測試場景,還是上面的播放器的例子,我們可以定義如下java配置類:

@Configuration
@ComponentScan(basePackageClasses = CDPlayer.class)
public class ContextConfigurationDemo {
    @Bean
    @Profile("dev")
    public CD cddev() {
        return new CD("dev-artist","dev-name");
    }

    @Bean
    @Profile("test")
    public CD cdtest() {
        return new CD("test-artist", "test-name");
    }
}

這裏我們定義了兩個bean,都返回class CD類型的實例,通過 @Profile 註解分別將其定義到”dev“和”test“兩個profile下。
爲了測試這個例子,我們將測試代碼更改爲:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ContextConfigurationDemo.class)
@ActiveProfiles("dev")
public class DemoTest {
    @Autowired
    private CDPlayer player;

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

可以看到,在測試class的頂部,我們通過 @ActiveProfiles 註解激活了”dev“profile,這樣,cddev()方法創建的bean就會被實例化並且注入到CDplayer的實例中,而cdtest()方法創建的 CD bean就不會被初始化。

@Profile 註解不僅可以作用於創建bean的方法,還可以作用於整個Configuration class,只需將 @Profile 註解移到類前頭即可。

除了在java 中配置,xml文件同樣可以配置profile,也同樣有兩種作用域,配置文件域和單個Bean域。配置的方法爲:
作用於整個配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans/spring-beans.xsd"
profile="dev">
</beans>

作用於單個bean:

<bean id="xxx" class="xxx.xxx.xxx" profile="dev">
</bean>

激活profile

我們定義了幾個profile,怎麼設置究竟哪個起作用呢?
Spring通過兩個參數配置激活的profile:
spring.profiles.active和spring.profiles.default
如果設置了spring.profiles.active,那麼就由它決定哪個profile被激活,此時,spring.profiles.default就不起作用;如果spring.profiles.active沒有設置,就由spring.profiles.default決定激活哪個profile;如果兩個都沒有被配置,就只有那些不屬於任何profile的bean創建。

定義這兩個參數的方式共有一下幾種:
1. 通過DispatcherServlet的參數配置
2. 作爲context 參數配置
3. 使用環境變量配置
4. 通過JVM系統參數配置

Conditional

Profile根據不同的環境決定一些bean是否被創建,而Conditional根據一些條件是否滿足決定一些bean是否被實例化。
在Spring中,這一功能由@Conditional 註解來完成,它作用在創建bean的方法上,和@Bean 註解一起使用。
假設你有一個MagicBean,你希望僅當Magic存在時它才被實例化,你可以這樣完成:

@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean() {
    return new MagicBean();
}

@Conditional 註解有一個MagicExistsCondition.class參數,這個參數決定了條件是否滿足,這個類必須實現Condition 接口,這個接口只有一個返回布爾值的matches方法,這個接口的定義爲:

public interface Condition {
boolean matches(ConditionContext ctxt, AnnotatedTypeMetadata metadata);
}

如果matchs方法返回true,該bean就會被創建,否則該bean就不會被創建。

自動注入的模糊性

上面我們舉得例子都比較簡單,一個需要注入的變量只有一個bean實例滿足要求,當有多個實例滿足要求時,Spring就無法決定注入 哪個實例,就會拋出異常。此時,就需要一種在bean實例衝突時的仲裁方案。
Spring有兩種解決方法。最簡單的是使用@Primary 註解,當有兩個滿足注入條件的bean實例時,帶有@Primary註解的Bean會被注入。這個註解既可以和@Component 註解一起使用在class級,也可以和@Bean 註解用在java配置文件中,當然也可以在xml中通過指定<bean>標籤的”primary=true“來設置。

@Primary 註解這種一刀切的方式不同,Spring還提供了一種方式處理注入的模糊性,使用@Qualifier 註解。這個註解的作用通過定義一個子集來縮小bean的範圍,直到只有一個bean滿足要求。與@Primary 註解不同,@Qualifier 註解作用於需要注入的地方,也就是和@Inject@AutoWired 註解一起使用,表明注入的bean需要滿足的條件。他的使用也有兩種方式,一種是直接指定要注入的Bean的class,例如:

@AutoWired
@Qualifier("iceCream")
private Dessert dessert;

上面的代碼指明dessert參數將由class IceCream的實例注入。
這種方式仍然有侷限,使用的較少,最多的還是使用如下的方式:
假設需要注入的仍然是一個Dessert實例,滿足的實例有如下幾個:

//IceCream 和Cookie 類都是 Dessert 接口的實現類
@Bean
@Qualifier("cold")
public Dessert iceCream() {
    return new IceCream();
}

@Bean
@Qualifier("hot")
public Dessert cookie() {
    return new Cookie();
}

然後指定注入的bean爲IceCream的實例可以這樣:

@AutoWired
@Qualifier("cold")
private Dessert dessert;

可以看到,我們在定義@Bean時也指定一個@Qualifier註解,這相當於將一個bean加入到了一個集合中,然後就可以在要注入的地方指定需要注入的集合是哪個。
一個集合中可能有不止一個bean實例,此時,我們可以在要注入的地方通過多個@Qualifier 註解限定最終只有一個bean符合要求。如:

@AutoWired
@Qualifier("cold")
@Qualifier("fruity")
private Dessert dessert;

Bean 的作用域

在使用依賴注入時,我們定義一次Bean的實例,可以將其注入到多個不同的地方,那麼,這些注入到不同地方的Bean的實例是隻有一個還是每次注入都新創建一個實例呢?在Spring中這是可以配置的。
默認情況下,Bean只實例化一次,注入到不同地方的Bean都是一個實例,在一個地方更改了這個實例,全局有效。這有時候不適合一些應用場景。

Spring一共提供了四種bean的作用範圍,分別是:
1. Singleton
2. prototype
3. Session
4. Request

後兩種都只在web 中有效。
Singleton表示這個bean是全局唯一的,無論注入多少次,它都只被初始化一次;
Prototype表示每次注入都創建一個新的實例。
Session表示在wen應用中,每個會話都單獨創建一個bean實例;
Request表示在web應用中,每次請求都創建一個bean的實例;

如何制定bean的作用域呢?在java配置或自動發現中,通過@Scope 註解來具體具體的作用域,這個註解和@Bean@Component 註解一起使用。如:

@Bean
@Scope(ConfigurableBeanFactory.PROTOTYPE)
public CD cd() {
    return new CD();
}

這樣,每個需要注入class CD實例的地方得到的都是不同的實例。

在xml文件中,通過<bean> 標籤的”scope“屬性指定。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章