Edited at

SpringのDIコンテナの動作イメージ(雰囲気)を掴もう

本エントリでは、SpringのDIコンテナがどんな感じで動作(オブジェクトの生成、依存オブジェクトの解決)しているのか?あたりを(ざっくり)紹介してみようかと思います。本エントリでは基本的には「DI」について知っている前提で記載するので、そもそも「DI」って何者?という方は、ネットや書籍などで「DI」について調べてみてください。

なお、本エントリの中で紹介するイラストの内容は、(正確性を求めると本エントリで紹介したいことの本質が損なわれる可能性があると判断し・・・)Springが行なっている実際の処理とは異なる部分があることをご了承頂きたいと思います。


DIという考え方を使わずにプログラムを書く

SpringのDIコンテナを使ったプログラムを書く前に、まずは・・・DIコンテナおよびDIという考え方を一切使わずに、複数のクラス(オブジェクト)を使ってプログラムを書くとどうなるか見てみましょう。

おそらく・・・以下のような感じになるでしょう。


  1. mainメソッド内でAクラスのオブジェクトを生成する

  2. Aクラスのfooメソッドを呼び出す

  3. Aクラスのfooメソッドの中でBクラスのオブジェクトを生成する

  4. Bクラスのbarメソッドを呼び出す

ごくごく普通のJavaプログラムですね。


DIの考え方を取り入れてプログラムを書く

次に・・・DIという考え方を取り入れてプログラムを書いてみます。具体的には、AクラスでBクラスのオブジェクトを生成してからメソッドを呼び出している部分(Bクラスへの依存性が発生している部分)にDIの考え方を取り入れてみます。


  1. mainメソッド内で(Aクラスが依存する)Bクラスのオブジェクトを生成する

  2. mainメソッド内でBクラスのオブジェクトをコンストラクタの引数に渡してAクラスのオブジェクトを生成する(=コンストラクタを使ったDI)

  3. Aクラスのfooメソッドを呼び出す

  4. Bクラスのbarメソッドを呼び出す

AクラスにDIの考え方を取り入れたことで、Aクラスの中でBクラスのオブジェクトを生成する処理がなくなりました。このような構成にすることで、AクラスはBクラスに加えてBクラスの子クラスのオブジェクトを使って処理を実行することができるようになります(例えば、Aクラスの単体テストを行う際に、Bクラスではなくテスト用のBクラスの子クラスを使うことができるようになります)。


NOTE:

本エントリでは「コンストラクタを使ったDI」をベースに記載していますが、DIの種類には「セッターメソッドを使ったDI」もあります。

コンストラクタとセッターの使い分けは、以下のような基準で行うのがよいかと思います(Springのリファレンスの受け売りですw)。


  • DIが必須のものはコンストラクタ

  • DIが任意のもの(デフォルトで利用するオブジェクトがありDIはオプション扱いのもの)はセッターメソッド

上記の使い分けを意識すると以下のような感じの利用イメージになります。

Bootstrapクラスを介して実行する場合は「Aオブジェクトに予め生成してあるBオブジェクト」をそのまま使い、テストクラスを介して実行する場合は「テスト用のBオブジェクト」をDIして(差し替えて)使うような構成にしてみました。

なお、本エントリでは紹介しませんが、Springには「フィールドへ直接DI」する仕組みも用意されています(最近の流れとしては「フィールドへのDI」は使わない方向になっているように感じますが、コード量が少なくて済むのは「フィールドへのDI」なので・・・ついつい/おもわず使ってしまうことがあるw)。



SpringのDIコンテナを使ってプログラムを書く

利用するクラスをDI可能なクラスに修正したところで、(お待ちかねの!?)SpringのDIコンテナを使ってプログラムを書くとどうなるかをみていきましょう。


Bean定義クラスの作成

プログラムを直す前に・・・プログラム内で利用するクラス(AクラスとBクラス)のオブジェクトを生成する部分を、Bean定義クラス(コンフィギュレーションクラス)化します。


DIコンテナを使うようにプログラムを修正

Bean定義クラスを作成したら、DIコンテナ(アプリケーションコンテキスト)を作成し、DIコンテナから実処理のエントリーポイントとなるクラスのBean(Aクラスのオブジェクト)を取得するようにプログラムを修正します。


DIコンテナを使った際の動作イメージ

SpringのDIコンテナを使う準備ができたところで、SpringのDIコンテナがどのような感じで動くのかをみておきましょう。


  1. DIコンテナ(アプリケーションコンテキスト)を生成する。 アプリケーションコンテキストは、コンストラクタの引数に渡したBean定義クラスの内容を読み取って、Bean(オブジェクト)を生成しDIコンテナ内で管理する。

  2. 依存性を持たないBクラスのBeanを生成するメソッドが呼び出される

  3. Bクラスのオブジェクトを生成する(DIコンテナに追加される)

  4. DIコンテナで管理されているBクラスのBeanを引数としてAクラスのBeanを生成するメソッドが呼び出される

  5. BクラスのBeanをコンストラクタの引数に渡してAクラスのオブジェクトを生成する(=コンストラクタを使ったDI)

  6. DIコンテナの生成完了後に、DIコンテナから実処理のエントリーポイントとなるクラス(Aクラス)のBeanを取得する

  7. 「DIコンテナから取得したAクラスのBean」のfooメソッドを呼び出す

  8. 「DIコンテナによってAクラスのBean」にDIされた「BクラスのBean」のbarメソッドを呼び出す


コンポーネントスキャン機能を使ってBean定義を行う

SpringのDIコンテナを使うようにプログラムは修正しましたが、実アプリでは使うクラスが2つだけということはほぼなく・・・もっと大量のクラスを使うことになります。その際に・・・Bean定義クラス(図中:MyConfigクラス)にBeanを生成するためのメソッドを定義するのは辛そうですよね!?

もしBean定義が辛いと思った方は、コンポーネントスキャン機能を使うことでBean定義を簡略化することができます。具体的には・・・


  • コンポーネントスキャン機能を有効化するためにBean定義クラスに「@ComponentScan」を指定 ※必要に応じて属性を指定

  • スキャン対象にしたいクラスに「@Component」(or @Serviceなど@Componentが付与されているアノテーション)を指定

してください。

全体的な流れはコンポーネントスキャン機能を使わない時と同じですが、Beanを生成するプロセス(図中の②〜④あたりのプロセス)が異なります。コンポーネントスキャン機能を使う場合は、スキャンしたクラスのメタデータ(クラスアノテーション、メソッドアノテーションなど)からBean定義を決定してBeanを生成します。


サンプルプログラム

多少内容は異なりますが、本エントリで紹介した内容のサンプルを以下のリポジトリに格納しておきました。


まとめ

今回は、SpringのDIコンテナを使った時の動作イメージを紹介してみました。本エントリで紹介した内容はSpringのDIコンテナの全体像のほんの一部分(さわりの部分)であり、DIコンテナが提供する機能を有効的に活用するためには「Beanのスコープ」「Beanの優先順位」「初期化処理/破棄処理」「AOP(Proxy)」「プロファイル」などの仕組みに関する知識も必要になってきますが、本エントリを読むことでSpringのDIコンテナを使ったアプリケーション開発の第一歩を踏み出してもらえれば幸いです :grin:

実際にはDIコンテナの仕組みを知らなくても「見様見真似」でアプリケーションの開発はできちゃいますが :sweat_smile:・・・、(どんなことでも)基礎もちゃんと押さえておかないと「真似するものがない」時にフリーズしてしまうエンジニアになってしまう可能性があります。目先の「成果」も大事ですが(優先しなくてはいけないこともありますが)、自分が作っているプログラムがアプリケーションとしてどのように動作するのか(=動作原理)を理解しておくことは非常に大事だと思います。とりあえず動いているけどどうやって動いているの今一かわかっていない(=モヤモヤしている)のであれば、一旦立ち止まって基礎を学んでそのモヤモヤを取り除くことで、今まで見えなかったこと(気がつくことができなかったことが)が見えてくると思います。