Spring入門(八):自動裝配的歧義性

  • 2019 年 10 月 3 日
  • 筆記

1. 什麼是自動裝配的歧義性?

在Spring中,裝配bean有以下3種方式:

  1. 自動裝配
  2. Java配置
  3. xml配置

在這3種方式中,自動裝配為我們帶來了很大的便利,大大的降低了我們需要手動裝配bean的程式碼量。

不過,自動裝配也不是萬能的,因為僅有一個bean匹配條件時,Spring才能實現自動裝配,如果出現不止1個bean匹配條件時,Spring就會不知道要裝配哪個bean,拋出org.springframework.beans.factory.NoUniqueBeanDefinitionException異常,這就是自動裝配的歧義性。

為了方便理解,我們舉個具體的例子。

首先,我們新建個介面Dessert,該介面僅有1個方法showName():

package chapter03.ambiguity;    public interface Dessert {      void showName();  }

然後定義3個該介面的實現類Cake,Cookies,IceCream:

package chapter03.ambiguity;    import org.springframework.stereotype.Component;    @Component  public class Cake implements Dessert {      @Override      public void showName() {          System.out.println("蛋糕");      }  }
package chapter03.ambiguity;    import org.springframework.stereotype.Component;    @Component  public class Cookies implements Dessert {      @Override      public void showName() {          System.out.println("餅乾");      }  }
package chapter03.ambiguity;    import org.springframework.stereotype.Component;    @Component  public class IceCream implements Dessert {      @Override      public void showName() {          System.out.println("冰激凌");      }  }

然後新建甜點店類DessertShop,該類的setDessert()方法需要裝配1個Dessert的實例bean:

package chapter03.ambiguity;    import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.stereotype.Component;    @Component  public class DessertShop {      private Dessert dessert;        public Dessert getDessert() {          return dessert;      }        @Autowired      public void setDessert(Dessert dessert) {          this.dessert = dessert;      }        public void showDessertName() {          this.dessert.showName();      }  }

不過現在符合裝配條件的有3個bean,它們的bean ID(默認情況下是類名首字母小寫)分別為cake,cookies,iceCream,Spring該自動裝配哪個呢?

帶著這個疑問,我們先新建配置類AmbiguityConfig:

package chapter03.ambiguity;    import org.springframework.context.annotation.ComponentScan;    @ComponentScan  public class AmbiguityConfig {  }

這個類的關鍵是添加了@ComponentScan註解,讓Spring自動掃描已經定義好的bean。

最後,新建類Main,在其main()方法中添加如下測試程式碼:

package chapter03.ambiguity;    import org.springframework.context.annotation.AnnotationConfigApplicationContext;    public class Main {      public static void main(String[] args) {          AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AmbiguityConfig.class);            DessertShop dessertShop = context.getBean(DessertShop.class);          dessertShop.showDessertName();            context.close();      }  }

運行程式碼,發現拋出org.springframework.beans.factory.NoUniqueBeanDefinitionException異常,如下所示:

那麼如何解決自動裝配的歧義性呢?Spring提供了以下2種方案:

  1. 標記首選的bean
  2. 使用限定符

2. 標記首選的bean

既然現在有3個匹配條件的bean,我們可以通過@Primary註解標記下哪個是首選的bean,這樣當Spring發現有不止1個匹配條件的bean時,就會選擇這個首選的bean。

比如3種甜點裡,我最喜歡吃餅乾,那麼我就把Cookies標記為首選的bean:

package chapter03.ambiguity;    import org.springframework.context.annotation.Primary;  import org.springframework.stereotype.Component;    @Component  @Primary  public class Cookies implements Dessert {      @Override      public void showName() {          System.out.println("餅乾");      }  }

再次運行測試程式碼,輸出結果如下所示:

餅乾

圓滿解決了歧義性的問題,不過有一天,有個同事不小心在IceCream上也添加了@Primary註解:

package chapter03.ambiguity;    import org.springframework.context.annotation.Primary;  import org.springframework.stereotype.Component;    @Component  @Primary  public class IceCream implements Dessert {      @Override      public void showName() {          System.out.println("冰激凌");      }  }

編譯都正常,因此都沒注意,但發布後運行時,卻拋出如下異常:

意思就是發現了不止1個首選的bean,因為此時Spring又不知道該選擇哪個了,也就是有了新的歧義性,所以甩鍋拋出了異常。

3. 使用限定符

3.1 基於bean ID的限定符

Spring還提供了另一個註解@Qualifier註解來解決自動裝配的歧義性,它可以與@Autowired或者@Inject一起使用,在注入的時候指定想要注入哪個bean。

比如,我們把IceCream注入到setDessert()的方法參數之中:

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

這裡傳遞的iceCream指的是IceCream類默認生成的bean ID。

再次運行測試程式碼,輸出結果如下所示:

冰激凌

我們可以發現,使用了@Qualifier註解後,我們之前標記的@Primary註解被忽略了,也就是說,@Qualifier註解的優先順序比@Primary註解的優先順序高。

使用默認的限定符雖然解決了問題,不過可能會引入一些問題。比如我在重構程式碼時,將IceCream類名修改成了Gelato:

package chapter03.ambiguity;    import org.springframework.context.annotation.Primary;  import org.springframework.stereotype.Component;    @Component  @Primary  public class Gelato implements Dessert {      @Override      public void showName() {          System.out.println("冰激凌");      }  }

此時運行程式碼,會發現拋出org.springframework.beans.factory.NoSuchBeanDefinitionException異常,如下所示:

這是因為IceCream重命名為Gelato之後,bean ID由iceCream變成了gelato,但我們注入地方的程式碼仍然使用的是iceCream這個bean ID,導致沒有找到匹配條件的bean。

鑒於使用默認的限定符的這種局限性,我們可以使用自定義的限定符來解決這個問題。

為不影響後面程式碼的測試結果,將Gelato類再改回IceCream

3.2 基於面向特性的限定符

為了避免因為修改類名而導致自動裝配失效的問題,我們可以在@Component或者@Bean註解聲明bean時添加上@Qualifier註解,如下所示:

package chapter03.ambiguity;    import org.springframework.beans.factory.annotation.Qualifier;  import org.springframework.stereotype.Component;    @Component  @Qualifier("cold")  public class IceCream implements Dessert {      @Override      public void showName() {          System.out.println("冰激凌");      }  }

然後在注入的地方,不再使用默認生成的bean ID,而是使用剛剛指定的cold限定符:

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

運行測試程式碼,輸入結果如下所示:

冰激凌

此時將IceCream類重命名為Gelato,程式碼可以正常運行,不會受影響。

然後有一天,某位開發又新建了類Popsicle,該類也使用了cold限定符:

package chapter03.ambiguity;    import org.springframework.beans.factory.annotation.Qualifier;  import org.springframework.stereotype.Component;    @Component  @Qualifier("cold")  public class Popsicle implements Dessert {      @Override      public void showName() {          System.out.println("棒冰");      }  }

此時又帶來了新的歧義性問題,因為Spring又不知道該如何選擇了,運行程式碼會拋出org.springframework.beans.factory.NoUniqueBeanDefinitionException異常,如下所示:

此時,我們就需要用到自定義的限定符了。

3.3 自定義的限定符註解

首先,我們新建以下3個註解:

package chapter03.ambiguity;    import org.springframework.beans.factory.annotation.Qualifier;    import java.lang.annotation.ElementType;  import java.lang.annotation.Retention;  import java.lang.annotation.RetentionPolicy;  import java.lang.annotation.Target;    @Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})  @Retention(RetentionPolicy.RUNTIME)  @Qualifier  public @interface Cold {  }
package chapter03.ambiguity;    import org.springframework.beans.factory.annotation.Qualifier;    import java.lang.annotation.ElementType;  import java.lang.annotation.Retention;  import java.lang.annotation.RetentionPolicy;  import java.lang.annotation.Target;    @Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})  @Retention(RetentionPolicy.RUNTIME)  @Qualifier  public @interface Creamy {  }
package chapter03.ambiguity;    import org.springframework.beans.factory.annotation.Qualifier;    import java.lang.annotation.ElementType;  import java.lang.annotation.Retention;  import java.lang.annotation.RetentionPolicy;  import java.lang.annotation.Target;    @Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})  @Retention(RetentionPolicy.RUNTIME)  @Qualifier  public @interface Fruity {  }

注意事項:這3個註解在定義時都添加了@Qualifier註解,因此它們具有了@Qualifier註解的特性

然後將IceCream類修改為:

package chapter03.ambiguity;    import org.springframework.stereotype.Component;    @Component  @Cold  @Creamy  public class IceCream implements Dessert {      @Override      public void showName() {          System.out.println("冰激凌");      }  }

將Popsicle類修改為:

package chapter03.ambiguity;    import org.springframework.stereotype.Component;    @Component  @Cold  @Fruity  public class Popsicle implements Dessert {      @Override      public void showName() {          System.out.println("棒冰");      }  }

最後,修改下注入地方的程式碼,使其只能匹配到1個滿足條件的bean,如下所示:

@Autowired  @Cold  @Creamy  public void setDessert(Dessert dessert) {      this.dessert = dessert;  }

運行測試程式碼,輸出結果如下所示:

冰激凌

由此,我們也可以發現,自定義註解與@Qualifier註解相比,有以下2個優點:

  1. 可以同時使用多個自定義註解,但@Qualifier註解只能使用1個
  2. 使用自定義註解比@Qualifier註解更為類型安全

4. 源碼及參考

源碼地址:https://github.com/zwwhnly/spring-action.git,歡迎下載。

Craig Walls 《Spring實戰(第4版)》

5. 最後

歡迎掃碼關注微信公眾號:「申城異鄉人」,定期分享Java技術乾貨,讓我們一起進步。