Spring Bootでプロファイルごとに実装を切り替える
TL; DR
@Profile
や @ConditionalOnExpression
を使う。ちょっとつらいけどがんばる。
サンプルコードは下記のとおり。
やりたいこと
システム日時を取得するユーティリティがあるが、テストのときに日時が毎回変わると困るので、特定の場合は固定値を返すようにしたい*1。
TERASOLUNAの下記機能とほぼ同じ。ただしSpring Bootなので、XMLを使わずアノテーションでなんとかしたい。
環境
- Spring Boot 2.5.0
解決方法
@Profile
アノテーションを使用し、指定されたプロファイルに応じてインジェクションされる実装を切り替える。
例えば、インタフェース DateTimeUtils
に対して本物のシステム日時を返す実装クラス SystemDateTimeUtils
と、固定日時を返す実装クラス FixedDateTimeUtils
を作るとする。
下記のような動作にしたい。
- プロファイル
with-fixeddatetime
が指定されている場合はFixedDateTimeUtils
を使う - そうでない場合は
SystemDateTimeUtils
を使う
固定日時を返す実装クラス FixedDateTimeUtils
では、@Profile("with-fixeddatetime")
を付与する。
@Component @Profile("with-fixeddatetime") public class FixedDateTimeUtils implements DateTimeUtils { // ...
本物のシステム日時を返す実装クラス SystemDateTimeUtils
では、@Profile("!with-fixeddatetime")
を付与する。
@Profile
でのプロファイル指定はSpring 式言語 (SpEL)が使える。!with-fixeddatetime
で「with-fixeddatetime
以外」となる。
@Component @Profile("!with-fixeddatetime") public class SystemDateTimeUtils implements DateTimeUtils { // ...
プロファイルの指定には、下記のような方法がある。
application.properties
で指定
spring.profiles.active=with-fixeddatetime
@SpringBootTest @ActiveProfiles("with-fixeddatetime") class DemoApplicationWithFixedDateTimeTests { // ...
その他、実行時の引数や環境変数での指定なども可能。
参考:2.6. アクティブ Spring プロファイルを設定する | Spring Boot 「使い方」ガイド - リファレンスドキュメント
@Profile("!with-fixeddatetime")
って書かないといけないのはつらい
つらいけどしょうがない。
本当は @Profile("!with-fixeddatetime")
って書かなくても with-fixeddatetime
じゃなければデフォルトで @Profile
無しのものを使ってほしいところ。
しかし @Profile
を付与しない場合は「どのプロファイルでも常に使われる」という動作になるらしい。今回の例で SystemDateTimeUtils
から @Profile
を外すと、with-fixeddatetime
プロファイルを指定した場合に下記エラーとなる。
*************************** APPLICATION FAILED TO START *************************** Description: Field dateTimeUtils in com.example.demo.service.impl.DemoServiceImpl required a single bean, but 2 were found: - fixedDateTimeUtils: defined in file [/path/to/springboot-profile-switch-demo/target/classes/com/example/demo/util/impl/FixedDateTimeUtils.class] - systemDateTimeUtils: defined in file [/path/to/springboot-profile-switch-demo/target/classes/com/example/demo/util/impl/SystemDateTimeUtils.class] Action: Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
@Profile("default")
だと、default
プロファイルでない場合に使われないので、これもNG。
@ComponentScan
でコンポーネントスキャンの対象を変更することでの対応もできそうだが、ちょっと大掛かりではある。
実装クラスが3つ以上あるんですけど???
下記のような動作にする。
- プロファイルが
with-fixeddatetime
の場合はFixedDateTimeUtils
を使う - プロファイルが
with-adjusteddatetime
の場合はAdjustedDateTimeUtils
を使う - それ以外の場合は
SystemDateTimeUtils
を使う
AdjustedDateTimeUtils
には @Profile("with-adjusteddatetime")
を付与するとして…
@Component @Profile("with-adjusteddatetime") public class AdjustedDateTimeUtils implements DateTimeUtils { // ...
SystemDateTimeUtils
の @Profile
は with-fixeddatetime
と with-adjusteddatetime
の両方を除外するために、@Profile("!with-fixeddatetime && !with-adjusteddatetime")
みたいになる。つらい。
@Component @Profile("!with-fixeddatetime && !with-adjusteddatetime") public class SystemDateTimeUtils implements DateTimeUtils { // ...
SpELは正規表現もサポートされているので、@ConditionalOnExpression
を使用して「with-〜
にマッチしない」という指定もできる。プロファイル名をルール化できる場合は、この方法がよいかもしれない。
@Component @ConditionalOnExpression("#{ not ('${spring.profiles.active}' matches '^with-.+') }") public class SystemDateTimeUtils implements DateTimeUtils { // ...
この場合、JUnitテストクラスでは @TestPropertySource
でプロファイルを指定する必要がある。あまりちゃんと調べられていないけど、@ActiveProfiles("プロファイル名")
だと spring.profiles.active
に値が設定されず、@ConditionalOnExpression
が意図通りに動かない(Environment#getProperty("spring.profiles.active")
が null
になる)。
@SpringBootTest @TestPropertySource(properties = "spring.profiles.active=with-fixeddatetime") class DemoApplicationWithFixedDateTimeTests { // ...
SpELについては、下記のドキュメントを見るとよい。
4. Spring 式言語 (SpEL) | Spring Framework コアテクノロジー - リファレンス
演算子については下記の部分を参照。
4.3.7. 演算子 | Spring Framework コアテクノロジー - リファレンス
参考
*1:テストだけであればMockitoを使ったほうが真っ当ではあるが、そうもいかないことが稀によくある。また、テスト以外でも状況に応じて挙動を切り替えたいこともたまにある。実行環境が異なる場合とか、バッチを特定の日付で実行したい場合とか…。