- 澁川喜規
- Programming
- 約 2,600 文字
- 14,100 View
SpringBootのDependency Injection(DI)は便利ですよね? 利用する側にコンストラクタインジェクションやら、フィールドインジェクションやらセッターインジェクションやらの形式で書いておくと、DIコンテナが勝手に実行時に対象となるクラスをもってきてインスタンスの生成をしてくれますし、インスタンスのライフサイクルをインジェクションされるクラス側に書けます。
|
実行時にDIしてくれるとはいっても、コンストラクタのパラメータとかクラスのフィールドはコンパイル前にソースコードにハードコードされてしまいます。本当に実行時に決まるような値をもとに動的に生成するクラスを変えたいという要件があったのでやり方を調べてみました。SpringBoot上だったので、汎用的な仕組みではなくてSpringBootの仕組みに乗っかるようにしています。
動的というのは、例えば、リクエストしてくるユーザーのランクを見て、VIPユーザー用ロジックを選択したい、みたいなストラテジーパターンです。ユーザーのランクの種類は動的に増えたりするのでハードコードしたくない、みたいな感じの要件だとします。
まずはロジックの登録機構
やはりDIするにはアノテーションですよね。 @UserRankアノテーションを作ります。1つだけ引数を持つ単一値アノテーションとします。で、アノテーションの合成を使って、@Componentもつけています。これは、このアノテーションをつけたら、即座にDI対象(別途@Serviceやら@Componentやら@Beanをつける必要はない)とするためのものです。
package com.example.annotations; |
String value()のところが引数で、valueという名前であれば、@UserRank("VIP")と書けますが、仮にtypeだと、@UserRank(type="VIP")とする必要があります。また、複数個パラメータをつけることも可能です。VIPになった勤続回数でも選択したいが、特別な場合以外は省略したい場合、int streak() default 0;みたいにdefaultをつけると省略可能になります。
これで、ロジック側に動的にフィルタリングするためのアノテーションが作れました。
実際にはアノテーションを使わないでも、特定インターフェースを実装しているものとか、クラス名でなどの条件でコンポーネントは探せるのですが、やはりアノテーションのほうがアスペクト指向っぽい感じで、ロジックの中身と探索ルールを切り離して表現できるのでアノテーションにしています。
動的に選択したいサービスクラスを実装する。
まずは実装につけるインターフェースを作っておきます。
package com.example; |
実装を2つ作ります。
package com.example.services; |
package com.example.services; |
コンポーネントをスキャンしてコンポーネントのマップを作る
ちょっと長いですが、コンポーネントのマップを作るクラスが以下の通りです。
ClassPathScanningCandidateComponentProviderクラスを使うと特定のパッケージ以下のクラス群を取得できます。取得時には名前とかいろいろフィルタが設定できるのですが、ここではUserRankというアノテーションがついているクラスを全取得しています。一度スキャンしたらその結果使いまわしたいのでstaticなMapに入れています。
package com.example; |
インスタンス作成機構の追加
インスタンス作成はSpringBootの提供するDIコンテナの機構を使います。そうすれば、サービスクラスが何かしらの外部依存を持っていてもそれのDIも一緒に行えます。
DIのファクトリーはBeanFactoryを使うのですが、これ自身はDIでインジェクションしてもらえばOKです。クラスの定義は前のコードで取得できていますので、あとは名前をもとにクラスをもってきて、beanFactory.getBean()を呼んでインスタンス化するだけです。
import org.springframework.beans.factory.BeanFactory; |
使ってみる
それではコントローラに組み込んでみます。
package com.example; |
はい。これで動的にインスタンスをとってきて実行するより動的なDIコンテナが実装できました。
もっと簡単な実装もあるよ
@ryushiさんに教えてもらった記事ではもっと簡単なやり方が書いてありました。
SpringBootのDIコンテナに自身に、コンポーネントに文字列で名前をつけて、その文字列を使ってインスタンス化を行う機構があります。
|
このようにDIのためのアノテーションに文字列をつけてあげて、getBean()にその名前を渡すと絞り込みを行ってくれるというものです(ちょっとサンプルを短くなるように改変しています)。
|
最初の実装も今回はサンプルのためにだいぶシンプル化していますが、要件としてはDI対象を探すロジックは本当はもうちょっと複雑で、マッチしなかったときのフォールバックとかもやろうと思っていたので、このシンプルな実装とは別のClassPathScanningCandidateComponentProviderでとってきたクラスリストをMapに入れて、いろいろな検索が行えるような実装にしました。
シンプルな機構で特殊な検索条件を実現するためには、アノテーションに入れるリテラルの名前のルールを作り、それを実装者が守る必要がありますが、たんなる文字列リテラルで複雑なルールを作ってもコンパイル時のチェックとかも効かないので不親切かなという点がネックでした。独自アノテーション作成時に@AliasForで親のアノテーションに値を渡すときにパラメータの加工とか合成とかいろいろコードが書ければ実現できそうでしたが、そういうのはできなそうでしたので。
まとめ
独立性の高いロジックをどかどか追加して、それを動的な条件をもとに選択して実行する、みたいなときに使える機構を作ってみました。
アノテーション単独での実装方法は調べるとすぐ出てきたのですが、その情報をもとにクラス一覧を取得してくるコード、またそのアノテーションの引数をもとにマップを作って、動的に選択してインスタンスを作るところなど、つなぎ合わせのサンプルは出てこなかったので、いろいろ調べながら書いてみました。
このようなロジックを実装したライブラリとかあるかもしれませんが、Javaのメタプログラミング的なところをいろいろ知れて楽しかったです。