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
- If there is only one bean object of the same type, that bean object is used.
- If there are two or more bean objects of the same type and there is a matching
@Primaryor@Qualifiersetting, the bean object with the matching value is used if it exists. - If there are two or more bean objects of the same type and there is no
@Qualifieror@Primarysetting, Spring looks for a bean object whose name matches the@Beanmethod 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.