1. 序章

この記事は、Springのステートマシンプロジェクトに焦点を当てています。これは、ワークフローやその他の種類の有限状態オートマトン表現の問題を表すために使用できます。

2. Mavenの依存関係

開始するには、メインのMaven依存関係を追加する必要があります。

<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-core</artifactId>
    <version>3.2.0.RELEASE</version>
</dependency>

この依存関係の最新バージョンは、ここにあります。

3. ステートマシン構成

それでは、簡単なステートマシンを定義することから始めましょう。

@Configuration
@EnableStateMachine
public class SimpleStateMachineConfiguration 
  extends StateMachineConfigurerAdapter<String, String> {

    @Override
    public void configure(StateMachineStateConfigurer<String, String> states) 
      throws Exception {
 
        states
          .withStates()
          .initial("SI")
          .end("SF")
          .states(
            new HashSet<String>(Arrays.asList("S1", "S2", "S3")));

    }

    @Override
    public void configure(
      StateMachineTransitionConfigurer<String, String> transitions) 
      throws Exception {
 
        transitions.withExternal()
          .source("SI").target("S1").event("E1").and()
          .withExternal()
          .source("S1").target("S2").event("E2").and()
          .withExternal()
          .source("S2").target("SF").event("end");
    }
}

このクラスは、従来のSpring構成およびステートマシンとして注釈が付けられていることに注意してください。 また、さまざまな初期化メソッドを呼び出すことができるように、StateMachineConfigurerAdapterを拡張する必要があります。 構成方法の1つでは、ステートマシンのすべての可能な状態を定義し、もう1つでは、イベントが現在の状態をどのように変更するかを定義します。

上記の構成は、非常に単純で直線的な遷移状態マシンを示しています。これは、従うのに十分簡単なはずです。

次に、Springコンテキストを開始し、構成で定義されたステートマシンへの参照を取得する必要があります。

@Autowired
private StateMachine<String, String> stateMachine;

ステートマシンを入手したら、それを開始する必要があります。

stateMachine.start();

マシンが初期状態になっているので、イベントを送信して遷移をトリガーできます。

stateMachine.sendEvent("E1");

ステートマシンの現在の状態をいつでも確認できます。

stateMachine.getState();

4. 行動

状態遷移の周りに実行されるいくつかのアクションを追加しましょう。 まず、同じ構成ファイルでアクションをSpringBeanとして定義します。

@Bean
public Action<String, String> initAction() {
    return ctx -> System.out.println(ctx.getTarget().getId());
}

次に、遷移に関する上記で作成したアクションを構成クラスに登録できます。

@Override
public void configure(
  StateMachineTransitionConfigurer<String, String> transitions)
  throws Exception {
 
    transitions.withExternal()
      transitions.withExternal()
      .source("SI").target("S1")
      .event("E1").action(initAction())

このアクションは、イベントE1を介してSIからS1への遷移が発生したときに実行されます。 アクションは、州自体に関連付けることができます。

@Bean
public Action<String, String> executeAction() {
    return ctx -> System.out.println("Do" + ctx.getTarget().getId());
}

states
  .withStates()
  .state("S3", executeAction(), errorAction());

この状態定義関数は、マシンがターゲット状態にあるときに実行される操作と、オプションでエラーアクションハンドラーを受け入れます。

エラーアクションハンドラーは他のアクションと大差ありませんが、状態のアクションの評価中に例外がスローされた場合に呼び出されます。

@Bean
public Action<String, String> errorAction() {
    return ctx -> System.out.println(
      "Error " + ctx.getSource().getId() + ctx.getException());
}

entry do 、およびexit状態遷移の個々のアクションを登録することもできます。

@Bean
public Action<String, String> entryAction() {
    return ctx -> System.out.println(
      "Entry " + ctx.getTarget().getId());
}

@Bean
public Action<String, String> executeAction() {
    return ctx -> 
      System.out.println("Do " + ctx.getTarget().getId());
}

@Bean
public Action<String, String> exitAction() {
    return ctx -> System.out.println(
      "Exit " + ctx.getSource().getId() + " -> " + ctx.getTarget().getId());
}
states
  .withStates()
  .stateEntry("S3", entryAction())
  .state("S3", executeAction())
  .stateExit("S3", exitAction());

それぞれのアクションは、対応する状態遷移で実行されます。 たとえば、入力時にいくつかの前提条件を確認したり、終了時にいくつかのレポートをトリガーしたりすることができます。

5. グローバルリスナー

ステートマシンに対してグローバルイベントリスナーを定義できます。 これらのリスナーは、状態遷移が発生するたびに呼び出され、ロギングやセキュリティなどに利用できます。

まず、別の構成方法を追加する必要があります。これは、状態や遷移を処理せず、ステートマシン自体の構成を処理する方法です。

StateMachineListenerAdapter を拡張して、リスナーを定義する必要があります。

public class StateMachineListener extends StateMachineListenerAdapter {
 
    @Override
    public void stateChanged(State from, State to) {
        System.out.printf("Transitioned from %s to %s%n", from == null ? 
          "none" : from.getId(), to.getId());
    }
}

ここでは、 stateChanged のみをオーバーライドしますが、他の多くのフックも使用できます。

6. 拡張状態

Spring State Machineはその状態を追跡しますが、 application の状態を追跡するには、計算値、管理者からのエントリ、外部システムの呼び出しからの応答など、と呼ばれるものを使用する必要があります。 ]拡張状態

アカウント申請が2つのレベルの承認を通過することを確認したいとします。 拡張状態で保存された整数を使用して、承認数を追跡できます。

@Bean
public Action<String, String> executeAction() {
    return ctx -> {
        int approvals = (int) ctx.getExtendedState().getVariables()
          .getOrDefault("approvalCount", 0);
        approvals++;
        ctx.getExtendedState().getVariables()
          .put("approvalCount", approvals);
    };
}

7. 警備員

ガードを使用して、状態への遷移が実行される前に一部のデータを検証できます。 ガードはアクションと非常によく似ています。

@Bean
public Guard<String, String> simpleGuard() {
    return ctx -> (int) ctx.getExtendedState()
      .getVariables()
      .getOrDefault("approvalCount", 0) > 0;
}

ここでの顕著な違いは、ガードがtrueまたはfalseを返し、遷移の発生を許可するかどうかをステートマシンに通知することです。

ガードとしてのSPeL式のサポートも存在します。 上記の例は、次のように書くこともできます。

.guardExpression("extendedState.variables.approvalCount > 0")

8. ビルダーからのステートマシン

StateMachineBuilder を使用すると、Springアノテーションを使用したり、Springコンテキストを作成したりせずに、ステートマシンを作成できます。

StateMachineBuilder.Builder<String, String> builder 
  = StateMachineBuilder.builder();
builder.configureStates().withStates()
  .initial("SI")
  .state("S1")
  .end("SF");

builder.configureTransitions()
  .withExternal()
  .source("SI").target("S1").event("E1")
  .and().withExternal()
  .source("S1").target("SF").event("E2");

StateMachine<String, String> machine = builder.build();

9. 階層状態

階層状態は、複数の withStates() parent()と組み合わせて使用して構成できます。

states
  .withStates()
    .initial("SI")
    .state("SI")
    .end("SF")
    .and()
  .withStates()
    .parent("SI")
    .initial("SUB1")
    .state("SUB2")
    .end("SUBEND");

この種のセットアップでは、ステートマシンが複数の状態を持つことができるため、 getState()を呼び出すと複数のIDが生成されます。 たとえば、起動直後は次の式になります。

stateMachine.getState().getIds()
["SI", "SUB1"]

10. ジャンクション(選択肢)

これまで、本質的に線形である状態遷移を作成してきました。 これはかなり面白くないだけでなく、開発者が実装を求められる実際のユースケースも反映していません。 オッズは条件付きパスを実装する必要があることであり、Springステートマシンのジャンクション(または選択肢)により、まさにそれを実行できます。

まず、状態定義で状態をジャンクション(選択)としてマークする必要があります。

states
  .withStates()
  .junction("SJ")

次に、遷移で、if-then-else構造に対応するfirst / then/lastオプションを定義します。

.withJunction()
  .source("SJ")
  .first("high", highGuard())
  .then("medium", mediumGuard())
  .last("low")

firstthenは、2番目の引数を取ります。これは、どのパスを取るかを見つけるために呼び出される通常のガードです。

@Bean
public Guard<String, String> mediumGuard() {
    return ctx -> false;
}

@Bean
public Guard<String, String> highGuard() {
    return ctx -> false;
}

遷移はジャンクションノードで停止しませんが、定義されたガードをすぐに実行し、指定されたルートの1つに移動することに注意してください。

上記の例では、ステートマシンにSJに移行するように指示すると、両方のガードがfalseを返すため、実際の状態はlowになります。

最後の注意点は APIは、ジャンクションと選択肢の両方を提供します。 ただし、機能的にはすべての面で同一です。

11. フォーク

実行を複数の独立した実行パスに分割する必要がある場合があります。 これは、フォーク機能を使用して実現できます。

まず、ノードをフォークノードとして指定し、ステートマシンが分割を実行する階層領域を作成する必要があります。

states
  .withStates()
  .initial("SI")
  .fork("SFork")
  .and()
  .withStates()
    .parent("SFork")
    .initial("Sub1-1")
    .end("Sub1-2")
  .and()
  .withStates()
    .parent("SFork")
    .initial("Sub2-1")
    .end("Sub2-2");

次に、フォーク遷移を定義します。

.withFork()
  .source("SFork")
  .target("Sub1-1")
  .target("Sub2-1");

12. 加入

フォーク操作の補完は結合です。 これにより、他のいくつかの状態の完了に依存する状態遷移を設定できます。

フォークと同様に、状態定義で結合ノードを指定する必要があります。

states
  .withStates()
  .join("SJoin")

次に、遷移で、結合状態を有効にするために完了する必要のある状態を定義します。

transitions
  .withJoin()
    .source("Sub1-2")
    .source("Sub2-2")
    .target("SJoin");

それでおしまい! この構成では、Sub1-2Sub2-2の両方が達成されると、ステートマシンはSJoinに移行します。

13. 列挙型文字列の代わりに

上記の例では、文字列定数を使用して状態とイベントを定義し、明確さと単純さを実現しています。 実際の本番システムでは、Javaの列挙型を使用して、スペルミスを回避し、型の安全性を高めることができます。

まず、システムで考えられるすべての状態とイベントを定義する必要があります。

public enum ApplicationReviewStates {
    PEER_REVIEW, PRINCIPAL_REVIEW, APPROVED, REJECTED
}

public enum ApplicationReviewEvents {
    APPROVE, REJECT
}

また、構成を拡張するときに、列挙型を汎用パラメーターとして渡す必要があります。

public class SimpleEnumStateMachineConfiguration 
  extends StateMachineConfigurerAdapter
  <ApplicationReviewStates, ApplicationReviewEvents>

定義したら、文字列の代わりに列挙型定数を使用できます。 たとえば、遷移を定義するには:

transitions.withExternal()
  .source(ApplicationReviewStates.PEER_REVIEW)
  .target(ApplicationReviewStates.PRINCIPAL_REVIEW)
  .event(ApplicationReviewEvents.APPROVE)

14. 結論

この記事では、Springステートマシンのいくつかの機能について説明しました。

いつものように、サンプルのソースコードはGitHubにあります。