Automatic Spring Dependency Injection with @Primary and @Qualifier

Automatic Spring Dependency Injection

Automatic dependency injection is a feature where the Spring container finds the required dependent object and injects it into the target object even when the object is not specified explicitly in the Spring configuration file.

In some cases, when beans of the same type are configured and injected automatically, an unintended object can be injected. This article uses example code to see when that happens.

Create the Project

First, create a Spring Boot starter project with the following curl command.

curl https://start.spring.io/starter.tgz \
-d bootVersion=2.6.6 \
-d baseDir=spring-core-bean-injection \
-d groupId=com.devkuma \
-d artifactId=spring-core-bean-injection \
-d packageName=com.devkuma.bean.injection \
-d applicationName=BeanInjectionApplication \
-d packaging=jar \
-d language=kotlin \
-d javaVersion=11 \
-d type=gradle-project | tar -xzvf -

This command does not add any special dependencies.

When There Is One Bean of the Same Type

This is the case where only one bean of the same type exists and there are no duplicates.

The following class will be registered as a bean.

package com.devkuma.bean.injection.ex1

class FooBean1(
    val id: Int,
    val name: String
)

In the configuration object, only one bean is configured with the FooBean1 class.

package com.devkuma.bean.injection.ex1

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class FooBeanConfig1 {

    @Bean
    fun fooBean1() =
        FooBean1(
            id = 1,
            name = "fooBean1"
        )
}

The service object receives the FooBean1 type bean created by the configuration.

package com.devkuma.bean.injection.ex1

import org.springframework.stereotype.Service

@Service
class FooService1(
    private var fooBean: FooBean1
) {
    fun printBean() {
        println("fooBean: id=${fooBean.id}, name=${fooBean.name}")
    }
}

Now call the printBean method and display the object.

package com.devkuma.bean.injection

import com.devkuma.bean.injection.ex1.FooService1
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class BeanInjectionApplication(
    private val fooService1: FooService1,
) : CommandLineRunner {

    override fun run(vararg args: String?) {
        println("Ex 1) fooService1 ---------")
        fooService1.printBean()
    }
}

fun main(args: Array<String>) {
    runApplication<BeanInjectionApplication>(*args)
}

Execution result:

Ex 1) fooService1 ---------
fooBean: id=1, name=fooBean1

As expected, the id and name configured in the bean are displayed.

When There Are Two Beans of the Same Type

Using @Primary and @Qualifier

Next, create two beans of the same type, configure them with @Primary and @Qualifier, and compare the case where @Qualifier is used at the injection point with the case where it is not used.

Create the bean class as follows.

package com.devkuma.bean.injection.ex2

class FooBean2(
    val id: Int,
    val name: String
)

In the configuration object, two beans are configured with the FooBean2 class. One object is marked with the @Primary annotation, and the other object is given a bean name with the @Qualifier annotation.

package com.devkuma.bean.injection.ex2

import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary

@Configuration
class FooBeanConfig2 {

    @Primary
    @Bean
    fun fooBean20() =
        FooBean2(
            id = 20,
            name = "fooBean20"
        )

    @Qualifier("fooBean21")
    @Bean
    fun fooBean21() =
        FooBean2(
            id = 21,
            name = "fooBean21"
        )
}

The service object receives two FooBean2 type beans created by the configuration.

package com.devkuma.bean.injection.ex2

import org.springframework.stereotype.Service

@Service
class BadFooService2(
    private val fooBean20: FooBean2,
    private val fooBean21: FooBean2
) {
    fun printBean() {
        println("fooBean20: id=${fooBean20.id}, name=${fooBean20.name}")
        println("fooBean21: id=${fooBean21.id}, name=${fooBean21.name}")
    }
}

Another service object also receives two FooBean2 type beans created by the configuration, but it uses @Qualifier to explicitly specify the bean names to inject.

package com.devkuma.bean.injection.ex2

import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service

@Service
class GoodFooService2(
    @Qualifier("fooBean20") private val fooBean20: FooBean2,
    @Qualifier("fooBean21") private val fooBean21: FooBean2
) {
    fun printBean() {
        println("fooBean20: id=${fooBean20.id}, name=${fooBean20.name}")
        println("fooBean21: id=${fooBean21.id}, name=${fooBean21.name}")
    }
}

Now call the printBean method and display the objects.

package com.devkuma.bean.injection

import com.devkuma.bean.injection.ex2.BadFooService2
import com.devkuma.bean.injection.ex2.GoodFooService2
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class BeanInjectionApplication(
    private val badFooService2: BadFooService2
) : CommandLineRunner {

    override fun run(vararg args: String?) {
        println("Ex 2) badFooService2 ------")
        badFooService2.printBean()
    }
}

fun main(args: Array<String>) {
    runApplication<BeanInjectionApplication>(*args)
}

Execution result:

Ex 2) badFooService2 ------
fooBean20: id=20, name=fooBean20
fooBean21: id=20, name=fooBean20

In the first case, badFooService2, the contents of the fooBean20 and fooBean21 beans are displayed as the same. The contents of fooBean20 are displayed for fooBean21.

In the second case, goodFooService2, the bean contents are displayed according to the names explicitly specified with @Qualifier.

Not Using @Primary and @Qualifier

Finally, create two beans of the same type without setting @Primary or @Qualifier.

Create the bean class as follows.

package com.devkuma.bean.injection.ex3

class FooBean3(
    val id: Int,
    val name: String
)

In the configuration object, two beans are configured with the FooBean3 class. As mentioned above, neither @Primary nor @Qualifier is configured.

package com.devkuma.bean.injection.ex3

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class FooBeanConfig3 {

    @Bean
    fun fooBean31() =
        FooBean3(
            id = 30,
            name = "fooBean31"
        )

    @Bean
    fun fooBean32() =
        FooBean3(
            id = 31,
            name = "fooBean32"
        )
}

The service object receives two FooBean3 type beans created by the configuration, and it does not use @Qualifier during injection.

package com.devkuma.bean.injection.ex3

import org.springframework.stereotype.Service

@Service
class FooService3(
    private var fooBean31: FooBean3,
    private var fooBean32: FooBean3
) {
    fun printBean() {
        println("fooBean31: id=${fooBean31.id}, name=${fooBean31.name}")
        println("fooBean30: id=${fooBean32.id}, name=${fooBean32.name}")
    }
}

Call the printBean method again and display the objects.

package com.devkuma.bean.injection

import com.devkuma.bean.injection.ex3.FooService3
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class BeanInjectionApplication(
    private val goodFooService2: GoodFooService2
) : CommandLineRunner {

    override fun run(vararg args: String?) {
        println("Ex 3) fooService3 ---------")
        fooService3.printBean()
    }
}

fun main(args: Array<String>) {
    runApplication<BeanInjectionApplication>(*args)
}

Execution result:

Ex 3) fooService3 ---------
fooBean31: id=30, name=fooBean31
fooBean30: id=31, name=fooBean32

The result shows that both fooBean31 and fooBean32 display the id and name configured for them.

Summary

  1. If there is only one bean object of the same type, that bean object is used.
  2. If there are two or more bean objects of the same type and there is a matching @Primary or @Qualifier setting, the bean object with the matching value is used if it exists.
  3. If there are two or more bean objects of the same type and there is no @Qualifier or @Primary setting, Spring looks for a bean object whose name matches the @Bean method name. If it exists, that object is used.

Conclusion

Unless @Primary or @Qualifier is truly required, it is recommended to avoid using them and instead match the @Bean method name with the variable name receiving the injection.

The example code above is available on GitHub.