SpringでDIした実装クラスの実行順序を制御する

公開日:2024-05-21
最終更新日:2024-05-21

Java

Spring-boot

概要

  • Springでインターフェースを実装した複数のクラスの実行順序をアノテーションで制御する
  • 同じインプットに対して複数の処理を直列に実行したい場合に便利

というお話。

はじめに

みなさん、Netflixのシティーハンター見ました?
私は見ました。控えめに言ってめちゃくちゃ良かったなと思いました。
鈴木亮平の演じる冴羽獠が完成度高すぎてビビりました。

ということで今日はみんな大好きGet Wildを歌おうと思います(唐突

本題

インターフェースを用意する

まずは複数の実装クラスを束ねるインターフェースを用意しましょう。

GetWild.java

public interface GetWild {

  void execute();
}

インターフェースを実装する

先ほど用意したインターフェースの実装クラスを用意し、 @Component を使ってDI登録しましょう。

GetWildAndToughAlone.java

@Component
public class GetWildAndToughAlone implements GetWild {

  @Override
  void execute() {
    System.out.println("Get wild and touch ひとりでは);
  }
}

CannotSolvePuzzle.java

@Component
public class CannotSolvePuzzle implements GetWild {

  @Override
  void execute() {
    System.out.println("解けない愛のパズルを抱いて");
  }
}

GetWildAndToughCity.java

@Component
public class GetWildAndToughCity implements GetWild {

  @Override
  void execute() {
    System.out.println("Get wild and touch この街で");
  }
}

DoNotWantSpoiled.java

@Component
public class DoNotWantSpoiled implements GetWild {

  @Override
  void execute() {
    System.out.println("やさしさに甘えていたくはない");
  }
}

実行してみる

全て同じインターフェースを実装しているので、DIする際には List でまとめてDIすることが可能になります。

GetWildTest.java

@SpringBootTest
public class GetWildTest {

  @Autowired
  private List<GetWild> getWilds;

  @Test
  void test() {
    getWilds.forEach(GetWild::execute);
  }
}

ただ実行結果を見ると、ちゃんと歌ってくれていません。

解けない愛のパズルを抱いて
やさしさに甘えていたくはない
Get wild and touch ひとりでは
Get wild and touch この街で

デフォルトの実行順序は辞書順

DIする側で List としてまとめてDIして、for-eachで直列実行できるのはコードの記述量が少なくて済むのでとても便利です。
が、 List にまとめられる際にどういう順番で詰められるのか?というと、答えはクラス名の辞書順になります。

つまり先ほどの実装クラスのクラス名を辞書順に並べると、

  1. CannotSolvePuzzle
  2. DoNotWantSpoiled
  3. GetWildAndToughAlone
  4. GetWildAndToughCity

という順番になるため、元の歌詞の順番で歌ってくれません。

@Orderで順序を制御する

この @Order というSpringのアノテーション、特にSpring Security周りを触ったことがある人には馴染みのあるアノテーションかと思います。
カスタムフィルターをSpring Securityのデフォルトのフィルターの前後に差し込んだりするのに使ったりします。

Javadoc

私自身、この発想を得たのがSpring Securityでの独自認証の実装をした経験からになります。

ということで先ほどの実装クラスに @Order を付与していきます。 指定する数値が小さい方が優先度が高い、つまり先に実行されることになります。

GetWildAndToughAlone.java

@Component
@Order(1) // 追加
public class GetWildAndToughAlone implements GetWild {

  @Override
  void execute() {
    System.out.println("Get wild and touch ひとりでは);
  }
}

CannotSolvePuzzle.java

@Component
@Order(2) // 追加
public class CannotSolvePuzzle implements GetWild {

  @Override
  void execute() {
    System.out.println("解けない愛のパズルを抱いて");
  }
}

GetWildAndToughCity.java

@Component
@Order(3) // 追加
public class GetWildAndToughCity implements GetWild {

  @Override
  void execute() {
    System.out.println("Get wild and touch この街で");
  }
}

DoNotWantSpoiled.java

@Component
@Order(4) // 追加
public class DoNotWantSpoiled implements GetWild {

  @Override
  void execute() {
    System.out.println("やさしさに甘えていたくはない");
  }
}

再実行してみよう

先ほどのテストクラスを再度実行してみましょう。

Get wild and touch ひとりでは
解けない愛のパズルを抱いて
Get wild and touch この街で
やさしさに甘えていたくはない

ちゃんとした歌詞の順番で歌ってくれています。

応用編

今回のサンプルでは引数なし・戻り値なしというシンプルなものにしましたが、冒頭にも書いた通り、同じインプットに対して複数の処理を直列実行することも可能です。

例えば適当なコンテキストオブジェクトを用意して、

public record SomeContext(
  String firstName,
  String lastName
) {}

インターフェースの引数をコンテキストオブジェクトにしてやれば、同じ引数に対して行うロジックをそれぞれ疎結合に分離しながらも、DIして使う際にまとめて扱うことができます。

public interface SomeInterface {

  void execute(SomeContext context);
}
public class HandleFirstName implements SomeInterface {

  @Override
  void execute(SomeContext context) {
    System.out.println(context.firstName());
  }
}
public class ConcatName implements SomeInterface {

  @Override
  void execute(SomeContext context) {
    System.out.println(context.firstName() + " " + context.lastName());
  }
}

さらにI/Oをジェネリクスを使って型消去をしてより汎用的にすることも可能ですし、ジェネリクスの境界値を使えば汎用的にしながらも実装する際にはある程度の型の縛りを入れることも可能になります。

まとめ

  • @Order を使うとSpringに登録されるクラスの優先順位(実行順序)を制御できるよ
  • 同一インターフェースを実装していれば List でまとめてDIできるよ
  • ContextパターンやCommandパターンと相性が良いかもしれないよ

©︎ s-kugel All Rights Reserved.