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

予想どおり、設定したidnameが表示されました。

同じ型の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の場合は、fooBean20fooBean21のBean内容が同じように表示されました。fooBean21fooBean20の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

結果を見ると、fooBean31fooBean32のどちらも、設定したとおりのidnameが表示されました。

まとめ

  1. 同じ型のBeanオブジェクトが1つであれば、そのBeanオブジェクトを使用します。
  2. 同じ型のBeanオブジェクトが2つ以上あり、@Primary@Qualifierの設定がある場合は、同じ値を持つBeanオブジェクトを探し、存在すればそのオブジェクトを使用します。
  3. 同じ型のBeanオブジェクトが2つ以上あり、@Qualifier@Primaryの設定がない場合は、@Beanメソッド名と同じBeanオブジェクトを探し、存在すればそのオブジェクトを使用します。

結論

@Primary@Qualifierは、どうしても必要な場合を除いて使用せず、@Beanメソッド名と注入される変数名を合わせることをおすすめします。

上記のサンプルコードはGitHubで確認できます。