处理自动装配的歧义性
在自动装配时,仅有一个bean装配所需的结果时,自动装配是最有效的。但是不仅有一个bean能够匹配的话,这种歧义性会阻碍Spring自动装配属性、构造器参数或方法参数。
为了阐述自动装配的歧义性,假设使用@Autowired
注解标注了setDessert()
方法:
@Autowired
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
在本例中,Dessert
是一个接口,并且有三个类实现了这个接口,分别为Cake、Cookies和IceCream:
@Component
public class Cake implements Dessert {……}
@Component
public class Cookies implements Dessert {……}
@Component
public class IceCream implements Dessert {……}
因为这三个类均使用了@Autowired
注解,在组件扫描时,能够发现它们并将其创建为Spring应用上下文里面的bean。然后,当Spring试图自动装配setDessert()
中的Dessert
采纳数时,发现它并没有唯一的、无歧义的可选值,Spring会抛出org.springframework.beans.factory.NoUniqueBeanDefinitionException
:
nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.mec.about_auto.Dessert' available: expected single matching bean but found 3: cake,cookies,iceCream
为了解决这个问题,可以选择bean中的某一个设为首选(primary)的bean,或者使用限定符(qualifier)来帮助Spring将可选的bean的范围缩小到只有一个。
标示首选的bean
在声明bean的时候通过将其中一个可选的bean设置为首选(primary)笨啊能够避免自动装配时的歧义性。通过@Primary
来表示首选。@Primary
能够与@Component
组合使用在组件扫描的bean上,也可以与@bean
组合使用在Java配置的bean声明中。
@Component
@Primary
public class IceCream implements Dessert {……}
如果使用XML配置bean的话,同样可以实现这样的功能。<bean>
元素有一个primary
属性用来指定首选的bean。但是如果你标示了两个或更多的首选bean,那么它就无法正常工作了。显然,当不知一个bean被设置成了首选bean,那实际上就没有首选bean了。
限定自动装配的bean
在标示首选的bean中所提到的@Primary
注解仅仅可也标示一个优先选择方案,如果当首选bean的数量超过一个时,我们没有其他的方法进一步缩小可选范围。使用Spring的限定符能够在所有可选的bean上进行进一步的缩小范围,最终达到只有一个bean满足所有的限定条件。@Qualifier
注解是使用限定符的主要方式。它与@Autowired
和@Inject
注解协同使用,在注入的时候指定想要注入的是哪一个bean。例如:
@Autowired
@Qualifier("iceCream")
private Dessert dessert;
@org.junit.Test
public void dessertShouldNotBeNull() {
assertNotNull(dessert);
}
在这里,为@Qualifier
注解所设置的参数就是想要注入的bean的ID。所有使用@Component
注解声明的类都会被创建为bean,并且bean的ID为首字母变为小写的类名。因此,@Qualifier("iceCream")
指向的组件扫描时创建的bean,并且这个bean是IceCream 的实例。
基于默认的bean ID作为限定符是非常简单的,但可能存在一些问题。如果重构了IceCream
类,将其重命名为Gelato,此时bean的ID和默认的限定名会变为gelato(ID与默认的限定符为类名称的全小写),这就无法匹配setDessert()
方法的限定符,就会失败。这是因为setDessert()
方法上的限定符与IceCream
类的bean ID是紧耦合的,对类名称做任意改动都会导致注入的失败。不过可以通过创建自定义的限定符来解决这个问题。
创建自定义的限定符
为bean设置自定义限定符,不依赖于bean ID作为限定符。这样就可以解决类的bean ID与类名称紧耦合的问题。需要做的是在bean的声明上添加@Qualifier
注解(它可以与@Component
注解组合使用)。
@Autowired
@Qualifier("cold")
private Dessert dessert;
当前情况下,cold限定符被分配给了IceCream
bean。因为它没有耦合类名,所以可以随意重构IceCream
类的类名,而不用担心破坏自动装配。只需在注入的地方,引用cold限定符即可。
@Component
@Qualifier("cold")
public class IceCream implements Dessert {……}
当使用Java配置显示定义bean时,@Qualifier
可以与@Bean
同时使用。
使用自定义的限定符注解
如果多个bean具有相同的特性,则使用特性限定符也会出现问题:同时又多个带有相同特性限定符的bean,使Spring不知道选取哪一个bean进行注入,再次出现了二义性问题。此时就需要使用更多的限定符来缩小范围,最终限定到只有一个bean。
可能想到的方法就是在注入点和bean定义的地方同时添加另外一个@Qualifier
系统注解。
@Component
@Qualifier("cold")
@Qualifier("ceramy")
public class IceCream implements Dessert {……}
虽然这样做理论上可以解决二义性的问题,但是Java不允许在同一个条目上重复出现相同的多个注解(虽然Java 8允许出现重复的注解,只要这个注解在定义的时候带有@Repeatable
注解就可以,但是Spring中的@Qualifier
注解并没有在定义的时候添加@Repeatable
注解。)。如果某一个类上出现了两个相同的注解,则编译器会提示错误。不过可以创建自定限定符注解来解决这一个问题。
这里需要做的就是创建一个注解,本身要使用@Qualifier
注解来标注,这样就不需要再使用@Qualifier("cold")
注解,而是改用自定义的@Cold
注解,该注解的定义如下所示:
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {}
在定义时,添加了@Qualifier
注解,它们就具有了@Qualifier
注解的特性,它们本身实际上就称为了限定符注解。在是哟昂了自定义注解@Cold
和@Creamy
后,IceCream类如下所示:
@Component
@Cold
@Creamy
public class IceCream implements Dessert {……}
通过自定义注解的限定符注解,可以同时使用多个限定符,不再会出现编译器的限制或错误。同时,现对于使用原始的@Qualifier
并借助String类型来指定限定符,自定义注解也更为类型安全。
通过本文简单介绍了Spring在自动装配时遇到歧义性时的处理办法,通过学习简单记录下来,希望对于读者有少许帮助。