Categories: Spring Framework

【Spring DI】インターフェースを実装した2つのクラスが2つとも@Componentを付与されていた場合どうなるのか

本記事は、Spring Framework(非boot)のDIの動作確認を行った。
以下の4パターンの動作確認を行いました。

No. context:component-scanにパッケージが含まれているか 実装クラス1 `実装クラス2 実行結果
1 x DI可能
2 x DI可能
3 実行時エラーが発生
4 x x 実行時エラーが発生

本記事タイトルの「インターフェースを実装した2つのクラスが2つとも@Componentを付与されていた場合どうなるのか」というのはパターン3になります。

各パターンで使用するJSP、インターフェース、context:component-scan要素の内容は同じものを使用します。

各パターンの結果を確認する画面(JSP)home.jsp

各パターンの結果を表示する用の画面として以下を用意しています。

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>
<html>
	<head>
		<title>Home</title>
	</head>
	<body>
		<h1>Hello world!</h1>
		<P> ${str} </P>
	</body>
</html>

コンポーネントスキャンの対象プロジェクトの確認

servlet-context.xml(C:\workspace\sample\src\main\webapp\WEB-INF\spring\appServlet.servlet-context.xml)を開き、context:component-scan要素の内容を確認する。
下記を確認すると「com.my.app」プロジェクトがコンポーネントスキャンの対象になっていることが確認します。

<context:component-scan base-package="com.my.app" />

インターフェース SampleServiceInterface.java

実装クラスの元ネタとなるインターフェースは以下のように定義しておきます。

package com.my.app.service;

public interface SampleServiceInterface {

	public String execute();
}

コントローラークラス HomeController.java

package com.my.app.ctrl;

import java.util.Locale;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.my.app.service.SampleServiceInterface;

@Controller
public class HomeController {

	public SampleServiceInterface sampleServiceInterface;

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

	@RequestMapping(value = "/", method = RequestMethod.GET)
	public String home(Locale locale, Model model) {

		String result = sampleServiceInterface.execute();//DIして実装クラスを実行

		model.addAttribute("str", result );

		return "home";
	}
}

パターン1の場合

パターン1では、SampleServiceA.java@Componentアノテーションを付与し、SampleServiceB.javaでは付与しません。

実装クラス1 SampleServiceA.java

このクラスに@Componentアノテーションを付与します。

package com.my.app.service.impl;

import org.springframework.stereotype.Component;

import com.my.app.service.SampleServiceInterface;

@Component
public class SampleServiceA implements SampleServiceInterface{

	@Override
	public String execute() {

		return "is a SampleServiceA";
	}
}

実装クラス2 SampleServiceB.java

このクラスには@Componentアノテーションを付与しません。

package com.my.app.service.impl;

import com.my.app.service.SampleServiceInterface;

public class SampleServiceB implements SampleServiceInterface{

	@Override
	public String execute() {

		return "is a SampleServiceB";
	}
}

実行結果

実行すると、画面に以下のように表示されました。SampleServiceA.javaがreturnした値を画面に表示できていることを確認できました。

パターン2の場合

パターン2では、SampleServiceA.javaには@Componentアノテーションを付与せず、SampleServiceB.javaに付与します。

実装クラス1 SampleServiceA.java

このクラスには@Componentアノテーションを付与しません。

package com.my.app.service.impl;

import com.my.app.service.SampleServiceInterface;

public class SampleServiceA implements SampleServiceInterface{

	@Override
	public String execute() {

		return "is a SampleServiceA";
	}
}

実装クラス2 SampleServiceB.java

このクラスに@Componentアノテーションを付与します。

package com.my.app.service.impl;

import org.springframework.stereotype.Component;

import com.my.app.service.SampleServiceInterface;

@Component
public class SampleServiceB implements SampleServiceInterface{

	@Override
	public String execute() {

		return "is a SampleServiceB";
	}
}

実行結果

実行すると、画面に以下のように表示されました。SampleServiceB.javaがreturnした値を画面に表示できていることを確認できました。

パターン3の場合

パターン3では、SampleServiceA.javaSampleServiceB.javaの両方に@Componentアノテーションを付与します。

実装クラス1 SampleServiceA.java

@Componentアノテーションを付与します。

package com.my.app.service.impl;

import org.springframework.stereotype.Component;

import com.my.app.service.SampleServiceInterface;

@Component
public class SampleServiceA implements SampleServiceInterface{

	@Override
	public String execute() {

		return "is a SampleServiceA";
	}
}

実装クラス2 SampleServiceB.java

@Componentアノテーションを付与します。

package com.my.app.service.impl;

import org.springframework.stereotype.Component;

import com.my.app.service.SampleServiceInterface;

@Component
public class SampleServiceB implements SampleServiceInterface{

	@Override
	public String execute() {

		return "is a SampleServiceB";
	}
}

実行結果

実行時、以下のエラーがコンソールに表示され、起動することができませんでした。
【例外】

javax.servlet.ServletException: サーブレット appServlet のServlet.init()が例外を投げました
	org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)

【原因1】UnsatisfiedDependencyExceptionが発生

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'homeController' defined in file [C:\workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\sample\WEB-INF\classes\com\my\app\ctrl\HomeController.class]: Unsatisfied dependency expressed through constructor argument with index 0 of type [com.my.app.service.SampleServiceInterface]: : No unique bean of type [com.my.app.service.SampleServiceInterface] is defined: expected single matching bean but found 2: [sampleServiceA, sampleServiceB]; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [com.my.app.service.SampleServiceInterface] is defined: expected single matching bean but found 2: [sampleServiceA, sampleServiceB]
	org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:730)

【原因2】NoSuchBeanDefinitionExceptionが発生

org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [com.my.app.service.SampleServiceInterface] is defined: expected single matching bean but found 2: [sampleServiceA, sampleServiceB]
	org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:800)

ログを見ると以下のように出力されています。

 No unique bean of type [com.my.app.service.SampleServiceInterface] is defined: expected single matching bean but found 2: [sampleServiceA, sampleServiceB]

DeepL日本語に訳すると

com.my.app.service.SampleServiceInterface] タイプのユニークなビーンが定義されていません: 1つの一致するビーンを期待しましたが、2つ見つかりました: [sampleServiceA, sampleServiceB].

となりました。つまり、「インターフェースの中に実装クラスが2つ見つかったよ。実装クラスはユニークにしてね(1つにしてね。)」と解釈できます。
要するに、@Componentアノテーションが付与された実装クラスを1つだけにしてください。ということです。

パターン4の場合

パターン4では、SampleServiceA.javaSampleServiceB.javaの両方に@Componentアノテーションを付与しません。

実装クラス1 SampleServiceA.java

@Componentアノテーションを付与しません。

package com.my.app.service.impl;

import com.my.app.service.SampleServiceInterface;

public class SampleServiceA implements SampleServiceInterface{

	@Override
	public String execute() {

		return "is a SampleServiceA";
	}
}

実装クラス2 SampleServiceB.java

package com.my.app.service.impl;

import com.my.app.service.SampleServiceInterface;

public class SampleServiceB implements SampleServiceInterface{

	@Override
	public String execute() {

		return "is a SampleServiceB";
	}
}

実行結果

実行時、以下のエラーがコンソールに表示され、起動することができませんでした。
【例外】

javax.servlet.ServletException: サーブレット appServlet のServlet.init()が例外を投げました
	org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)

【原因1】UnsatisfiedDependencyExceptionが発生

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'homeController' defined in file [C:\workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\sample\WEB-INF\classes\com\my\app\ctrl\HomeController.class]: Unsatisfied dependency expressed through constructor argument with index 0 of type [com.my.app.service.SampleServiceInterface]: : No matching bean of type [com.my.app.service.SampleServiceInterface] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No matching bean of type [com.my.app.service.SampleServiceInterface] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
	org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:730)

【原因2】NoSuchBeanDefinitionExceptionが発生

org.springframework.beans.factory.NoSuchBeanDefinitionException: No matching bean of type [com.my.app.service.SampleServiceInterface] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
	org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoSuchBeanDefinitionException(DefaultListableBeanFactory.java:924)

ログを見ると以下のように出力されています。

No matching bean of type [com.my.app.service.SampleServiceInterface] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}

DeepL日本語に訳すると

依存関係に [com.my.app.service.SampleServiceInterface] タイプの一致する bean が見つかりません: この依存関係の autowire 候補として修飾される bean が少なくとも 1 つ期待されます。依存関係のアノテーション。{}

となりました。つまり「@Componentアノテーションが付与された実装クラスが一つもなかったよ。」ということになります。

issiki_wp