Categories: Spring Framework

【Spring DI】component-scanの研究1

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

No. context:component-scanにパッケージが含まれているか コンポーネントスキャンの対象となるアノテーションの付与 Autowiredの付与 実行結果
1 DI可能
2 x 実行時エラーが発生
3 x 実行時エラーが発生
4 x 実行時エラーが発生

インスタンス生成アノテーション

Springが起動時にインスタンスを自動生成するためには、インスタンス化させたいクラスに「コンポーネントスキャンの対象となるアノテーション」を付与する必要がある。以下の4種類ある。

  • @Controller
  • @Service
  • @Repository
  • @Component

* @Componentは、他3つに当てはまらない場合に指定するアノテーション。

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

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

HTML

<%@ 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>

パターン1の場合

パターン1はDIが正常に出来ている例です。

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

まずは、どのプロジェクトがコンポーネントスキャンの対象になっているかを確認します。
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" />

インスタンス生成アノテーションの付与

「com.my.app」プロジェクト配下のSampleService@Componentアノテーションを付与しました。 このクラスの処理は「—-@Component—-」という文字列を返すのみです。

Java

package com.my.app.service;

import org.springframework.stereotype.Component;

@Component
public class SampleService {

	public String execute() {
		return "----@Component----";
	}
}

@Autowiredアノテーションの付与

「com.my.app」プロジェクト配下のコントローラクラスであるHomeControllerにコンストラクタインジェクション方式で@Autowiredアノテーションを付与した。

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.SampleService;

@Controller
public class HomeController {

	public SampleService sampleService;

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

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

		String result = sampleService.execute();

		model.addAttribute("str", result );

		return "home";
	}
}

実行結果

以下のとおり、画面上に「—-@Component—-」という文字列が表示されました。
つまり、DIできています。

パターン2

パターン2は@Autowiredアノテーションを削除することで、インスタンスを注入するタイミングをなくした状態の動作確認をします。

@Autowiredアノテーションを削除する

「com.my.app」プロジェクト配下のコントローラクラスであるHomeControllerから@Autowiredアノテーションをコメントアウトして削除します。
ここ以外のクラスは変更しません。パターン1と同じにします。

Java

package com.my.app.ctrl;

import java.util.Locale;

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.SampleService;

@Controller
public class HomeController {

	public SampleService sampleService;

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

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

		String result = sampleService.execute();

		model.addAttribute("str", result );

		return "home";
	}
}

実行結果

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

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

【原因1】BeanCreationExceptionが発生

org.springframework.beans.factory.BeanCreationException: 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]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [com.my.app.ctrl.HomeController]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.my.app.ctrl.HomeController.<init>()
	org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:997)

【原因2】BeanInstantiationExceptionが発生

org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [com.my.app.ctrl.HomeController]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.my.app.ctrl.HomeController.<init>()
	org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:72)

【原因3】NoSuchMethodExceptionが発生

java.lang.NoSuchMethodException: com.my.app.ctrl.HomeController.<init>()
	java.lang.Class.getConstructor0(Class.java:3082)
	java.lang.Class.getDeclaredConstructor(Class.java:2178)
	org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:67)

原因2のエラーをDeepL日本語訳すると

org.springframework.beans.BeanInstantiationException: Bean クラス [com.my.app.ctrl.HomeController] をインスタンス化できませんでした。デフォルトコンストラクタが見つかりません。ネストされた例外は java.lang.NoSuchMethodException: com.my.app.ctrl.HomeController.<init>()です。

となります。つまりデフォルトのコンストラクタが存在すればいいわけなので、以下のように、引数を持たないコンストラクタを宣言すれば実行時エラーは回避できます。ただし、SampleServiceのインスタンスが注入できないので、画面を表示するとエラーになります。

public HomeController(){

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

パターン3の場合

パターン3では、@Componentアノテーションを削除することでインスタンスを生成せないようにします。
(コントローラ側のクラスでは@Autowiredアノテーションを残しているので、インスタンスを注入するタイミングは存在してるけど、注入するためのインスタンスがない。というパターンになります。)

インスタンス生成アノテーションを削除する

「com.my.app」プロジェクト配下のSampleServiceから@Componentアノテーションをコメントアウトして削除してみます。
ここ以外のクラスは変更しません。パターン1と同じにします。

package com.my.app.service;

import org.springframework.stereotype.Component;

// @Component
public class SampleService {

	public String execute() {
		return "----@Component----";
	}
}

実行結果

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

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.SampleService]: : No matching bean of type [com.my.app.service.SampleService] 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.SampleService] 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.SampleService] 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)

原因2のエラーをDeepL日本語訳すると

この依存関係のための autowire candidate として修飾される少なくとも 1 つの bean が期待されました。依存関係のアノテーション。{}

となりました。「1つのbeanを期待した」というのはインスタンスが1個あるはずだったけど、@Componentを付けていなかったからインスタンスがなかったということなんだと思います。

コントローラクラスのコンパイルは通っている

HomeControllerクラスにて、コンストラクタインジェクション方式でSampleServiceに対して@Autowiredアノテーションを付与していますが、これは特にコンパイルエラーになどにはなっていません。

パターン4の場合

パターン4では、コンポーネントスキャンの対象になっていないプロジェクトにクラスを作成した場合の動作確認をしてみます。
現在の設定では以下のように「com.my.app」プロジェクトがコンポーネントスキャンの対象プロジェクトになっているため、「com.my.demo」プロジェクトにDemoService.javaを作成してみます。

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

コントローラ側ではDemoService.javaをDIできるようにしておきます。

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.demo.DemoService;

@Controller
public class HomeController {

	public DemoService demoService;

	@Autowired
    public HomeController(DemoService demoService){
        this.demoService = demoService;
    }

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

		String result = demoService.execute();

		model.addAttribute("str", result );

		return "home";
	}
}

新規作成したDemoService.javaもインスタンスが生成されるように@Componentアノテーションを付与しておきます。

package com.my.demo;

import org.springframework.stereotype.Component;

@Component
public class DemoService {

	public String execute() {
		return "----DemoService----";
	}
}

実行結果

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

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.demo.DemoService]: : No matching bean of type [com.my.demo.DemoService] 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.demo.DemoService] 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.demo.DemoService] 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)

org.springframework.beans.factory.NoSuchBeanDefinitionException:依存関係に一致するタイプ[com.my.demo.DemoService]のBeanが見つかりません:この依存関係のautowire候補として適格な少なくとも1つのBeanが必要です。依存関係の注釈:{}

となりました。この文章だと、context:component-scanを直せばよいということが分かりづらいですね。

DIさせるには

今回作成した「com.my.demo」プロジェクトをDIの対象とするためには、servlet-context.xmlcontext:component-scan要素にプロジェクト名を追加すればOKです。 複数のプロジェクトを指定するには以下のようにカンマ(,)区切りでプロジェクトを指定します。

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

servlet-context.xmlの修正後、サーバを再起動すれば、以下のように起動が確認できます。

DIを使わずに正常に実行する方法

@Autowiredアノテーションを付与せず、自分で任意のタイミングでdemoService = new DemoService()を行うと、正常に実行することができます。

package com.my.app.ctrl;

import java.util.Locale;

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.demo.DemoService;

@Controller
public class HomeController {

	public DemoService demoService;

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

		demoService = new DemoService();
		String result = demoService.execute();

		model.addAttribute("str", result );

		return "home";
	}
}

実行結果

以下のとおり、画面上に「—-DemoService—-」という文字列が表示されました。
つまり、DIを使わずにインスタンスを呼び出すことができています。

issiki_wp