1. 程式人生 > >Spring. 處理自動裝配的歧義性

Spring. 處理自動裝配的歧義性

在使用 @Autowired 註解的時候,如果有多個 bean 都能滿足依賴關係,Spring 將丟擲一個異常,這時,我們有必要處理自動裝配的歧義性,來唯一確定一個要使用的 bean。

1.標示首選的 bean

在宣告 bean 的時候,可通過將其中一個可選的 bean 設為首選( primary ) 來避免自動裝配的歧義性,當遇到歧義性的時候,Spring 將使用首選的 bean,而不是其他可選的 bean,如下方式將宣告一個首選的 bean:

@Component
@Primary
public class IceCream implements Dessert{
    ...
}

或者,使用如果你是用 Java 配置顯式地宣告,那麼 @Bean 方法應該如下所示:

@Bean
@Primary
public Dessert iceCream(){
    return new IceCream();
}

如果你是用 XML 配置的話,同樣可以實現這樣的功能:<bean>元素有一個 primary 屬性用來指定首選的 bean

<bean id="iceCream" class="com.dessertdater.IceCream"
      primary="true" />

不管你選用哪種方式,效果都是相同的,都是在告訴 Spring 在遇到歧義性的時候要選擇首選的 bean。

但是,如果你標示了兩個或更多的首選 bean,那麼它就無法正常工作了,這帶來了新的歧義性問題:就像 Spring 無法從多個可選的 bean 中做出選擇一樣,它也無法從多個首選 bean 中做出選擇。限定符是一種更加強大的機制。


2.限定自動裝配的 bean

@Qualifier 註解是使用限定符的主要方式,它可以與 @Autowired 和 @Inject 協同使用,在注入的時候指定想要注入進去的是哪個 bean。

@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
    this.dessert = dessert;
}

這是使用限定符的最簡單的例子。為 @Qualifier 註解所設定的引數就是想要注入的 bean 的 ID。

以上示例,Spring 會將 ID 為 iceCream 的 bean 注入到 方法中。

實際上,還有一點需要補充。更準確的講,@Qualifier("iceCream") 所引用的 bean 要具有 String 型別的 “iceCream” 作為限定符。如果沒有指定其他的限定符的話,所有的 bean 都會給定一個預設的限定符,這個限定符與 bean 的 ID 相同。因此,框架會將具有 “iceCream” 限定符的 bean 注入到 setDessert() 方法中。這恰巧就是 ID 為 iceCream 的 bean ,它是 IceCream 類在元件掃描的時候建立的。

基於預設的 bean ID 作為限定符是非常簡單的,但這有時候會引入一些問題。

如果你重構了 IceCream 類,將其命名為 Gelato 的話,bean 的 ID 和預設的限定符會變成 gelato,這就無法匹配 setDessert() 方法中的限定符。自動裝配會失敗。

這裡的問題在於 setDessert() 方法上所指定的限定符與要注入的 bean 的名稱是緊耦合的。對類名稱的任意改動都會導致限定符失效。

建立自定義的限定符

我們可以為 bean 設定自己的限定符,而不是依賴於將 bean ID 作為限定符。所需要做的就是在 bean 宣告上新增 @Qualifier 註解:

@Component
@Qualifier("cold")
public class IceCream implements Dessert{
    
}

以上,“cold” 限定符和被它修飾的類(這裡是 IceCream)做繫結,因為它沒有耦合類名,因此你可以隨意重構 IceCream 的類名,而不必擔心會破壞自動裝配。在注入的地方,只要引用 ”cold“ 限定符就可以了:

@Bean
@Qualifier("cold")
public Dessert iceCream(){
    return new IceCream();
}

問題到這裡看似已經完美解決,但是真的沒有問題了嗎?

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

試想,如果兩個 bean 都具有相同的特性,而你為兩個 bean 都配置了相同的限定符,例如:

@Component
@Qualifier("cold")
public class Popsicle implements Dessert{

}
@Component
@Qualifier("cold")
public class IceCream implements Dessert{
    
}

這時,再次出現了歧義性問題。

可能想到的解決辦法是在注入點和 bean 定義的地方同時再新增另外一個 @Qualifier 註解,IceCream 類大概就會如下所示:

@Component
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert{
    
}

然而,Java 不允許在同一個條目上重複出現相同型別的多個註解(也不是完全不允許:Java 8 允許出現重複的註解,只要這個註解本身在定義的時候帶有 @Repeatable 註解就可以。不過,Spring 的 @Qualifier 註解並沒有在定義時新增 @Repeatable 註解),如果你試圖這樣做,編譯器會提示錯誤。

所以,在這裡,使用 @Qualifier 註解並沒有辦法將自動裝配的可選 bean 縮小範圍到僅有一個可選的 bean。

但是,我們可以建立自定義的限定符註解,當自定義的限定符註解被修飾到多個類上,注入 bean 仍然具有歧義性的時候,我們只需要再建立一個自定義註解,如此就能繞過 @Qualifier 註解不能重複出現的規則,卻又能達到如上例所示的效果。

首先我們需要建立註解,它本身要用 @Qualifier 註解來標註,這樣它便具有了 @Qualifier 註解的特性,它本身實際上就成為了限定符註解。

定義 @Cold 註解如下:

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

}

定義 @Creamy 註解如下:

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

}

現在,我們來看一下 IceCream ,併為其新增 @Cold 和 @Creamy 註解:

@Component
@Cold
@Creamy
public class IceCream implements Dessert{

}

由於是不同的註解,這種方式完全合法。

到此,已經完全解決了自動裝配 bean 所遇到的歧義性問題。