本記事は、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.xml
のservlet
要素内に以下の設定をします。以下の例ではhonban
と書いているのでSampleServiceB.java
が動きます。
web.xml
<init-param>
<param-name>spring.profiles.active</param-name>
<param-value>honban</param-value>
</init-param>