概要
SpringBootを触り始めた際、「DIコンテナは大事だよ!」と諸先輩方に教えていただき、
よーし、それじゃあがんばって理解するぞ!と意気込んだわけですが、
「なるほど!」と理解できるまで少し時間がかかったので、
もし同じ方がいらっしゃったら…ということで、
本記事で「DIコンテナって何?話も見えない!」の状態から
「なんとなく雰囲気はわかった!もっと詳しく調べてみる!」の状態になってもらえたら嬉しいなと思います。
必要知識
・ほんのすこーしだけJava
読者対象
・SpringBoot初学者
そもそもDIってなんぞ?
DIとは「Dependency Injection(依存性の注入)」のことです。
依存?注入?んんん?となった方、ご安心下さい。
サンプルコードを例にできるだけ簡単に説明します。
public class ClassA {
// フィールド
private ClassB classB;
// コンストラクタ
private ClassA(ClassB classB) {
this.classB = classB;
}
// メソッド
public void methodA() {
this.classB.methodB();
}
}
●依存性について
依存とは、「あるクラスが別のクラスをフィールドで持ち、そのメソッドを利用すること」です。
サンプルコードの例ですと、ClassAはClassBのインスタンスをフィールドに持っていて、
methodA()でClassBのmethodB()にアクセスしています。
もし、ClassBのmethodB()に引数を追加するとなると、
必然的にClassAのコードも変更しなければなりません。
これが「ClassAはClassBに依存している」ということになります。
●注入について
注入とは、「変数にインスタンスを代入すること」です。
サンプルコードの例ですと、コンストラクタでやっているこの部分ですね。
// コンストラクタ
private ClassA(ClassB classB) {
this.classB = classB;
}
コンストラクタやセッター等で初期化しておかないと、classBフィールドのmethodB()を呼び出した際にエラーが発生しちゃいますよね。
●依存性の注入
では「依存」と「注入」が理解できたところで「依存性の注入」のお話です。
広い意味では、依存性の注入とは「あるクラスが必要とするオブジェクトを、外部から渡してあげること」です。
さらに、インターフェースや抽象クラスを使うことで、注入するクラスを柔軟に切り替えられるようになります。
例えばJavaにはListというインターフェースがあります。
そのインターフェースを実装したクラスでなければ、List型の変数に代入することはできません。
Listを継承したArrayListやLinkedList等があります。
List<Object> listA = new ArrayList<>();
List<Object> listB = new LinkedList<>();
複数の実装クラスが存在する場合、以下の2つの処理を行う必要があります。
・どの実装クラスを使用するのか(依存)
・そのクラスのインスタンスを変数に入れる(注入)
つまりListにArrayListを注入するのか、LinkedListを注入するのかを決めます。
「依存」と「注入」を合わせて、「依存性の注入」ということになります。
●じゃあなぜインターフェースを使うのか?
もちろん色々と旨味はありますが、
注入するクラスの選択肢が増えることで開発、テスト、保守がしやすくなるからです。
// 実装クラスを切り替えることができる(疎結合)
List<Object> listA = new ArrayList<>();
// ArrayList以外に変更できない(密結合)
ArrayList<Object> listB = new ArrayList<>();
密結合の場合、テストする際にArrayListのデータしか使用できないですが、
疎結合の場合、Listさえ継承しておけばokなので捗りますよね。
ということで、依存性の注入っておいしいですよね、というところまではなんとなく理解していただけたと思います。
ここでようやっと本題に入っていきます。
●じゃあSpringのDIって何をしてるの?
SpringのDIは大きく3つの仕事をしています。
- DIの対象クラスを探す
- @Autowiredアノテーションが付与されている箇所にインスタンスを注入する
- インスタンスのライフサイクルの管理(生成と破棄)
そんな短く言われても…ということなので、詳しく説明していきます。
1. DIの対象クラスを探す
Spring起動時、DIの対象となるクラスを探します。
この処理のことを「コンポーネントスキャン」と言います。
コンポーネントスキャンでは、以下のアノテーションが付与されたクラスを探します。
@Component
@Controller
@Service
@Repository
@Configuration
@RestController
@ControllerAdvice
@ManagedBean
@Named
@Mapper
@Bean
対象クラスが見つかったら、そのクラスをDIコンテナに登録してくれます。
そうです、一言でいうと、
DIコンテナとは、「DI対象のクラスを管理してくれる入れ物」、ということになります。
ちなみにDIコンテナに登録されているクラスのことを「Bean」ともいいます。
2. @Autowiredアノテーションが付与されている箇所にインスタンスを注入する
イメージとしては以下の通りです。
// Springさんお仕事前
@Autowired
private ClassB classB;
// Springお仕事後
private ClassB classB = new DIContainer.getClassB();
補足:コンストラクタインジェクションについて
上記の例ではわかりやすさのためにフィールドに
@Autowiredを付ける方式(フィールドインジェクション)を紹介しましたが、
現在のSpring公式ではコンストラクタインジェクションが推奨されています。
Spring 4.3以降、コンストラクタが1つだけのクラスでは@Autowiredの記述すら省略可能です。@Service public class ClassA { private final ClassB classB; // コンストラクタが1つだけなら@Autowired不要 public ClassA(ClassB classB) { this.classB = classB; } }コンストラクタインジェクションには以下のメリットがあります。
・フィールドをfinalにできるので、不変性が保証される
・必須の依存が明確になる(コンストラクタの引数を見ればわかる)
・テスト時にnewするだけでモックを渡せるので、DIコンテナなしでもテストしやすい
3. インスタンスのライフサイクルの管理(生成と破棄)
Springは、インスタンスの生成から破棄までを責任をもって管理してくれます。
インスタンスの生成とはクラスをnewすることです。
破棄については、Springコンテナがシャットダウンする際に@PreDestroyなどのコールバックを通じて後片付けを行い、
その後ガベージコレクションがメモリからインスタンスを解放してくれます。
つまり、開発者がインスタンスの生成・破棄のタイミングを気にしなくていいのがポイントです。
そしてインスタンスの生存期間は「スコープ」で指定することができます。
スコープを指定するためには、クラスに@Scopeアノテーションを付けます。
@Controller
@Scope("request")
public class TestController {
// ...
}
Springが用意してくれている主なスコープは以下の通りです。
| スコープ | 説明 |
|---|---|
| singleton | Spring起動時にインスタンスを1つだけ生成します。アプリケーション全体で1つのインスタンスを共有して使用します。@Scopeアノテーションを付与しない場合は全てsingletonになります。 |
| prototype | Beanを取得するたびにインスタンスが毎回生成されます。 |
| session | HTTPのセッション単位でインスタンスが生成されます。つまり、ユーザーがログインしている間だけインスタンスが存在します。Webアプリケーションの場合のみ使用可能。 |
| request | HTTPのリクエスト単位でインスタンスが生成されます。Webアプリケーションの場合のみ使用可能。 |
| application | サーブレットのコンテキスト単位でインスタンスが生成されます。Webアプリケーションの場合のみ使用可能。 |
補足: 以前のSpringには
globalSessionスコープがありましたが、Spring 5以降ポートレットサポートが削除されたため、現在は使用できません。
※参考サイト
https://spring.pleiades.io/spring-framework/reference/core/beans/factory-scopes.html
結局DIのうまみって?
これもサンプルコードで見ていきます。
DIを使わない場合:
public class ClassA {
// クラス内部で直接newしている
private ClassB classB = new ClassB();
// メソッド
public void methodA() {
this.classB.methodB();
}
}
上記のようなコードがあったとしまして、このClassAをテストしようとします。
ただ、ClassA内で直接ClassBをnewしてしまっているので、ClassBをスタブやモックに変更することができません。
他にも、新しいクラスを作成してもそのクラスへの切替が手間です。
DIを使った場合:
@Service
public class ClassA {
private final ClassB classB;
// 外部からClassBを受け取る
public ClassA(ClassB classB) {
this.classB = classB;
}
public void methodA() {
this.classB.methodB();
}
}
// テスト時にはモックを注入できる!
ClassB mockB = new MockClassB();
ClassA classA = new ClassA(mockB);
DIを使っておけば、あら素敵、簡単にクラスの切り替えができる!
というところが、おいしいところの1つかなと思います。
最後に
本記事はあくまで取っ掛かり用として作成したので、
かなり端折っており、「もっとこういう機能もあるよ!こういうおいしいところもあるよ!」というものがあります。
ただいっぺんに色々言われても混乱しちゃうと思うので、
冒頭にも書いた通り、まずは本記事で雰囲気を掴んでいただけると嬉しいです。