Spring Bootでプロファイルごとに実装を切り替える

TL; DR

@Profile@ConditionalOnExpression を使う。ちょっとつらいけどがんばる。

サンプルコードは下記のとおり。

github.com

やりたいこと

システム日時を取得するユーティリティがあるが、テストのときに日時が毎回変わると困るので、特定の場合は固定値を返すようにしたい*1

TERASOLUNAの下記機能とほぼ同じ。ただしSpring Bootなので、XMLを使わずアノテーションでなんとかしたい。

terasolunaorg.github.io

環境

  • Spring Boot 2.5.0

解決方法

@Profile アノテーションを使用し、指定されたプロファイルに応じてインジェクションされる実装を切り替える。

例えば、インタフェース DateTimeUtils に対して本物のシステム日時を返す実装クラス SystemDateTimeUtils と、固定日時を返す実装クラス FixedDateTimeUtils を作るとする。

下記のような動作にしたい。

  • プロファイル with-fixeddatetime が指定されている場合は FixedDateTimeUtils を使う
  • そうでない場合は SystemDateTimeUtils を使う

f:id:ser1zw:20210606091828p:plain

固定日時を返す実装クラス 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つ以上あるんですけど???

f:id:ser1zw:20210606092559p:plain

下記のような動作にする。

  • プロファイルが with-fixeddatetime の場合は FixedDateTimeUtils を使う
  • プロファイルが with-adjusteddatetime の場合は AdjustedDateTimeUtils を使う
  • それ以外の場合は SystemDateTimeUtils を使う

AdjustedDateTimeUtils には @Profile("with-adjusteddatetime") を付与するとして…

@Component
@Profile("with-adjusteddatetime")
public class AdjustedDateTimeUtils implements DateTimeUtils {
    // ...

SystemDateTimeUtils@Profilewith-fixeddatetimewith-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を使ったほうが真っ当ではあるが、そうもいかないことが稀によくある。また、テスト以外でも状況に応じて挙動を切り替えたいこともたまにある。実行環境が異なる場合とか、バッチを特定の日付で実行したい場合とか…。