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

使用@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 等注解一起使用。

版权声明:著作权归作者所有。

thumb_up 0 | star_outline 0 | textsms 0