Categories: Spring Framework

【Spring DI】@Qualifier、@Primary、@Profileの使い方

本記事は、Spring Framework(非boot)のDIの動作確認を行った。
以下は、interfaceを実装したサービスクラスが複数存在する状態でコントローラークラスからAutowiredして動かしたときのソースです。

問題のコード

<コントローラークラス>

HomeController.java

@Controller
public class HomeController {

    public SampleServiceInterface sampleService;

    @Autowired
    public HomeController(SampleServiceInterface sampleService){
        this.sampleService = sampleService;
    }

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home(Locale locale, Model model) {
        System.out.println("コントローラ");
        String result = sampleService.execute();

        model.addAttribute("str", result);

        return "home";
    }
}

<サービスクラス>

SampleServiceInterface.java

package com.my.app.service;

public interface SampleServiceInterface {

    public String execute();
}

<サービスクラスの実装クラスA>

SampleServiceA.java

@Service
public class SampleServiceA implements SampleServiceInterface{

	@Override
	public String execute() {

		return "is a SampleServiceA";
	}
}

<サービスクラスの実装クラスB>

SampleServiceB.java

@Service
public class SampleServiceB implements SampleServiceInterface{

	@Override
	public String execute() {

		return "is a SampleServiceB";
	}
}

実行後

実行後はこんなエラーが出てきてTomcatが立ち上がりません。
エラー1

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'homeController' defined in file [C:\usr\STS_3.9.6_win64\sts-bundle\pivotal-tc-server\instances\base-instance\wtpwebapps\sample3\WEB-INF\classes\com\my\app\ctrl\HomeController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.my.app.service.SampleServiceInterface' available: expected single matching bean but found 2: sampleServiceA,sampleServiceB

翻訳すると以下の意味になります。

org.springframework.beans.factory.UnsatisfiedDependencyException: ファイル [C:\usr\STS_3.9.6_win64\sts-bundle\pivotal-tc-server\instances\base-instance\wtpwebapps\ で定義された名前 'homeController' を持つ Bean の作成中にエラーが発生しましたsample3\WEB-INF\classes\com\my\app\ctrl\HomeController.class]: コンストラクター パラメーター 0 で表される満たされていない依存関係。ネストされた例外は org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualinging bean of type 'com.my.app.service.SampleServiceInterface' available: 予想される単一の一致する Bean ですが、2 が見つかりました: sampleServiceA、sampleServiceB

エラー2

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.my.app.service.SampleServiceInterface' available: expected single matching bean but found 2: sampleServiceA,sampleServiceB

翻訳すると以下の意味になります。


原因: org.springframework.beans.factory.NoUniqueBeanDefinitionException: タイプ 'com.my.app.service.SampleServiceInterface' の適格な Bean がありません: 単一の一致する Bean が予想されますが、2 が見つかりました: sampleServiceA、sampleServiceB

これを解決するための方法が@Qualifier@Primary@Profileのいずれかを付与する方法です。
順に見ていきます。

解決方法①:@Qualifierアノテーションを使用してBeanを指定する

import org.springframework.beans.factory.annotation.Qualifierをimportします。

コントローラークラスでは以下のように@Autowiredするタイミングで@Qualifierアノテーションに読み込みたいBean名(サービスクラスの実装)を指定します。

HomeController.java

@Autowired
public HomeController(@Qualifier("ServiceA")SampleServiceInterface sampleService){
    this.sampleService = sampleService;
}

サービスクラスの実装クラスAでは@Serviceの引数に名前をつけます。これがBean名です。

SampleServiceA.java

@Service("ServiceA")
public class SampleServiceA implements SampleServiceInterface{・・・}

サービスクラスの実装クラスBでは@Serviceの引数に名前をつけます。これがBean名です。

SampleServiceB.java

@Service("ServiceB")
public class SampleServiceB implements SampleServiceInterface{・・・}

この状態で実行すると以下のように正常に画面が表示されます。

上記の例では@Qualifier("ServiceA")としているので、 SampleServiceA.javaがDIされて実行されています。
もし、SampleServiceB.javaを実行したければ、コントローラークラスで指定していた@Qualifier`アノテーションのBean名を以下のように変更するだけでOKです。

@Autowired
public HomeController(@Qualifier("ServiceB")SampleServiceInterface sampleService){
    this.sampleService = sampleService;
}

はい、これでSampleServiceB.javaが実行されました。

解決方法②:@Primaryアノテーションを使用する

この方法の場合、コントローラークラスには@Qualifierアノテーションの指定は不要です。 DIさせたいクラスでimport org.springframework.context.annotation.Primary;をiomportし、 @Primaryアノテーションを付与するだけです。

SampleServiceA.java

@Service
@Primary
public class SampleServiceA implements SampleServiceInterface{

	@Override
	public String execute() {

		return "is a SampleServiceA";
	}
}

解決方法③:@Profileアノテーションを使用する

テスト環境や本番環境など、環境ごとにDIするクラスを切り替えたい場合には@Profileアノテーションを使用します。
使用するにはimport org.springframework.context.annotation.Profile;をimportします。

SampleServiceA.java

@Service
@Profile("dev")
public class SampleServiceA implements SampleServiceInterface{
・・・
}

SampleServiceB.java

@Service
@Profile("honban")
public class SampleServiceB implements SampleServiceInterface{
・・・
}

web.xmlservlet要素内に以下の設定をします。以下の例ではhonbanと書いているのでSampleServiceB.javaが動きます。

web.xml

<init-param>
    <param-name>spring.profiles.active</param-name>
    <param-value>honban</param-value>
</init-param>
issiki_wp