学而不思则罔,思而不学则殆。
应用场景
Spring使用自动装配将bean引用注入到构造参数和属性中时,仅有一个bean匹配所需的结果时,自动装配才是有效的,如果不止一个bean能够匹配,这种歧义性会阻碍Spring自动装配属性、构造器参数或方法参数,且会抛出NoUniqueBeanDefinitionException。
如下有Computer:
package chapter3; public interface Computer { void play(); }
类AppleComputer,LenovoComputer,HaierComputer都实现了接口Computer,且均使用了@Component注解:
package chapter3; import org.springframework.stereotype.Component; @Component public class AppleComputer implements Computer { public void play() { System.out.println("apple computer playing..."); } }
package chapter3; import org.springframework.stereotype.Component; @Component public class LenovoComputer implements Computer { public void play() { System.out.println("lenovo computer playing..."); } }
package chapter3; import org.springframework.stereotype.Component; @Component public class HaierComputer implements Computer { public void play() { System.out.println("haier computer playing..."); } }
在Spring试图自动装配时,Spring无法做出选择,只好宣告失败并抛出异常:
package chapter3; import org.springframework.stereotype.Component; @Component public class HaierComputer implements Computer { public void play() { System.out.println("haier computer playing..."); } }
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'chapter3.ComputerTest':
Unsatisfied dependency expressed through method 'setComputer' parameter 0; nested exception is org.springframework.beans.
factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'chapter3.Computer' available: expected single matching
bean but found 3: appleComputer,haierComputer,lenovoComputer
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'chapter3.
Computer' available: expected single matching bean but found 3: appleComputer,haierComputer,lenovoComputer
当遇到这种歧义性的时候,Spring提供了多种可选方案来解决这样的问题:设置首选的bean;使用限定符。
设定首选的bean
在Spring中可以通过@Primary来指定首选的bean,@Primary能够与@Component组合用在组件扫描的bean上,也可以与@Bean组合用在Java配置的bean声明中。
如下使用@Component与@Primary组合指定AppleComputer为首选的bean,那么当创建Computer引用时,Spring会使用首选的bean。
package chapter3; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; @Component @Primary public class AppleComputer implements Computer { public void play() { System.out.println("apple computer playing..."); } }
package chapter3; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @Configuration @ComponentScan public class ComputerConfig { @Bean @Primary public Computer appleComputer() { return new AppleComputer(); } }
在XML中使用primary属性指定首选的bean
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="appleComputer" class="chapter3.AppleComputer" primary="true"></bean> </beans>
@Primary无法指定唯一的、无歧义性的bean,如果你在LenovoComputer或HaierComputer类上也加上@Primary,那么也带来了新的歧义性问题。
使用限定符限定自动装配的bean
@Qualifier注解是使用限定符的主要方式。它可以与@Autowired和@Inject协同使用,在注入的时候指定想要注入的bean ID。
package chapter3; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes=ComputerConfig.class) public class ComputerTest { private Computer computer; @Autowired @Qualifier("appleComputer") public void setComputer(Computer computer) { this.computer = computer; } @Test public void run() { computer.play(); } }
因为@Qualifier限定符与要注入的bean的名称时紧耦合的,假设修改AppleComputer类的名称,那么限定符会失效,这个时候我们可以创建自定义的限定符,而不是依赖于将bean ID作为限定条件。
package chapter3; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @Component @Qualifier("apple") public class AppleComputer implements Computer { public void play() { System.out.println("apple computer playing..."); } }
package chapter3; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes=ComputerConfig.class) public class ComputerTest { private Computer computer; @Autowired @Qualifier("apple") public void setComputer(Computer computer) { this.computer = computer; } @Test public void run() { computer.play(); } }
还是存在一个问题,仅使用一个@Qualifier仍然无法确定唯一的、无歧义性的bean。
Java 8 允许出现重复的注解,只要这个注解本身在定义时带有@Repeatable注解,但是@Qualifier注解在定义时没有该定义。
* Copyright 2002-2011 the original author or authors. package org.springframework.beans.factory.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * This annotation may be used on a field or parameter as a qualifier for * candidate beans when autowiring. It may also be used to annotate other * custom annotations that can then in turn be used as qualifiers. * * @author Mark Fisher * @author Juergen Hoeller * @since 2.5 * @see Autowired */ @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Qualifier { String value() default ""; }
虽然我们无法只借助@Qualifier限定符注解就确定唯一、无歧义的bean,但是,我们可以创建自定义的限定符注解,然后进行任意组合直至将可选范围缩小到只有一个bean满需需求。
创建自定义的限定符注解
创建@Apple限定符注解
package chapter3; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.beans.factory.annotation.Qualifier; @Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Apple { }