Spring State Machineプロジェクトの手引き

Spring State Machineプロジェクトのガイド

1. 前書き

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

2. メーベン依存

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


    org.springframework.statemachine
    spring-statemachine-core
    1.2.3.RELEASE

この依存関係の最新バージョンはhereで見つかる可能性があります。

3. ステートマシン構成

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

@Configuration
@EnableStateMachine
public class SimpleStateMachineConfiguration
  extends StateMachineConfigurerAdapter {

    @Override
    public void configure(StateMachineStateConfigurer states)
      throws Exception {

        states
          .withStates()
          .initial("SI")
          .end("SF")
          .states(
            new HashSet(Arrays.asList("S1", "S2", "S3")));

    }

    @Override
    public void configure(
      StateMachineTransitionConfigurer 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つでは、イベントが現在の状態をどのように変更するかを定義します。

上記の構成は、非常にシンプルで直線的な遷移ステートマシンを設定します。

image

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

@Autowired
private StateMachine stateMachine;

ステートマシンを取得したら、開始する必要があります。

stateMachine.start();

マシンが初期状態になったので、イベントを送信してトランジションをトリガーできます。

stateMachine.sendEvent("E1");

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

stateMachine.getState();

4. 行動

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

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

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

@Override
public void configure(
  StateMachineTransitionConfigurer transitions)
  throws Exception {

    transitions.withExternal()
      transitions.withExternal()
      .source("SI").target("S1")
      .event("E1").action(initAction())

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

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

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

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

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

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

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

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

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

@Bean
public Action exitAction() {
    return ctx -> System.out.println(
      "Exit " + ctx.getSource().getId() + " -> " + ctx.getTarget().getId());
}
states
  .withStates()
  .stateEntry("S3", entryAction())
  .stateDo("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の状態を追跡するには、計算値、管理者からのエントリ、外部システムの呼び出しからの応答など、extended stateと呼ばれるものを使用する必要があります。 s。

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

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

7. 警備員

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

@Bean
public Guard 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 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 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 mediumGuard() {
    return ctx -> false;
}

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

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

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

最後の注意点は、the API provides both junctions and choices. However, functionally they are identical in every aspect.

11. Fork

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

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

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. Join

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

image

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

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

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

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

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

13. Stringsの代わりにEnums

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

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

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

public enum ApplicationReviewEvents {
    APPROVE, REJECT
}

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

public class SimpleEnumStateMachineConfiguration
  extends StateMachineConfigurerAdapter
  

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

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

14. 結論

この記事では、Springステートマシンの機能のいくつかを検討しました。

いつものように、サンプルのソースコードover on GitHubを見つけることができます。