1. 什么是自动装配的歧义性?
在 Spring 中, 装配 bean 有以下 3 种方式:
自动装配
Java 配置
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 种方案:
标记首选的 bean
使用限定符
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 个优点:
可以同时使用多个自定义注解, 但 @Qualifier 注解只能使用 1 个
使用自定义注解比 @Qualifier 注解更为类型安全
4. 源码及参考
源码地址: https://github.com/zwwhnly/spring-action.git , 欢迎下载.
Craig Walls 《Spring 实战 (第 4 版)》
5. 最后
来源: https://www.cnblogs.com/zwwhnly/p/11355879.html