Spring学习笔记之处理自动装配的歧义性
自建博客地址:https://www.bytelife.net,欢迎访问! 本文为博客自动同步文章,为了更好的阅读体验,建议您移步至我的博客👇
本文作者: Jeffrey
本文链接: https://www.bytelife.net/articles/22338.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
之前的文章中已经看到了Spring的自动装配有很大的用处,它可以帮助我们快速的装配bean,但是这里存在一个问题,在之前的装配中,仅有一个bean匹配所需的结果时,自动装配才是有效的。如果不仅只有一个bean能够匹配结果的话,这就会导致Spring不知道该装配哪个bean从而导致装配失败,例如下面这个例子,我们定义了一个Dessert接口,并且有三个类实现了这个接口,分别为Cake、Cookies和IceCream:
@Component
public class Cookies implements Dessert {
}
@Component
public class Cake implements Dessert {
}
@Component
public class IceCream implements Dessert {
}
这三个类均使用了@Component注解,在组件扫描的时候,能够发现他们并将其创建为Spring上下文中的bean。下面是测试代码:
import cn.javacodes.spring.beans.Dessert;
import cn.javacodes.spring.configuration.SpringConfig;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.junit.Assert.assertNotNull;
/**
* Created by Eric on 2016/10/20.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class Test {
private Dessert dessert ;
@Autowired
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
@org.junit.Test
public void test(){
assertNotNull(dessert);
}
}
当Spring试图自动装配setDessert()中的Dessert参数时,它并没有唯一、无歧义的可选值。所以Spring会抛出一个异常:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'Test': Unsatisfied dependency expressed through method 'setDessert' parameter 0: No qualifying bean of type [cn.javacodes.spring.beans.Dessert] is defined: expected single matching bean but found 3: cake,cookies,iceCream; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [cn.javacodes.spring.beans.Dessert] is defined: expected single matching bean but found 3: cake,cookies,iceCream
为了解决这个问题,Spring提供了多种解决方案,标示首选的bean(primary)和使用限定符(qualifier)。
一、标示首选的bean
在声明bean的时候,我们可以通过将其中一个可选的bean设置为首选(primary) bean,这样就可以避免歧义性了,使用方式如下,例如我们想将IceCream作为首选bean:
@Component
@Primary
public class IceCream implements Dessert {
}
当然了,你也可以在显式声明bean的时候将其设置为首选bean,比如:
@Bean
@Primary
public Dessert IceCream(){
return new IceCream();
}
当然,如果你喜欢使用XML来配置Bean,那么其方法如下:
<bean id="iceCream" class="cn.javacodes.spring.beans.IceCream"
primary="true" />
使用哪种方式告诉Spring首选bean的效果都是一样的,不过,如果你标示了两个或更多的首选bean,那么它就无法工作了,因为这又会带来歧义性的问题。
当然我们可以使用另一种更为强大的机制(限定符)来解决这个问题。
二、使用限定符
(一)@Qualifier注解
使用@Primary无法将可选方案的范围限定到唯一一个无歧义的选项,它只能标示一个优先的可选选项。当首选bean的数量超过1个时,我们并没有其它的办法将其限定到唯一的选项上。
Spring提供的限定符可以解决这个问题,@Qualifier注解是使用限定符的主要方式。它可以与@Autowired或@Inject协同使用。例如,我们想确保IceCream注入到setDessert()之中:
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
这是使用限定符最简单的例子了。为@Qualifier注解所设置的参数就是想要注入的bean的ID。所有使用@Component注解声明的类都会创建为bean,并且bean的ID为首字母变为小写的类名,因此这个例子中使用iceCream作为参数指向组件扫描时所创建的IceCream bean。
实际上,更准确的讲,@Qualifier(“iceCream”)所引用的bean要具有String类型的“iceCream”作为限定符。如果没有制定其他的限定符,那么所有的bean都会有一个默认的限定符,它的值为bean的ID。因此框架会将具有“iceCream”限定符的bean注入到setDessert()方法中。这恰巧就是ID为iceCream的bean。
基于默认的限定符看起来是很简单的,不过这里面存在一个问题,如果日后我们进行重构的时候,如果更改了IceCream类的类名比如更改为Gelato的话,那么自动创建的bean的ID就会变为“gelato”,这就无法匹配我们之前所写的限定符了,导致自动装配失败。
所以在这里setDessert()方法上所指定的限定符与要注入的bean的名称是紧耦合的。对类名称的任意改动都会造成限定符失效。
(二)创建自定义的限定符
我们可以为bean设置自己的限定符,而不是依赖与将bean ID作为限定符。例如:
@Component
@Qualifier("cold")
public class IceCream implements Dessert {
}
这样就解决了之前耦合类名的问题,然后就可以在需要的地方使用这个限定符了,例如:
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
当然,@Qualifier注解也可以与显式装配Bean的@Bean注解组合使用,再次不做赘述。
(三)使用自定义限定符的注解
上面的例子中使用了“cold”作为IceCream的限定符,在这里“cold”更像是一种特性来描述这个bean,当然,面向特性的限定符比bean ID更好一些,但是如果多个bean都具有相同的特性怎么办?
比如我们新加入一个类:
@Component
@Qualifier("cold")
public class Popsicle implements Dessert {
}
现在我们有了两个带有“cold”的限定符,自动装配的时候我们再次遇到了歧义性的问题,需要更多的限定符来将其可选范围缩小,现在我们可能想到的解决办法可能是类似像下面这种方式,使用多个@Qualifier注解:
@Component
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert {
}
但是这种方式是不行的,Java语言不允许在同一个条目上重复出现相同类型的注解(Java 8允许出现重复的注解,但是这个注解本身必须在定义的时候带有@Repeatable,可是Spring的@Qualifier注解并没有在定义时加入@Repeatable),为了解决这个问题,我们可以创建一个自定义的限定符注解,它本身需要使用@Qualifier注解来标注,例如:
@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {
}
同样你可以在创建一个Creamy注解来替代@Qualifier("creamy"):
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy {
}
同样的原理,你还可以创建类似@Soft、@Crispy等等其它注解。通过在定义注解的时候添加@Qualifier,这些注解就具有了Qualifier的特性,他们本身世界上就是一个限定符。现在我们重新编辑一下IceCream:
@Component
@Cold
@Creamy
public class IceCream implements Dessert {
}
类似的,Popsicle类可以添加@Cold、@Fruity注解:
@Component
@Fruity
@Cold
public class Popsicle implements Dessert {
}
最终,在注入点,我们使用必要的限定符注解进行任意组合即可:
@Autowired
@Cold
@Creamy
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
这样我们就可以随心所欲的使用自定义限定符注解来缩小匹配范围啦!当然,还是希望Spring可以尽快在新的版本中将@Qualifier注解中加入@Repeatable注解,这样就不用这么麻烦了(我估计要很久,因为Spring还需要保证在相对旧的Java版本上做兼容,Java 8的这一特性估计不会这么快被支持的)!