はじめに
前回の記事では、「なぜDIが必要なのか」を new をなくす3ステップで説明しました。
@Autowiredを書けばSpringが勝手にいい感じにしてくれる
——ここまでは分かった。でも、その「いい感じ」の中で、実際に いつ・どんな順番で インスタンスが作られて、注入されているのでしょうか?
@Component を付けたクラスが、なぜか自動的にどこかに登録され、@Autowired を付けた場所に、いつの間にか実体が入っている。この「いつの間にか」を分解するのが今回のテーマです。
対象読者:
- 前回記事(DIの基本)を読んだ人
-
@Autowiredの動くタイミングがよく分からない人 - 「Beanのライフサイクル」という言葉に馴染みがない人
1. DIコンテナの正体:ApplicationContext
これまで「DIコンテナ」と一括りに呼んでいましたが、Spring Frameworkにおいてその実体は ApplicationContext と呼ばれるオブジェクトです。
Spring Bootアプリケーションを起動すると、内部で自動的に ApplicationContext が1つ生成されます。これが、アプリ内の全ての @Component クラスの生成・管理・注入を一手に引き受ける「元締め」です。
DIコンテナがやっていることは、大きく分けると次の3フェーズです。
順番に見ていきましょう。
2. ① 収集フェーズ:誰が管理対象なのか調べる
アプリ起動時、Springはまず「どのクラスをDIコンテナで管理すべきか」を洗い出します。これを コンポーネントスキャン と呼びます。
@SpringBootApplication が付与されたクラスを起点に、同じパッケージ以下を走査し、@Component(や、それを内包する @Service @Repository @Controller など)が付いたクラスを探し出します。
このとき見つかったクラス1つひとつに対して、Springは「設計図」にあたる情報(Bean定義)を内部に作成します。この段階では、まだインスタンスは1つも作られていません。あくまで「これは後で管理すべきクラスだ」というリストアップだけが行われます。
前回の例で言えば、この段階で ClassA と ClassB が「Bean定義リスト」に載る、というイメージです。
3. ② 生成フェーズ:依存関係を解決しながらインスタンス化する
次にSpringは、リストアップしたBean定義をもとに、実際のインスタンスを生成していきます。
ここで重要なのが 生成順序 です。ClassA が ClassB に依存している場合、ClassA を先に作ってしまうと、注入すべき ClassB がまだ存在しないことになります。そのためSpringは、依存関係を解析し、依存されている側(ClassB)から先に生成する ように順序を組み立てます。
この「依存されている側から先に作る」という解決の連鎖があるため、Aがたくさんの依存を持っていたり、依存同士が複雑に絡み合っていたりすると、Springはグラフを辿るように生成順序を決めていきます。
ちなみに、
ClassAとClassBが互いを必要とし合っている状態を「循環参照(循環依存)」と呼びます。以前のSpringではフィールド注入を使うことでこれを強引に解決できましたが、設計の悪化を招くため、現在のSpring Boot(2.6以降)ではデフォルトでエラーになり起動しなくなりました。ここでも「コンストラクタ注入を使って、最初から循環依存が発生しないクリーンな設計にする」ことが推奨されています。
4. ③ 注入フェーズ:フィールドとコンストラクタでタイミングが違う
生成が終わったら、いよいよ注入です。ここで、前回のコラムで触れた「フィールド注入」と「コンストラクタ注入」の違いが効いてきます。
-
コンストラクタ注入:インスタンスを
newする(コンストラクタを呼ぶ)と同時に、依存先が渡される。つまり 生成と注入が同時に完了する。 -
フィールド注入:先にインスタンスを生成してから(
ClassAのからっぽの状態を作ってから)、あとからリフレクションという仕組みを使って、こっそりフィールドに値をねじ込む。
コンストラクタ注入だと「生成された時点で必要なものが全部揃っている」ことが保証されるのに対し、フィールド注入では「一瞬、依存先が空っぽの状態」が理論上存在します。実務でコンストラクタ注入が推奨される理由の一つは、実はこの注入タイミングの違いにもあります。
5. Beanは基本1つだけ:シングルトンスコープ
もう1つ知っておきたいのが、Beanの スコープ です。
DIコンテナで管理されるBeanは、特に指定しない限り シングルトン(singleton) として扱われます。つまり、アプリ内で ClassB のインスタンスは基本的に1つしか作られず、ClassB を必要とする全てのクラスに、同じインスタンス が使い回されます。
「注入されるたびに新しいインスタンスが作られる」と誤解しがちなポイントなので、要注意です(必要であれば @Scope("prototype") で毎回生成する挙動に変えることもできます)。
6. 全体の流れをおさらい
3フェーズを1枚の図にまとめると、こうなります。
@Autowired の「いつの間にか」の正体は、起動時にDIコンテナがこの一連の処理を黙々とこなしていた結果、というわけです。
まとめ
- DIコンテナの実体は
ApplicationContext。起動時に「収集→生成→注入」の3段階を行っている -
@Componentの収集では、まずBean定義(設計図)だけが作られる - インスタンス生成は「依存されている側」から順番に行われる
- コンストラクタ注入は生成と注入が同時、フィールド注入は生成後にあとから差し込まれる
- Beanは基本シングルトン。使い回されている前提を持っておく
「DIって結局newを肩代わりしてくれるだけでしょ?」の"肩代わり"の中身が、これで少し具体的になったのではないでしょうか。