Spring 学习笔记之处理自动装配的歧义性

之前的文章中已经看到了 Spring 的自动装配有很大的用处,它可以帮助我们快速的装配 bean,但是这里存在一个问题,在之前的装配中,仅有一个 bean 匹配所需的结果时,自动装配才是有效的。如果不仅只有一个 bean 能够匹配结果的话,这就会导致 Spring 不知道该装配哪个 bean 从而导致装配失败,例如下面这个例子,我们定义了一个 Dessert 接口,并且有三个类实现了这个接口,分别为 Cake、Cookies 和 IceCream:

1
2
3
4
5
6
7
8
9
@Component
public class Cookies implements Dessert {
}
@Component
public class Cake implements Dessert {
}
@Component
public class IceCream implements Dessert {
}

这三个类均使用了 @Component 注解,在组件扫描的时候,能够发现他们并将其创建为 Spring 上下文中的 bean。下面是测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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:

1
2
3
4
@Component
@Primary
public class IceCream implements Dessert {
}

当然了,你也可以在显式声明 bean 的时候将其设置为首选 bean,比如:

1
2
3
4
5
@Bean
@Primary
public Dessert IceCream(){
return new IceCream();
}

当然,如果你喜欢使用 XML 来配置 Bean,那么其方法如下:

1
2
<bean id="iceCream" class="cn.javacodes.spring.beans.IceCream"
primary="true" />

使用哪种方式告诉 Spring 首选 bean 的效果都是一样的,不过,如果你标示了两个或更多的首选 bean,那么它就无法工作了,因为这又会带来歧义性的问题。

当然我们可以使用另一种更为强大的机制(限定符)来解决这个问题。

二、使用限定符

(一)@Qualifier 注解

使用 @Primary 无法将可选方案的范围限定到唯一一个无歧义的选项,它只能标示一个优先的可选选项。当首选 bean 的数量超过 1 个时,我们并没有其它的办法将其限定到唯一的选项上。

Spring 提供的限定符可以解决这个问题,@Qualifier 注解是使用限定符的主要方式。它可以与 @Autowired 或 @Inject 协同使用。例如,我们想确保 IceCream 注入到 setDessert ()之中:

1
2
3
4
5
@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 作为限定符。例如:

1
2
3
4
@Component
@Qualifier("cold")
public class IceCream implements Dessert {
}

这样就解决了之前耦合类名的问题,然后就可以在需要的地方使用这个限定符了,例如:

1
2
3
4
5
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}

当然,@Qualifier 注解也可以与显式装配 Bean 的 @Bean 注解组合使用,再次不做赘述。

(三)使用自定义限定符的注解

上面的例子中使用了 “cold” 作为 IceCream 的限定符,在这里 “cold” 更像是一种特性来描述这个 bean,当然,面向特性的限定符比 bean ID 更好一些,但是如果多个 bean 都具有相同的特性怎么办?

比如我们新加入一个类:

1
2
3
4
@Component
@Qualifier("cold")
public class Popsicle implements Dessert {
}

现在我们有了两个带有 “cold” 的限定符,自动装配的时候我们再次遇到了歧义性的问题,需要更多的限定符来将其可选范围缩小,现在我们可能想到的解决办法可能是类似像下面这种方式,使用多个 @Qualifier 注解:

1
2
3
4
5
@Component
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert {
}

但是这种方式是不行的,Java 语言不允许在同一个条目上重复出现相同类型的注解(Java 8 允许出现重复的注解,但是这个注解本身必须在定义的时候带有 @Repeatable,可是 Spring 的 @Qualifier 注解并没有在定义时加入 @Repeatable),为了解决这个问题,我们可以创建一个自定义的限定符注解,它本身需要使用 @Qualifier 注解来标注,例如:

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

同样你可以在创建一个 Creamy 注解来替代 @Qualifier (“creamy”):

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

同样的原理,你还可以创建类似 @Soft、@Crispy 等等其它注解。通过在定义注解的时候添加 @Qualifier,这些注解就具有了 Qualifier 的特性,他们本身世界上就是一个限定符。现在我们重新编辑一下 IceCream:

1
2
3
4
5
@Component
@Cold
@Creamy
public class IceCream implements Dessert {
}

类似的,Popsicle 类可以添加 @Cold、@Fruity 注解:

1
2
3
4
5
@Component
@Fruity
@Cold
public class Popsicle implements Dessert {
}

最终,在注入点,我们使用必要的限定符注解进行任意组合即可:

1
2
3
4
5
6
@Autowired
@Cold
@Creamy
public void setDessert(Dessert dessert){
this.dessert = dessert;
}

这样我们就可以随心所欲的使用自定义限定符注解来缩小匹配范围啦!当然,还是希望 Spring 可以尽快在新的版本中将 @Qualifier 注解中加入 @Repeatable 注解,这样就不用这么麻烦了(我估计要很久,因为 Spring 还需要保证在相对旧的 Java 版本上做兼容,Java 8 的这一特性估计不会这么快被支持的)!