本記事は、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つに当てはまらない場合に指定するアノテーション。
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は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は@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では、@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では、コンポーネントスキャンの対象になっていないプロジェクトにクラスを作成した場合の動作確認をしてみます。
現在の設定では以下のように「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を直せばよいということが分かりづらいですね。
今回作成した「com.my.demo」プロジェクトをDIの対象とするためには、servlet-context.xmlのcontext:component-scan要素にプロジェクト名を追加すればOKです。
複数のプロジェクトを指定するには以下のようにカンマ(,)区切りでプロジェクトを指定します。
<context:component-scan base-package="com.my.app,com.my.demo" />
servlet-context.xmlの修正後、サーバを再起動すれば、以下のように起動が確認できます。
@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を使わずにインスタンスを呼び出すことができています。