在第2章中,我们已经看到如何使用自动装配让Spring完全负责将bean引用注入到构造参数和属性中。自动装配能够提供很大的帮助,因为它会减少装配应用程序组件时所需要的显式配置的数量。
不过,仅有一个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 { ... }
因为这三个实现均使用了@Component注解,在组件扫描的时候,能够发现它们并将其创建为Spring应用上下文里面的bean。然后,当Spring试图自动装配setDessert()中的Dessert参数时,它并没有唯一、无歧义的可选值。在从多种甜点中做出选择时,尽管大多数人并不会有什么困难,但是Spring却无法做出选择。Spring此时别无他法,只好宣告失败并抛出异常。更精确地讲,Spring会抛出NoUniqueBeanDefinitionException:
nested exception is
org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type [com.desserteater.Dessert] is defined:
expected single matching bean but found 3: cake,cookies,iceCream
当然,使用吃甜点的样例来阐述自动装配在遇到歧义性时所面临的问题多少有些牵强。在实际中,自动装配歧义性的问题其实比你想象中的更为罕见。就算这种歧义性确实是个问题,但更常见的情况是给定的类型只有一个实现类,因此自动装配能够很好地运行。
但是,当确实发生歧义性的时候,Spring提供了多种可选方案来解决这样的问题。你可以将可选bean中的某一个设为首选(primary)的bean,或者使用限定符(qualifier)来帮助Spring将可选的bean的范围缩小到只有一个bean。
3.3.1 标示首选的bean
如果你像我一样,喜欢所有类型的甜点,如蛋糕、饼干、冰激凌……它们都很美味。但如果只能在其中选择一种甜点的话,那你最喜欢的是哪一种呢?
在声明bean的时候,通过将其中一个可选的bean设置为首选(primary)bean能够避免自动装配时的歧义性。当遇到歧义性的时候,Spring将会使用首选的bean,而不是其他可选的bean。实际上,你所声明就是“最喜欢”的bean。
假设冰激凌就是你最喜欢的甜点。在Spring中,可以通过@Primary来表达最喜欢的方案。@Primary能够与@Component组合用在组件扫描的bean上,也可以与@Bean组合用在Java配置的bean声明中。比如,下面的代码展现了如何将@Component注解的IceCream bean声明为首选的bean:
@Component
@Primary
public class IceCream implements Dessert { ... }
或者,如果你通过Java配置显式地声明IceCream,那么@Bean方法应该如下所示:
@Bean
@Primary
public Dessert iceCream() {
return new IceCream();
}
如果你使用XML配置bean的话,同样可以实现这样的功能。<bean>元素有一个primary属性用来指定首选的bean:
<bean id="iceCream"
class="com.desserteater.IceCream"
primary="true" />
不管你采用什么方式来标示首选bean,效果都是一样的,都是告诉Spring在遇到歧义性的时候要选择首选的bean。
但是,如果你标示了两个或更多的首选bean,那么它就无法正常工作了。比如,假设Cake类如下所示:
@Component
@Primary
public class Cake implements Dessert { ... }
现在,有两个首选的Dessert bean:Cake和IceCream。这带来了新的歧义性问题。就像Spring无法从多个可选的bean中做出选择一样,它也无法从多个首选的bean中做出选择。显然,如果不止一个bean被设置成了首选bean,那实际上也就是没有首选bean了。
就解决歧义性问题而言,限定符是一种更为强大的机制,下面就将对其进行介绍。
3.3.2 限定自动装配的bean
设置首选bean的局限性在于@Primary无法将可选方案的范围限定到唯一一个无歧义性的选项中。它只能标示一个优先的可选方案。当首选bean的数量超过一个时,我们并没有其他的方法进一步缩小可选范围。
与之相反,Spring的限定符能够在所有可选的bean上进行缩小范围的操作,最终能够达到只有一个bean满足所规定的限制条件。如果将所有的限定符都用上后依然存在歧义性,那么你可以继续使用更多的限定符来缩小选择范围。
@Qualifier注解是使用限定符的主要方式。它可以与@Autowired和@Inject协同使用,在注入的时候指定想要注入进去的是哪个bean。例如,我们想要确保要将IceCream注入到setDessert()之中:
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
这是使用限定符的最简单的例子。为@Qualifier注解所设置的参数就是想要注入的bean的ID。所有使用@Component注解声明的类都会创建为bean,并且bean的ID为首字母变为小写的类名。因此,@Qualifier("iceCream")指向的是组件扫描时所创建的bean,并且这个bean是IceCream类的实例。
实际上,还有一点需要补充一下。更准确地讲,@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组合使用,如下所示:
@Component
@Qualifier("cold")
public class IceCream implements Dessert { ... }
在这种情况下,cold限定符分配给了IceCreambean。因为它没有耦合类名,因此你可以随意重构IceCream的类名,而不必担心会破坏自动装配。在注入的地方,只要引用cold限定符就可以了:
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
值得一提的是,当通过Java配置显式定义bean的时候,@Qualifier也可以与@Bean注解一起使用:
@Bean
@Qualifier("cold")
public Dessert iceCream() {
return new IceCream();
}
当使用自定义的@Qualifier值时,最佳实践是为bean选择特征性或描述性的术语,而不是使用随意的名字。在本例中,我将IceCream bean描述为“cold”bean。在注入的时候,可以将这个需求理解为“给我一个凉的甜点”,这其实就是描述的IceCream。类似地,我可以将Cake描述为“soft”,将Cookie描述为“crispy”。
使用自定义的限定符注解
面向特性的限定符要比基于bean ID的限定符更好一些。但是,如果多个bean都具备相同特性的话,这种做法也会出现问题。例如,如果引入了这个新的Dessert bean,会发生什么情况呢:
@Component
@Qualifier("cold")
public class Popsicle implements Dessert { ... }
不会吧?!现在我们有了两个带有“cold”限定符的甜点。在自动装配Dessert bean的时候,我们再次遇到了歧义性的问题,需要使用更多的限定符来将可选范围限定到只有一个bean。
可能想到的解决方案就是在注入点和bean定义的地方同时再添加另外一个@Qualifier注解。IceCream类大致就会如下所示:
@Component
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert { ... }
Popsicle类同样也可能再添加另外一个@Qualifier注解:
@Component
@Qualifier("cold")
@Qualifier("fruity")
public class Popsicle implements Dessert { ... }
在注入点中,我们可能会使用这样的方式来将范围缩小到IceCream:
@Autowired
@Qualifier("cold")
@Qualifier("creamy")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
这里只有一个小问题:Java不允许在同一个条目上重复出现相同类型的多个注解。如果你试图这样做的话,编译器会提示错误。在这里,使用@Qualifier注解并没有办法(至少没有直接的办法)将自动装配的可选bean缩小范围至仅有一个可选的bean。
但是,我们可以创建自定义的限定符注解,借助这样的注解来表达bean所希望限定的特性。这里所需要做的就是创建一个注解,它本身要使用@Qualifier注解来标注。这样我们将不再使用@Qualifier("cold"),而是使用自定义的@Cold注解,该注解的定义如下所示:
@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 { }
当你不想用@Qualifier注解的时候,可以类似地创建@Soft、@Crispy和@Fruity。通过在定义时添加@Qualifier注解,它们就具有了@Qualifier注解的特性。它们本身实际上就成为了限定符注解。
现在,我们可以重新看一下IceCream,并为其添加@Cold和@Creamy注解,如下所示:
@Component
@Cold
@Creamy
public class IceCream implements Dessert { ... }
类似地,Popsicle类可以添加@Cold和@Fruity注解:
@Component
@Cold
@Fruity
public class Popsicle implements Dessert { ... }
最终,在注入点,我们使用必要的限定符注解进行任意组合,从而将可选范围缩小到只有一个bean满足需求。为了得到IceCream bean,setDessert()方法可以这样使用注解:
@Autowired
@Cold
@Creamy
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
通过声明自定义的限定符注解,我们可以同时使用多个限定符,不会再有Java编译器的限制或错误。与此同时,相对于使用原始的@Qualifier并借助String类型来指定限定符,自定义的注解也更为类型安全。
让我们近距离观察一下setDessert()方法以及它的注解,这里并没有在任何地方明确指定要将IceCream自动装配到该方法中。相反,我们使用所需bean的特性来进行指定,即@Cold和@Creamy。因此,setDessert()方法依然能够与特定的Dessert实现保持解耦。任意满足这些特征的bean都是可以的。在当前选择Dessert实现时,恰好如此,IceCream是唯一能够与之匹配的bean。
在本节和前面的节中,我们讨论了几种通过自定义注解扩展Spring的方式。为了创建自定义的条件化注解,我们创建一个新的注解并在这个注解上添加了@Conditional。为了创建自定义的限定符注解,我们创建一个新的注解并在这个注解上添加了@Qualifier。这种技术可以用到很多的Spring注解中,从而能够将它们组合在一起形成特定目标的自定义注解。
现在我们来看一下如何在不同的作用域中声明bean。