Introduction

Autowiring by type can lead to ambiguity when we have multiple beans of the same base class type. Because there are multiple beans of the same type, it is important to have control over the bean selection process.

Spring provides @Primary annotation which declares a specific bean as primary, which means primary bean will be given higher preferences when autowiring to a single-valued dependency.

On the other hand, Spring also provides @Qualifier annotation in which the specific bean name is mentioned in its argument. Based on the argument, a specific bean is selected.

If you are new to Spring Framework, check out other articles related to dependency injection and autowiring.

1. Spring @Primary annotation – bean preference

When there are multiple beans of the same type, @Primary annotation can be used to give a higher preference to a specific bean. We will explore the beans registration in Spring in 2 ways, Java-based configurations and annotation-based.

a). Use @Primary with @Component annotation

Let us say we have a base class FileReader and two concrete classes PdfFileReader and WordFileReader implements the base class. When we register these concrete classes in spring, we get 2 beans of the type FileReader but of different names pdfFileReader and wordFileReader.

If we try to Autowire FileReader we will get Exception as there are multiple beans of the same type. To solve this we will assign higher preference to one of the bean class sayPdfFileReader by annotating it with @Primary. The PdfFileReader class is annotated with @Primary, indicating that it is the primary bean to be injected when there are multiple candidate beans that match a given injection point. Let’s break down the usage of @Primary and how it interacts with @Qualifier.

PdfFileReader.java
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");
  }
}

The class WordFileReader is not using @Primary annotation. The FileReader interface and the configs are shown below.

WordFileReader.java
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");
  }
}
FileReader.java
package basic.ioc.wiring.finetune;

public interface FileReader {
  void print();
}
ConfigWiring.java
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
}

Since the PdfFileReader is annotated with @Primary, when we autowire a field of type FileReader, the Spring IoC will inject the PdfFileReader bean. This is shown in the code below.

Junit test
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);
  }
}

However, if we want to inject WordFileReader for some reason, we need to make use of the @Qualifier annotation. This is explained in details below.

b). Bean ambiguity – NoUniqueBeanDefinitionException

If you do not specify the @Primary annotation, there will be ambiguity as there are 2 beans of the same type FileReader. When you try to inject such a bean, spring will throw an exception as below.

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

c). Use @Primary annotation with @Bean

The @Primary annotation can also be used in Java-based bean configurations declared using @Bean. In the below example, there are 3 insurance instances with the bean names insurance1, insurance2 and insurance3. The bean insurance1 is annotated with @Primary, so that this bean preferred when there is ambiguity.

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;
  }
}
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;
  }
}

Since there are multiple insurance beans, it can end up causing Exception related to bean ambiguity. However, we have instance1 annotated with @Primary, so by default instance1 bean will be injected unless a different bean name is mentioned.

Junit test
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");
  }
}

d). The source code of @Primary annotation

From the code below, it is clear that @Primary annotation is used on Class, interface (including annotation type), or enum declaration and methods (when used with bean).

Java
package org.springframework.context.annotation;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Primary {
}

2. Spring @Qualifier annotation – select bean

The @Autowired annotation does a great job of resolving the dependency. But it throws NoUniqueBeanDefinitionException when it can not resolve a specific bean due to ambiguity. This exception occurs when there is more than one bean of the same type is available in the container.

The @Primary annotation does a great job of giving a higher preference to a bean when there are multiple beans of the same types registered in the container. But it does not allow you to select a specific bean.

When you need more control over the bean selection process, you need to use Spring’s @Qualifier annotation. Irrespective of whether any of the beans is annotated with @Primary or not, the @Qualifier annotation will help you select the specific bean you want to inject.

JUnit test
@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());
  }
}

I have used the previous code examples in this code snippets to demonstrates how to pass the bean name in the @Qualifier annotation. As you can see in the above example, @Qualifier("wordFileReader") injects the wordFileReader and @Qualifier("insurance3") tells the Spring to inject insurance3 bean.

a). Use @Qualifier with @Component

You can also use the @Qualifier with @Component level to assign an extra name to the bean. A simple example is shown below.

Java
@Component
@Qualifier("restData")
public class RestData implements Endpoint {
    //...
}
 
@Component
@Qualifier("graphData")
public class GraphQlData implements Endpoint {
    //...
}
Java
  @Autowired
  @Qualifier("restData")
  private Endpoint endpoint;

b). Use @Qualifier with @Bean

You can similarly combine @Qualifier annotation with the @Bean annotation to assign an extra name to the bean. In the below code example, it is possible to use either @Qualifier("insurance3") or @Qualifier("generalInsurance") because of the presence of @Primary annotation.

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;
  }
}
Java
@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());
  }
}

c). Source code of @Qualifier annotation

As shown below, @Qualifier can be used on property, method arguments, methods, classes, interface, enums, custom annotations, etc.

Java
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 "";
}

Conclusion

Both the annotations @Primary and @Qualifier in Spring play a great role in resolving ambiguity, but they are for a different purpose. The @Primary annotation sets the bean preference and it is used with the @Bean or @Component etc stereotype annotations. On the other hand, @Qualifier is usually used with @Autowired or @Inject etc annotations.

  • @Primary: This annotation is used to indicate the primary bean to be injected when there are multiple candidates of the same type. It sets the default bean preference. It’s typically used in conjunction with @Bean or @Component annotations to mark a bean as the primary candidate for autowiring. When multiple beans of the same type are present in the application context, the one annotated with @Primary will be chosen by default for injection.
  • @Qualifier: This annotation is used to specify a particular bean to be injected when there are multiple beans of the same type. It allows for more fine-grained control over bean injection by providing a qualifier to disambiguate between multiple beans. @Qualifier is typically used in conjunction with @Autowired or @Inject annotations to specify the name or qualifier value of the desired bean to be injected.

In summary, while both @Primary and @Qualifier annotations help resolve ambiguity in bean injection, they serve different purposes and are used in different contexts within the Spring framework. @Primary sets the default preference for a bean, while @Qualifier allows for explicit selection of a bean based on its qualifier value. Understanding when and how to use these annotations is essential for effective bean wiring and dependency injection in Spring applications.

The code examples are available in the GitHub code repository. The Junit test cases are here and examples classes are here.


By |Last Updated: April 2nd, 2024|Categories: Spring Framework|