DIコンテナに蓄えられた Bean/コンポーネント(以降、Bean で統一します)を自由度高く Injection して使える Spring Boot ですが、Bean を使い分ける方法にはいくつか選択肢があります。
その選択肢を段階を踏まえて説明したいと思います。
前提
今回のサンプルは次の前提としています
- Bean のライフサイクルはシングルトン
- Bean の指定方法は @Component で固定(読者の方の利用状況に応じて @Service や @Controller といったもので読み替えてもらえたら)
- サンプルの例での injection の指定方法は、コンストラクタインジェクション(Spring 4.3以降、@autowired を省略できるので)
- 動作検証は Spring Boot 2.6.4 + Java 11
コードで静的に使い分ける
型で使い分ける
これはある意味、普通に injection しているだけの段階でやれていることではありますが
@Component
public class Component1 {
// 略
}
@Component
public class Component2 {
// 略
}
という Bean は
@Component
public class ProxyService {
private final Component1 component1;
private final Component2 component2;
// 型でそれぞれ決定されて DI される
public ProxyService(Component1 component1, Component2 component2){
// メンバへの設定等は略
}
}
という記述で injection されて Component1 と 2 の使い分けができます。
(例では簡単に紹介するために 1 と 2 を両方 injection されるようにしていますが、通常はそれぞれ使いたい場面でどちらか一つだけを指定することになると思います)
name で使い分ける
よくあるパターンですが、次のように型(interface)を同じにして、その interface の実装を使い分けたい場合ではどうでしょうか?
@Component
public class SimpleImplementation implements CommonInterface {
// 略
}
@Component
public class ComplexImplementation implements CommonInterface {
// 略
}
その場合には定義側では Bean に name をつけてあげて
@Component("simple")
public class SimpleImplementation implements CommonInterface {
// 略
}
@Component("complex")
public class ComplexImplementation implements CommonInterface {
// 略
}
利用する側では
@Component
public class ProxyService {
private final CommonInterface simpleImplementation;
private final CommonInterface complexImplementation;
// name でそれぞれ決定されて DI される
public ProxySercie(@Qualifier("simple")CommonInterface simpleImpl,
CommonInterface complex){
// メンバへの設定等は略
}
}
でそれぞれを区別して injection させることができます。
(2つ目の引数の complex のように、name と変数名が一致する場合は Qualifier 無しでも解決してくれます)
備考1
method で Bean を返す場合については https://ik.am/entries/377 などを参照して下さい。
この記事の例でもありますが、name を明示的に使わずにデフォルトの Bean name(name のつけられ方については https://www.baeldung.com/spring-bean-names を参照)でも指定できますが、name を使った方が丁寧でトラブルが少ないと思います。
とはいえ、デフォルトの Bean name (この例でいえばクラス名)が name 相当な識別性あるものだったり、ある種の割り切りをするならば name 省略でも良いとは思います。
さらにえいば、
name と変数名が一致する場合は Qualifier 無しでも解決してくれます
なので、極端には name, Qualifier 両方の指定すら省略するこもできます。
備考2
lombok での Qualifier 指定の記述方法は https://stackoverflow.com/questions/38549657/is-it-possible-to-add-qualifiers-in-requiredargsconstructoronconstructor を参照してください
起動時に動的に使い分ける(↑ との差は起動時に動的に変更できるかどうか)
プロファイル(@Profile + @Primary)で使い分ける
Spring Boot は、起動時に profile を指定することで、様々なリソースの切り替えが出来るようになっています。
これを DI にも活用ができます。
上記では複数の同じインタフェースの Bean から name で指定した一つだけを明示的に選ぶ方法を紹介しましたが、
基本的にある場面でどちらを使うかが「起動時に」決まる場合には @Profile を使った次のような方法が取れます。
Bean 側では次のようにある Profile 固有で使う Bean は @Profile + @Primary を指定します
@Primary
@Component
@Profile("simple")
public class SimpleImplementation implements CommonInterface {
// 略
}
@Primary
@Component
@Profile("complex")
public class ComplexImplementation implements CommonInterface {
// 略
}
一方、それ以外(プロファイル指定なしや、上記以外)の起動指定時に使いたい Bean には @Profile + @Primary を指定しません。
@Component
public class DefaultImplementation implements CommonInterface {
// 略
}
一方 CommonInterface を利用する側では
@Component
public class ProxyService {
private final CommonInterface anyImplementation;
// Profile で決定された Bean が CommonInterface 側には DI される
public ProxySercie(CommonInterface anyImplementation){
// メンバへの設定等は略
}
}
でどれか一つだけを優先的に injection させることができます。
こうすることで
Profile指定 | Center align |
---|---|
simple | SimpleImplemantation |
complex | ComplexImplemantation |
それ以外 or 未指定 | DefaultImplemantation |
というように起動時に動的に実装の使い分けができます。
備考 1
Profile まわりについては
- https://qiita.com/suke_masa/items/98b4c1b562ea6ec89bf7
- https://scior.hatenablog.com/entry/2019/03/20/003058
といった記事を参照してください
備考 2
上記の例では Profile の指定を一つの場合だけで紹介しましたが、起動時の Profile は複数指定できる(-Dspring.profiles.active=prof1,prof2 といった形)ので、何パターンかの組み合わせができることはできます。
ただ、あまり複雑な使い分けの用途には向いてないので、例えばデバッグ機能の有効化とか、付加的なものの使い分けに向いていると思います。
設定ファイル(+@ConditionalOnProperty)で使い分ける
上記備考 2 辺りでも触れましたが、Profile は起動時に決まる動作環境の大区分として使うにはいいですが、細かい設定向けには向いていないですし、そもそも Profile では決まらないその内部での設定となると使えません。
そうい場合には設定ファイル(application.yml など)と @ConditionalOnProperty を組み合わせた方法が使えます。
Bean 側では @ConditionalOnProperty を使って利用条件を設定しておき、
@Component
@ConditionalOnProperty(prefix = "app", name = "use_type", havingValue = "simple", matchIfMissing = true)
public class SimpleImplementation implements CommonInterface {
// 略
}
@Component
@ConditionalOnProperty(prefix = "app", name = "use_type", havingValue = "complex")
public class ComplexImplementation implements CommonInterface {
// 略
}
設定ファイル、例えば application.yml なら
app:
use_type: "simple"
とすれば Simple 側の実装が有効になり、
この use_type を complex に指定すれば Complex 側の実装が有効になります。
(ちなみに、matchIfMissing=true 指定をしているので、use_type が未設定な場合は Simple 側)
備考 1
application.yml はプロファイル別にファイルを分けて定義をできますので、それと組み合わせると起動プロファイルに合わせたコンポーネントの使い分けを yml or properties でより詳細で自由度高く定義できることができるようになります。
備考 2
ちなみに、matchIfMissing=true 指定をしているので、use_type が未設定な場合は Simple 側
これは use_type が空設定ではなく、未設定(設定が存在しない)の場合だけなので注意が必要です。
例えば、空設定含めた simple/complex 以外の値を use_type が持つ場合は、どの Bean も対応せず起動時のエラーとなります。
ですので、完全に排他的にしたい場合は、片方は @Primary + @ConditionalOnProperty 、もう一方は @Primary も @ConditionalOnProperty も指定せずにデフォルト扱いにする、といった風にするという手があります。
あるいは、@Primary + @ConditionalOnProperty を2つ、デフォルトとして @Primary も @ConditionalOnProperty も指定しない Bean を用意するといった先に Profile の時にも使った3値的な対処方法もあると思います。
いずれにせよどう使い分けたいかという目的次第かと思います。
備考 3
ここでは一番使い勝手の良さそうな @ConditionalOnProperty を紹介しましたが、@ConditionalOn~ という形で様々な 条件による Bean を有効化するかどうかのアノテーションが用意されています。
特に @ConditionalOnExpression は SpEL で OnProperty より複雑な条件が書けますし、Spring の中でもよく使われている(らしい) Bean の依存関係(あっちが有効 or 無効ならこっちも有効)が定義できる @ConditionalOnBean / @ConditionalOnMissingBean などは使い勝手が良いと思います。
詳しくは
- https://reflectoring.io/spring-boot-conditionals/
- https://qiita.com/kazuki43zoo/items/8645d9765edd11c6f1dd#spring-boot%E3%81%8C%E6%8F%90%E4%BE%9B%E3%81%99%E3%82%8Bconditional%E3%81%AE%E5%90%88%E6%88%90%E3%82%A2%E3%83%8E%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3
などを参照してください。
起動後に動的に使い分ける、但し、ある程度自力で頑張って…(↑ との差は起動後の実行中に動的に変更できるかどうか)
設定を DB 等で持っていたりで動作を変えたいとか、(ユーザ指定・設定とかで)処理毎に使いたい動作が違うとかの場合には、Bean の切り替えを実行中に(再起動なしに)動的にしたくなりますが、これは Spring Boot の用意されている手段ではちょうどよいのが無い気がします。
ここはある程度自力で頑張って
@Component
public class SimpleImplementation implements CommonInterface {
// CommonInterface で定義された種別取得用のメソッド
@override
public UseType getType {
return UseType.SIMPLE;
}
// 略
}
@Component
public class ComplexImplementation implements CommonInterface {
@override
public UseType getType {
return UseType.COMPLEX;
}
// 略
}
@Component
public class ProxyService {
private final List<CommonInterface> commonInterfaces;
// CommonInterface の全 Bean が List として DI される
public ProxySercie(List<CommonInterface> commonInterfaces){
// メンバへの設定等は略
}
}
として、実際に使う場面では CommonInterface#getType() が返す値と動作条件で実装を使い分けるといった方法があると思います。
最後に
なるべく網羅的にしたつもりですが、私の知見ベースなところもあり、漏れがあるとは思います。
もしこういう方法もあるよという場合、コメントを頂けるとありがたいです。