Springの依存関係自動注入 @Primary、@Qualifier
Springの依存関係自動注入
依存関係の自動注入は、Springの設定ファイルでオブジェクトを注入するときに明示しなくても、Springコンテナが必要な依存対象オブジェクトを自動的に探し、必要なオブジェクトへ注入してくれる機能です。
ここでは、同じ型のBeanオブジェクトを設定して自動注入を受ける場合に、意図しないオブジェクトが注入されるケースがあります。どのような場合にそうなるのかを、サンプルコードで確認します。
プロジェクトの作成
まず、次のようにcurlコマンドを使用してSpring Bootの初期プロジェクトを作成します。
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 -
このコマンドでは、特別な依存関係は追加していません。
同じ型のBeanオブジェクトが1つの場合
同じ型のBeanオブジェクトが重複せず、1つだけ存在する場合です。
次はBeanとして作成するクラスです。
package com.devkuma.bean.injection.ex1
class FooBean1(
val id: Int,
val name: String
)
設定オブジェクトでは、FooBean1クラスでBeanを1つだけ設定しました。
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"
)
}
サービスオブジェクトでは、設定で作成したFooBean1型のBeanを注入されます。
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}")
}
}
次にprintBeanメソッドを呼び出して、オブジェクトを表示してみます。
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)
}
実行結果:
Ex 1) fooService1 ---------
fooBean: id=1, name=fooBean1
予想どおり、設定したidとnameが表示されました。
同じ型のBeanオブジェクトが2つの場合
@Primary、@Qualifierを使用する
2つ目は、同じ型のBeanオブジェクトを2つ作成して@Primaryと@Qualifierを設定し、注入される側で@Qualifierを使用する場合と使用しない場合を見ます。
Beanとして作成するクラスを次のように作成します。
package com.devkuma.bean.injection.ex2
class FooBean2(
val id: Int,
val name: String
)
設定オブジェクトでは、FooBean2クラスでBeanを2つ設定しました。1つのオブジェクトには@Primaryアノテーションを設定し、もう1つには@QualifierアノテーションでBean名を設定しました。
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"
)
}
サービスオブジェクトでは、設定で作成したFooBean2型のBeanを2つそれぞれ注入されます。
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}")
}
}
別のサービスオブジェクトでも、設定で作成したFooBean2型のBeanを2つそれぞれ注入されますが、@Qualifierを使用して注入するBean名を明示しています。
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}")
}
}
次にprintBeanメソッドを呼び出して、オブジェクトを表示してみます。
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)
}
実行結果:
Ex 2) badFooService2 ------
fooBean20: id=20, name=fooBean20
fooBean21: id=20, name=fooBean20
結果を見ると、最初のbadFooService2の場合は、fooBean20とfooBean21のBean内容が同じように表示されました。fooBean21にfooBean20のBean内容が表示されたということです。
2つ目のgoodFooService2の場合は、@Qualifierで明示したとおりにBeanの内容が表示されていることがわかります。
@Primary、@Qualifierを使用しない
最後に、同じ型のBeanオブジェクトを2つ作成し、@Primaryと@Qualifierを設定しない場合を見ます。
Beanとして作成するクラスを次のように作成しました。
package com.devkuma.bean.injection.ex3
class FooBean3(
val id: Int,
val name: String
)
設定オブジェクトでは、FooBean3クラスでBeanを2つ設定しました。前述のとおり、@Primaryと@Qualifierは別途設定していません。
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"
)
}
サービスオブジェクトでは、設定で作成したFooBean3型のBeanを2つそれぞれ注入されますが、注入時に@Qualifierは使用していません。
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}")
}
}
今回もprintBeanメソッドを呼び出して、オブジェクトを表示してみます。
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)
}
実行結果:
Ex 3) fooService3 ---------
fooBean31: id=30, name=fooBean31
fooBean30: id=31, name=fooBean32
結果を見ると、fooBean31とfooBean32のどちらも、設定したとおりのidとnameが表示されました。
まとめ
- 同じ型のBeanオブジェクトが1つであれば、そのBeanオブジェクトを使用します。
- 同じ型のBeanオブジェクトが2つ以上あり、
@Primaryや@Qualifierの設定がある場合は、同じ値を持つBeanオブジェクトを探し、存在すればそのオブジェクトを使用します。 - 同じ型のBeanオブジェクトが2つ以上あり、
@Qualifierや@Primaryの設定がない場合は、@Beanメソッド名と同じBeanオブジェクトを探し、存在すればそのオブジェクトを使用します。
結論
@Primaryや@Qualifierは、どうしても必要な場合を除いて使用せず、@Beanメソッド名と注入される変数名を合わせることをおすすめします。
上記のサンプルコードはGitHubで確認できます。