使用@Primary 和@Qualifier 微调自动装配

简介
当我们有多个相同基类类型的 bean 时,按类型自动装配会导致歧义。因为有多个相同类型的 bean,所以控制 bean 选择过程很重要。
Spring 提供了 @Primary 注释,它将特定的 bean 声明为主要的,这意味着在自动装配到单值依赖项时,主要 bean 将被赋予更高的首选项。
另一方面,Spring 还提供了 @Qualifier 注释,其中在其参数中提到了特定的 bean 名称。根据参数,选择特定的 bean。
Spring @Primary 注解 - bean优先级
当有多个相同类型的 bean 时,可以使用 @Primary 注解对特定 bean 给予更高的优先级。我们将以两种方式探索 Spring 中的 beans 注册,基于 Java 的配置和基于注解的方式。
将 @Primary 与 @Component 注释一起使用
假设我们有一个基类 FileReader 和两个具体类 PdfFileReader 和 WordFileReader 实现基类。当我们在 spring 中注册这些具体类时,我们会得到 2 个 FileReader 类型的 bean,但它们的名称不同 pdfFileReader 和 wordFileReader。
如果我们尝试 Autowire FileReader,我们将得到异常,因为有多个相同类型的 bean。为了解决这个问题,我们将通过使用@Primary 注释bean 类sayPdfFileReader 来为其分配更高的优先级。
FileReader
package basic.ioc.wiring.finetune;
public interface FileReader {
void print();
}
PdfFileReader
package basic.ioc.wiring.finetune;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
@Primary
@Component
public class PdfFileReader implements FileReader {
@Override
public void print() {
System.out.println("Inside PdfFileReader");
}
}
WordFileReader
package basic.ioc.wiring.finetune;
import org.springframework.stereotype.Component;
@Component
public class WordFileReader implements FileReader {
@Override
public void print() {
System.out.println("Inside WordFileReader");
}
}
ConfigWiring
package basic.ioc.wiring.finetune;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@ComponentScan("basic.ioc.wiring.finetune")
@Configuration
public class ConfigWiring {
//extra configs goes here
}
由于 PdfFileReader 带有 @Primary 注释,因此当我们自动装配 FileReader 类型的字段时,Spring IoC 将注入 PdfFileReader bean。这显示在下面的代码中。
package basic.ioc.wiring.finetune;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(ConfigWiring.class)
public class FineTuneAutowiring {
@Autowired
private FileReader fileReader;
@Test
public void testFileReader() {
Assert.assertNotNull(fileReader);
Assert.assertEquals(fileReader.getClass(), PdfFileReader.class);
}
}
Bean 歧义 – NoUniqueBeanDefinitionException
如果不指定@Primary 注解,则会出现歧义,因为有2 个相同类型的FileReader bean。当您尝试注入这样的 bean 时,spring 会抛出如下异常。
SEVERE: Caught exception while allowing TestExecutionListener [org.springframework.test.context.support.DependencyInjectionTestExecutionListener@506ae4d4] to prepare test instance [basic.ioc.wiring.finetune.FineTuneAutowiring@5b202a3a]
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'basic.ioc.wiring.finetune.FineTuneAutowiring': Unsatisfied dependency expressed through field 'fileReader'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'basic.ioc.wiring.finetune.FileReader' available: expected single matching bean but found 2: pdfFileReader,wordFileReader
将 @Primary 注释与 @Bean 一起使用
@Primary 注释也可以用于使用@Bean 声明的基于 Java 的 bean 配置。在下面的示例中,有 3 个保险实例,其 bean 名称为 insurance1、insurance2 和 insurance3。 bean insurance1 带有@Primary 注释,以便在有歧义时首选此 bean。
Insurance.java
package basic.ioc.wiring.finetune;
public class Insurance {
private String type;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
ConfigWiring.java
package basic.ioc.wiring.finetune;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@ComponentScan("basic.ioc.wiring.finetune")
@Configuration
public class ConfigWiring {
//bean name = insurance1
@Bean
@Primary
public Insurance insurance1(){
Insurance healthInsurance = new Insurance();
healthInsurance.setType("Health Insurance");
return healthInsurance;
}
//bean name = insurance2
@Bean
public Insurance insurance2(){
Insurance termInsurance = new Insurance();
termInsurance.setType("Term Insurance");
return termInsurance;
}
//bean name = insurance3
@Bean
public Insurance insurance3() {
Insurance generalInsurance = new Insurance();
generalInsurance.setType("General Insurance");
return generalInsurance;
}
}
由于有多个保险 bean,它最终可能导致与 bean 歧义相关的异常。但是,我们使用 @Primary 注释了 instance1,因此默认情况下将注入 instance1 bean,除非提到不同的 bean 名称。
package basic.ioc.wiring.finetune;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(ConfigWiring.class)
public class FineTuneAutowiring {
@Autowired
private Insurance insurance;
@Test
public void testInsuranceBean(){
Assert.assertNotNull(insurance);
Assert.assertEquals(insurance.getType(), "Health Insurance");
}
}
@Primary 注解的源码
从下面的代码可以看出@Primary注解用在Class、接口(包括注解类型)或枚举声明和方法(与bean一起使用时)。
package org.springframework.context.annotation;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Primary {
}
Spring @Qualifier 注解——选择bean
@Autowired 注释在解决依赖关系方面做得很好。但是当由于歧义而无法解析特定的 bean 时,它会抛出 NoUniqueBeanDefinitionException。当容器中有多个相同类型的 bean 可用时,会发生此异常。
当容器中注册了多个相同类型的 bean 时,@Primary 注释可以很好地为 bean 提供更高的优先级。但它不允许您选择特定的 bean。
当您需要对 bean 选择过程进行更多控制时,您需要使用 Spring 的 @Qualifier 注解。无论是否有任何 bean 使用 @Primary 注释,@Qualifier 注释都将帮助您选择要注入的特定 bean。
@SpringJUnitConfig(ConfigWiring.class)
public class FineTuneAutowiring {
@Autowired
@Qualifier("wordFileReader")
private FileReader fileReader2;
@Test
public void testWordFileReader() {
Assert.assertNotNull(fileReader2);
Assert.assertEquals(fileReader2.getClass(), WordFileReader.class);
}
@Autowired
@Qualifier("insurance3")
private Insurance myInsurance;
@Test
public void testInsuranceWithQualifier(){
Assert.assertNotNull(myInsurance);
Assert.assertEquals("General Insurance", myInsurance.getType());
}
}
我在这段代码片段中使用了前面的代码示例来演示如何在 @Qualifier 注释中传递 bean 名称。正如您在上面的示例中看到的,@Qualifier("wordFileReader") 注入 wordFileReader,@Qualifier("insurance3") 告诉 Spring 注入 insurance3 bean。
将@Qualifier 与@Component 一起使用
您还可以使用带有@Component 级别的@Qualifier 来为bean 分配一个额外的名称。一个简单的例子如下所示。
BeanClass
@Component
@Qualifier("restData")
public class RestData implements Endpoint {
//...
}
@Component
@Qualifier("graphData")
public class GraphQlData implements Endpoint {
//...
}
@Autowired
@Qualifier("restData")
private Endpoint endpoint;
将@Qualifier 与@Bean 一起使用
您可以类似地将@Qualifier 注释与@Bean 注释结合起来为bean 分配一个额外的名称。在下面的代码示例中,可以使用@Qualifier("insurance3") 或@Qualifier("generalInsurance")。
ConfigWiring.java
@ComponentScan("basic.ioc.wiringfinetune")
@Configuration
public class ConfigWiring {
//bean name = insurance3
@Bean
@Qualifier("generalInsurance")
public Insurance insurance3() {
Insurance generalInsurance = new Insurance();
generalInsurance.setType("General Insurance");
return generalInsurance;
}
}
@SpringJUnitConfig(ConfigWiring.class)
public class FineTuneAutowiring {
@Autowired
//@Qualifier("insurance3") - this will work
@Qualifier("generalInsurance")
private Insurance myInsurance;
@Test
public void testInsuranceWithQualifier(){
Assert.assertNotNull(myInsurance);
Assert.assertEquals("General Insurance", myInsurance.getType());
}
}
@Qualifier 注解的源代码
如下图,@Qualifier 可用于属性、方法参数、方法、类、接口、枚举、自定义注解等。
package org.springframework.beans.factory.annotation;
@Target({ElementType.FIELD, ElementType.METHOD,
ElementType.PARAMETER, ElementType.TYPE,
ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Qualifier {
String value() default "";
}
总结
Spring 中的注解@Primary 和@Qualifier 都在解决歧义方面发挥了重要作用,但它们的目的不同。@Primary 注释设置 bean 首选项,它与 @Bean 或 @Component 等原型注释一起使用。另一方面,@Qualifier 通常与@Autowired 或@Inject 等注解一起使用。