自作のDIコンテナ
このレッスンでは、DI コンテナの正体がとても素朴な対応表であることを、自分で書いて確かめられるようになります。
ApplicationContext は巨大な連想配列です
Spring の ApplicationContext は、名前と型からインスタンスを引ける対応表だと考えると、ほとんど正しく理解できます。起動時にコンポーネントスキャンと @Bean メソッドの実行で表を埋め、あとは要求に応じて引くだけです。
Java
ApplicationContext ctx = SpringApplication.run(Application.class, args);
OrderService service = ctx.getBean(OrderService.class);getBean がやっているのは、表から引いて返すことです。魔法があるとすれば、表を埋める過程のほうにあります。
表を埋めるときに再帰が起きます
OrderService を作るには Sender が要り、Sender の実装が Config を必要とするなら、先に Config を作らなければなりません。つまり生成の手順は次のようになります。
- 要求された型のコンストラクタを見る
- 引数の型を1つずつ取り出す
- その型の Bean をまだ作っていなければ、同じ手順で作りにいく
- 全部そろったらコンストラクタを呼ぶ
- できたインスタンスを表に入れる
3の「同じ手順で作りにいく」が再帰です。依存の木を葉から根に向かって解決していきます。前のレッスンで見た循環依存が起動時に検出できるのは、この再帰の途中で「いま作っている最中の型をもう一度作ろうとしている」と分かるからです。
一度作ったら使い回します
5で表に入れているのが効いています。次に同じ型が要求されたときは、生成の手順に入らず表から返します。この「一度だけ作って共有する」振る舞いが、既定のスコープである singleton の正体です。次のレッスンで扱います。
| コンテナの仕事 | 実体 |
|---|---|
| Bean の登録 | 対応表への put |
| Bean の取得 | 対応表からの get |
| 依存の解決 | 引数の型を辿る再帰 |
| singleton | 生成済みなら表から返す |
Spring はどこで型を知るのか
Spring は実行時にリフレクションでコンストラクタの引数の型を読み取ります。この講座の実行環境ではリフレクションを使う複雑な処理までは組み立てられないので、演習では「名前で登録して名前で取り出す」形に単純化します。型による解決も、内部では同じ表を型で引いているだけです。
Java
// Spring の getBean の 2 つの入口
ctx.getBean("orderService"); // 名前で引く
ctx.getBean(OrderService.class); // 型で引く名前で引く場合、既定の名前はクラス名の先頭を小文字にしたものになります。@Bean メソッドで登録したものはメソッド名が名前になります。型で引く場合、その型に代入できる Bean が2つ以上あると、どれを返せばよいか決まらないので例外になります。前のレッスンで触れた @Primary や @Qualifier は、この曖昧さを解くための道具でした。表を引くという単純な仕組みだからこそ、鍵が重複したときの扱いを人間が決めてやる必要がある、と考えると腑に落ちます。
起動時にまとめて作られます
singleton の Bean は、最初に必要になった時点ではなく、コンテナの起動時にまとめて生成されます。設定の書き間違いや依存の欠落を、リクエストが来る前に洗い出すためです。起動が数秒かかる代わりに、起動さえ通れば依存関係は全部そろっていることが保証されます。あとから遅らせたい場合に @Lazy を付けます。
演習で書くこと
Map を内側に持つ小さなコンテナを実装します。名前でインスタンスを登録し、名前で取り出し、取り出したものを使って処理した結果を返します。ここまで書ければ、ApplicationContext に対して抱いていた漠然とした畏れは、かなり小さくなるはずです。
要件
- Container は内部に Map を持ち、register と getBean の 2 つのメソッドを持つこと
- getBean は未登録の名前に対して null を返すこと
- run は en / ja / fr の 3 つを登録したうえで名前で引き、未登録なら NoSuchBeanDefinition: xxx を返すこと
入出力例
run("en", "taro") → "Hello, taro"
run("ja", "taro") → "こんにちは taro"
run("fr", "hanako") → "Bonjour, hanako"
run("de", "taro") → "NoSuchBeanDefinition: de"