在Spring中,装配bean有以下3种方式:
在这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种方案:
既然现在有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又不知道该选择哪个了,也就是有了新的歧义性,所以甩锅抛出了异常。
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
为了避免因为修改类名而导致自动装配失效的问题,我们可以在@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个注解:
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个优点:
源码地址: https://github.com/zwwhnly/spring-action.git ,欢迎下载。
Craig Walls 《Spring实战(第4版)》
打个小广告,欢迎扫码关注微信公众号:「申城异乡人」,定期分享Java技术干货,让我们一起进步。