Spring中bean的高級裝配:Profile、條件化bean、自動裝配的歧義性以及bean的作用域

一、環境與profile

在軟件開發時,有一個很大的挑戰就是將應用程序從一種環境遷移到另一種環境中。在開發階段中,某些環境相關做法可能並不適合遷移到生產環境中,甚至即便遷移也無法工作。數據庫配置,加密算法已經與外部系統的集成時跨環境部署時會發生變化的幾個典型例子。
在數據庫配置方面,在開發環境中我們可能會使用嵌入式數據庫,並且能夠加載測試數據;在生產環境中,可能更希望使用JNDI從容器中獲取一個DataSource;而在QA環境中,我們可能更希望配置爲Commons DBCP連接池。而上述的三個方法相同的僅限於都生成類型爲DataSource的bean,僅此而已。

1、配置profile bean

在Spring3.1版本中引入了bean profile的功能。要使用profile,首先要將所有不同的bean定義整理到一個或多個profile中,在應用部署到每個環境時,要確保對應的profile處於激活狀態。

1.1、在Java配置中,配置profile bean

使用@Profile註解指定某個bean屬於哪一個profile,在3.1版本中@Profile註解只能用在類上面,而在3,2之後@Profile註解也可以用在方法上面,因此可以將不同的bean的聲明放在同一個配置類中。下面舉個簡單例子說明一下。

需要注意的是,儘管每個DataSource bean都被聲明在一個profile中,並且只有當規定的profile被激活時,相應的bean纔會被創建,但是可能會有其他的bean並沒有聲明在profile範圍內。沒有被指定的bean始終都會被創建,和激活那個profile無關。

1.2、激活profile

Spring在確定哪個profile處於激活狀態時,會檢查兩個獨立的屬性:spring.profiles.active和spring.profiles.default兩個屬性。active優先級高於default,當active沒有設置的話,纔會檢查default。有多個方式來設置這兩個屬性:

  • 作爲DispatcherServlet的初始化參數;
  • 作爲web應用的上下文參數;
  • 作爲JDNI條目;
  • 作爲環境變量;
  • 在集成測試環境類上使用ActiveProfiles註解設置

具體使用哪一種或幾種需要根據自己情況,在用到上述方式時,還得參考更加詳細的資料。不過在Spring4.0之後引入了一個更加通用的實現條件化bean的定義,在這個機制中,條件完全由開發者自己確定。有關Profile機制的代碼調試好久總是出問題,我放棄了!!!!以後果斷選擇條件化的bean!!!!

二、條件化的bean

假如你想要一個或多個bean在你想要的時候纔會被創建,一種方法是可以使用之前提到過的Profile機制來實現,這裏來說一種更加通用的機制即使用@Conditional註解,它可以用到類上,也可以用到帶有@Bean註解的方法上。如果給定的條件計算結果爲true,則創建該bean,否則不創建。 來看以下例子:

//三好學生
@Component
@Qualifier("top_ten")
public class SanHaoStudent implements Student { 
    private String name;
    private double score;

    public SanHaoStudent(String name, double score) {
        this.name = name;
        this.score = score;
    }

    @Override
    public void showInfo() {
        System.out.println("----------三好學生---------");
        System.out.println("姓名:"+name+" "+"分數:"+score);
    }
}
//優秀學生
@Component
@Qualifier("top_twenty")
public class YouXiuStudent implements Student{
    private String name;
    private double score;

    public YouXiuStudent(String name, double score) {
        this.name = name;
        this.score = score;
    }
    @Override
    public void showInfo() {
        System.out.println("----------優秀學生---------");
        System.out.println("姓名:"+name+" "+"分數:"+score);
    }
}

再來看一下配置類:

@Configuration
public class JavaConfig {
    @Bean
    @Qualifier("top_ten")
    @Conditional(ScoresTopTen.class)
    public Student sanHaoStudent(){
        return new SanHaoStudent("xiaoshuang",99);
    }

    @Bean
    @Qualifier("top_twenty")
    @Conditional(ScoresTopTwenty.class)
    public Student youXiuStudent(){
        return new YouXiuStudent(env.getProperty("yafneg",88);
    }
}

注意在每個帶有@Bean的方法上都有一個@Conditional註解,註解裏面的類即爲條件類,它們均實現了Condition接口,該接口中有一個matches方法,當該方法返回true時,對應得bean纔會被創建。Condition接口內容如下:

public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

再來看看兩個條件類的具體實現:

public class ScoresTopTen implements Condition {
    double score = 99;
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        return score >= 90;
    }
}
public class ScoresTopTwenty implements Condition {
    double score = 80;
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        return score>=90;
    }
}

兩個條件類裏面的具體內容不重要,僅僅是爲了說明問題。很顯然第一個返回的時true,第二個返回的爲false。如果代碼不出錯的情況下,此時應該會創建SanHaoStudent bean,而不會創建YouXiuStudent bean。

測試代碼如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = JavaConfig.class)
public class Test {
    @org.junit.Test
    public void test(){
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(JavaConfig.class);
        Student student1 = (SanHaoStudent)context.getBean("sanHaoStudent");
        student1.showInfo();
        try {
            Student student2 = (YouXiuStudent)context.getBean("youXiuStudent");
            student2.showInfo();
        }catch (Exception e){
            System.out.println("YouXiuStudent未創建");
        }
    }
}
運行結果如下:
----------三好學生---------
姓名:xiaoshuang 分數:99.0
YouXiuStudent未創建

從上述結果可知,的確只創建了SanHaoStudent bean,而沒有創建YouXiuStudent bean。因此Spring的這個機制可以根據需要而創建需要的bean,而對於那些目前來說沒用的bean可以先不創建,這樣可以大大提高代碼性能,減少對內存的消耗。

三、解決Spring中自動裝配的歧義性

在自動裝配中有時候一個接口可能有多個實現類,則在自動注入bean時,Spring不能明確知道要注入哪個bean,因此可能會發生異常。空口無憑,代碼爲例:

現有一個甜點(Dessert)接口,它有三個實現類,IceCream類,Cookie類和Cake類。

IceCream類

@Component
public class IceCream implements Dessert {
    @Override
    public void getFavorite() {
        System.out.println("最喜歡喫冰淇淋!");
    }
}

Cookie 類

@Component
public class Cookie implements Dessert {
    @Override
    public void getFavorite() {
        System.out.println("最喜歡喫曲奇!");
    }
}

Cake 類

@Component
public class Cake implements Dessert {
    @Override
    public void getFavorite() {
        System.out.println("最喜歡喫餅乾!");
    }
}

測試類:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = JavaConfig.class)
public class DessertTest {
    private Dessert dessert;

    @Autowired
    public void setDessert(Dessert dessert) {//使用setter方法注入
        this.dessert = dessert;
    }

    @Test
    public void favoriteDessert(){
        dessert.getFavorite();
    }
}

運行程序,默默地等待。扣個鼻屎,玩會手機,擡頭一看,我草咋那麼多紅色日誌。你沒有猜錯,出錯啦!異常如下:

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: 
No qualifying bean of type 'com.tyf.day5.Demo1.Dessert' available: 
expected single matching bean but found 3: cake,cookie,iceCream

上述異常的大致意思就是在注入的過程中並有找到Desert類型唯一的bean,而是發現了三個即cake,cookie,iceCream。而在Spring自動注入時必須指明要注入哪個bean,這個得必須由開發者來指定,Spring容器是分不清到底應該注入哪個。解決歧義性的方式有三種:使用@Primary註解、使用@Qualifier註解對bean重命名以及使用自定義限定符註解(推薦使用)。

1、@Primary註解

存在多個相同類型的bean時,可以使用@Primary註解來標明哪一個bean爲首選bean,以此來解決自動注入的歧義性。由於個人比較喜歡喫冰淇淋,因此我將IceCream作爲首選bean,做法如下:

@Component
@Primary
public class IceCream implements Dessert {
    @Override
    public void getFavorite() {
        System.out.println("最喜歡喫冰淇淋!");
    }
}

做法很簡單,即在某個類上直接添加@Primary註解即可,其他代碼均不變。但是這種做法的侷限性很大,如果有多個首選的bean,那麼使用該註解就很難辦到了。

2、@Qualifier註解

使用@Qualifier註解的做法就是爲每個bean都起一個別名,然後在注入某個bean時再來指定要注入的bean,這樣做的目的是爲了縮小可選bean的範圍,最終能夠達到一個bean滿足規定的限制條件。做法如下:

IceCream類:
@Component
@Qualifier("cold")
public class IceCream implements Dessert {
    @Override
    public void getFavorite() {
        System.out.println("最喜歡喫冰淇淋!");
    }
}

測試類:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = JavaConfig.class)
public class DessertTest {
    private Dessert dessert;

    @Autowired
    @Qualifier("cold")
    public void setDessert(Dessert dessert) {//使用setter方法注入
        this.dessert = dessert;
    }

    @Test
    public void favoriteDessert(){
        dessert.getFavorite();
    }
}
結果如下:
最喜歡喫冰淇淋!

**

注意:當使用自定義的@Qualifier的值時,最佳實踐是爲bean選擇特徵性或描述性的術語,而不是隨便使用名字。

**

3、使用自定義的限定符註解

由於Java中不允許在同一個條目上重複出現相同類型的註解,因此當出現具有相同特徵的bean時,很難使用@Qualifier來限定唯一的bean。雖然不能使用相同類型的註解,但是可以使用不同類型的啊,因此可以自定義限定符註解。

首先來看一下如何來自定義註解:

@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Treate {
}

上述代碼實現了一個名爲Treat的註解,由於它帶有@Qualifier註解,因此這就是一個限定符註解。以同樣的方式再定義@Cool註解和@CanCode註解。

假如現在有兩個人,他們都很帥,但是一個是程序員,一個是醫生。他們的特徵都是很帥,因此只用Cool這個特徵不能分清他們,還得在使用註解加以區分。詳細代碼如下:

醫生類:

@Component
@Cool
@Treate  //自定義限定符註解
public class Doctor implements People {
    @Override
    public void career() {
        System.out.println("我是一名醫生");
    }
}

程序員類:

@Component
@Cool
@CanCode
public class Programmer implements People{
    @Override
    public void career() {
        System.out.println("我是一個程序員");
    }
}

測試類:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = JavaConfig.class)
public class Test {
    private People people;
    @Autowired
    @Cool
    @Treate

    public void setPeople(People people) {
        this.people = people;
    }

    @org.junit.Test
    public void test(){
        people.career();
    }
}
運行結果:
我是一名醫生

比較推薦第三種方法,因爲無論有多少個重複特徵,只要不是同一個東西都能夠再加特徵來加以區分,因此今後儘量使用該方法來解決歧義性。

四、bean的作用域

Spring定義了下面四種bean的作用域:

(1)單例(Singleton):在整個應用中只創建bean的一個實例;
(2)原型(Prototyoe):在每次注入或者通過Spring應用上下文獲取的時候,都會創建一個新的bean實例;
(3)會話(Session):在web應用中,爲每個回話創建一個bean實例;
(4)請求(Request):在web應用中爲每個請求創建一個bean實例。

1、單例(Singleton)和原型(Prototype)

單例是Spring默認的作用域,但對於容易變的類型,單例並不合適。若要選擇其他作用域,就需要使用@Scope註解,它可以和@Component以及@Bean註解一起使用。

例如將NotePad bean的作用域設置爲原型,在組建掃描來發現和聲明bean時,可以直接和@Component一起使用。

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class NotePad{
    //省略
}

如上所示即爲將NotePad的作用域設置爲原型,同樣還可以在Java配置文件中和@Bean一起使用或者在XML文件中配置,不再贅述。

2、會話(Session)和請求(Request)

會話和請求作用域一般都用於web應用中,電商平臺中的購物車是一個典型的會話作用域的bean。如果購物車是單例的,那麼在整個應用中所有用戶都使用一個bean;如果購物車是原型的,則在某處向購物車添加商品後,在應用的另一處就可能不可用了,因爲該bean時原型的。購物車和用戶有很強的關聯性,每個用戶一個購物車,因此會話作用域的購物車更合適。設置某個bean爲Session作用域的做法如下:

@Component
@Scope(value = WebApplicationContext.SCOPE_PROTOTYPE,proxyMode = ScopedProxyMode.INTERFACE)
public interface ShopCar{
    //省略
}

上述設置會讓Spring容器創建多個ShopCar bean的實例,但是對於一個會話來說它只會創建一個實例,在當前會話相關的操作中,這個bean相當於還是單例的。

需要注意的是proxyMode 屬性設置爲ScopedProxyMode.INTERFACE,這個屬性解決了將會話作用域的bean注入到單例bean中引發的問題。先來看一個proxyMode 的應用場景。

@Component
public class StoreService{
    private ShopCart shopCart;
    @Autorwired
    public void setShopCart(ShopCart shopCart){
        this.ShopCart = shopCart;
    }
}

因爲StoreService是一個單例的bean,會在Spring應用上下文加載的時候創建。當它創建的時候會試圖將ShopCart bean注入到setShopCart方法中,但是由於ShopCart 的作用域是會話的,此時並不存在,直到某個用戶進入系統創建會話後,纔會出現ShopCart實例。

另外系統中將會有多個ShopCart實例:每個用戶一個。我們並不想讓Spring注入某個固定的ShopCart實例到StoreService中。我們希望的是當StopService處理購物車功能時,它所使用的ShopCart實例恰好是當前會話對應的那一個。

Spring並不會將實際的ShopCart bean注入到StoreService中,Spring會注入一個到ShopCart bean的代理中。這個代理會暴露於ShopCart相同的方法,所以StoreService會認爲它就是一個購物車。但是當StoreService調用ShopCart的方法時,代理會對其進行懶解析並將調用委託給會話作用域內真正的ShopCart bean。

ScopedProxyMode.INTERFACE表明代理要實現這個代理要實現ShopCart接口,並將調用委託給實現bean。如果ShopCart是一個類的話,Spring沒有辦法創建基於接口的代理,此時必須使用CGLib來生成基於類的代理,做法即爲將proxyMode設置爲ScopedProxyMode.TARGET_CLASS。

**

注:請求作用域的bean會面臨相同的裝配問題

**

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