Spring FrameworkはDIコンテナです。 古くはXMLでコンポーネントを定義していました。 こんな感じのXMLです。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="foo" class="com.example.Foo" />
<bean id="bar" class="com.example.Bar" />
<property name="foo" ref="foo" />
</bean>
</beans>アノテーションでもコンポーネントを定義できます。
package com.example;
import org.springframework.stereotype.Component;
@Component
public class Foo {
}package com.example;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Bar {
@Autowired
private Foo foo;
public Foo getFoo() {
return foo;
}
}また@Beanをメソッドに付ければ戻り値をコンポーネントとして定義できます。
これは例えばアノテーションを付けられないクラスをコンポーネントとして扱いたい場合に便利です。
package com.example;
public class Foo {
}package com.example;
public class Bar {
private final Foo foo;
public Bar(Foo foo) {
this.foo = foo;
}
public Foo getFoo() {
return foo;
}
}package com.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FooBarConfig {
@Bean
public Foo foo() {
return new Foo();
}
@Bean
public Bar bar() {
return new Bar(foo());
}
}現在はXMLよりもアノテーションでコンポーネント定義をするのが主流です。
コンポーネントには他のコンポーネントをインジェクションできます。 インジェクションの方法はいくつかあります。
まずコンストラクタインジェクション。 コンストラクタ引数がインジェクション対象となります。
package com.example;
import org.springframework.stereotype.Component;
@Component
public class Bar {
private final Foo foo;
public Bar(Foo foo) {
this.foo = foo;
}
}次にフィールドインジェクション。
インジェクション対象のフィールドに@Autowiredを付けます。
package com.example;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Bar {
@Autowired
private Foo foo;
}それからセッターインジェクション。
インジェクション対象のセッター(セッターじゃなくてもメソッドなら良さそうな感じはする)に@Autowiredを付けます。
package com.example;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Bar {
private Foo foo;
@Autowired
public void setFoo(Foo foo) {
this.foo = foo;
}
}Springチームはコンストラクタインジェクションを推奨しています。 私も次の理由からコンストラクタインジェクション派です。
- フィールドを
finalにできる - インジェクション対象が多すぎる状態を可視化できる(依存が多すぎる状態は良くない)
コンポーネントはスコープを持ちます。
| スコープ名 | 説明 | 私の感想 |
|---|---|---|
singleton |
コンテナ内で一つのインスタンス。デフォルトのスコープ。 | 一番よく使う |
prototype |
コンポーネントを要求するたびにインスタンス化される。 | 特に使いどころが思いつかない |
request |
一回のHTTPリクエストごとに一つのインスタンス。Webのスコープ。 | ログインユーザーの情報を表すときに使う |
session |
HTTPセッションごとに一つのインスタンス。Webのスコープ。 | あまり使わない |
globalSession |
sessionに似たスコープで、Portletで使うらしい |
使ったことない(Portletも使ったことない) |
application |
ServletContextごとに一つのインスタンス。Webのスコープ。 |
基本的に1アプリケーション1プロセスなのでそうするとsingletonと変わらない |
websocket |
WebSocketのセッションごとに一つのインスタンス。Webのスコープ。 | WebSocketを使う場合は使うかも(そもそもWebSocketを仕事で使ったことがない) |
スコープがsingleton、request、sessionのコンポーネントを定義する方法を記載します。
まずスコープについて何も指定しなければsingletonです。
package com.example;
import org.springframework.stereotype.Component;
@Component
public class Foo {
}requestはクラスに@RequestScopeを付けます。
package com.example;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;
@Component
@RequestScope
public class Bar {
}sessionはクラスに@SessionScopeを付けます。
package com.example;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.SessionScope;
@Component
@SessionScope
public class Baz {
}@Beanでコンポーネントを定義する場合もデフォルトはsingletonで、requestやsessionにしたいときはメソッドにアノテーションを付けます。
package com.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.annotation.RequestScope;
import org.springframework.web.context.annotation.SessionScope;
@Configuration
public class FooBarBazConfig {
@Bean
public Foo foo() {
return new Foo();
}
@Bean
@RequestScope
public Bar bar() {
return new Bar();
}
@Bean
@SessionScope
public Baz baz() {
return new Baz();
}
}SpringのコンテナはApplicationContextです。
ApplicationContextがコンポーネントを管理していて、getBeanメソッドなどで取り出すことができます。
※ただし、DIコンテナを使う場合はlookupはしないようにしましょう。 lookupは実行するその時までコンポーネントが存在するか分かりません。 また、依存対象がシグネチャに現れないのでソースコード上の検索がこんなんです。 injectionであればコンテナ起動時にコンポーネントの存在はチェックできますし、依存対象がシグネチャに現れます。
余談ですがコンテナの実体は他のDIコンテナでいうと、Java EEのCDIならBeanManager、Seasar2ならS2Container、Google GuiceならInjectorがそれに当たります。
ここまででSpringはDIコンテナであり、アノテーションでコンポーネント定義ができるということを開設しました。
Spring Bootは自動で適用されるコンポーネント定義の集合体です。
spring-boot-autoconfigureというJARにMETA-INF/spring.factoriesというファイルがあります。
これを開くと次のような記述があります。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
(以下略)
=の右辺にはコンポーネントを定義するクラスが,区切りで列挙されており、条件に合うと自動で読み込まれてコンポーネントが登録されます。
条件に合うと、と述べましたが条件とは何でしょうか。
例えばThymeleafAutoConfigurationを開いてみましょう。
クラス定義は次のようになっています。
@Configuration
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass(TemplateMode.class)
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {3行目の@ConditionalOnClassが条件です。
これはクラスパス上にTemplateModeというクラスがあれば、このThymeleafAutoConfigurationを読み込むということです。
| 条件アノテーション | 説明 |
|---|---|
@ConditionalOnClass |
クラスパス上に指定されたクラスがあればコンポーネント登録する |
@ConditionalOnBean |
指定されたコンポーネントがあればコンポーネント登録する |
@ConditionalOnMissingBean |
指定されたコンポーネントが無ければコンポーネント登録する(上書きを想定してデフォルト値を設定する時に使う) |
@Conditional |
value要素に設定するConditionを評価した結果に応じてコンポーネント登録する |
@Profile |
プロファイル(spring.profiles.active)に応じてコンポーネント登録する |
@Conditionalと@Profileを使えば割と手軽に環境の違いによってモックに差し替えるといったことができます。
Spring BootはWebやThymeleafをはじめとして様々なモジュールがあります。
artifactIdはspring-boot-starter-xxxとなっています(xxxにはwebやjdbc、securityなどが入る)。
それをpom.xmlに追加するだけで先に述べたAutoConfigurationによってコンポーネント登録がされて使用可能になります。
<!-- これだけでSpring MVCが使えるようになる -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>TODO @Configurationが付いたクラスはproxyになり@Beanが付いたメソッドの戻り値は登録されたコンポーネントが返るようになる
TODO @SpringBootApplicationについて
proxyに関連しているブログ書いた。http://backpaper0.github.io/2018/02/22/spring_proxy.html