1前書き

この記事は、Springのhttp://projects.spring.io/spring-statemachine/State Machineプロジェクト]に焦点を当てています。これは、ワークフローやその他の種類の有限状態オートマトン表現問題を表現するために使用できます。


2 Mavenの依存関係

はじめに、Mavenの主要な依存関係を追加する必要があります。

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

この依存関係の最新バージョンはhttps://search.maven.org/classic/#search%7C1%7Cg%3A%22org.springframework.statemachine%22%20AND%20a%3A%22spring-statemachine-にあります。 core%22[ここ]。


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

上記の設定は、従うことが十分に簡単であるべきである非常に単純な、直線的な遷移状態機械を提示します。

リンク:/uploads/simple.png%20880w[]

それでは、Springコンテキストを起動し、設定で定義されたステートマシンへの参照を取得する必要があります。

@Autowired
private StateMachine<String, String> stateMachine;

ステートマシンが完成したら、起動する必要があります。

stateMachine.start();

私たちのマシンは初期状態になったので、イベントを送信して遷移を引き起こすことができます。

stateMachine.sendEvent("E1");

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

stateMachine.getState();


4行動

状態遷移に関連して実行されるアクションをいくつか追加しましょう。まず、同じ設定ファイルでSpring Beanとしてアクションを定義します。

@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())
  .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

状態を追跡するためには、計算値、管理者からのエントリ、外部システム呼び出しからの応答など、いわゆる拡張状態__を使用する必要があります。

アカウントの申請が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階層の状態

階層状態は、

parent()

とともに複数の

withStates()

を使用して構成できます。

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 State Machineのジャンクション(または選択)を使用すると、それを実現できます。

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

[source,java,gutter:,true]

states
.withStates()
.junction(“SJ”)

それから、トランジションでは、if-then-else構造に対応するfirst/then/lastオプションを定義します。

[source,java,gutter:,true]

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

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

[source,java,gutter:,true]

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

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

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

上の例では、ステートマシンにSJへの移行を指示すると、実際の状態は__low__になります。両方のガードがfalseを返すだけです。

最後の注意は、**  APIがジャンクションと選択の両方を提供するということです。

ただし、機能的にはそれらはすべての点で同一です。

===  **  11フォーク**

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

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

[source,java,gutter:,true]

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”);

それからフォーク遷移を定義します。

[source,java,gutter:,true]

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

===  **  12. 参加する**

fork操作の補足はjoinです。それは私たちがいくつかの他の状態を完成することに依存している状態への移行を設定することを可能にします:

リンク:/uploads/forkjoin.png%201044w[]

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

[source,java,gutter:,true]

states
.withStates()
.join(“SJoin”)

それから、トランジションでは、参加状態を有効にするためにどの状態を完了する必要があるかを定義します。

[source,java,gutter:,true]

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

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

===  **  13.  __文字列** の代わりに__Enums__

上記の例では、明確さと単純さのために状態とイベントを定義するために文字列定数を使用しました。実際のプロダクションシステムでは、スペルミスを避け、より安全な型を得るためにJavaのenumを使用したいと思うかもしれません。

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

[source,java,gutter:,true]

public enum ApplicationReviewStates {
PEER

REVIEW, PRINCIPAL

REVIEW, APPROVED, REJECTED
}

public enum ApplicationReviewEvents {
APPROVE, REJECT
}

設定を拡張するときにも、列挙型を一般的なパラメータとして渡す必要があります。

[source,java,gutter:,true]

public class SimpleEnumStateMachineConfiguration
extends StateMachineConfigurerAdapter
<ApplicationReviewStates, ApplicationReviewEvents>

一度定義すれば、文字列の代わりにenum定数を使うことができます。例えば遷移を定義するには:

[source,java,gutter:,true]

transitions.withExternal()
.source(ApplicationReviewStates.PEER

REVIEW)
.target(ApplicationReviewStates.PRINCIPAL

REVIEW)
.event(ApplicationReviewEvents.APPROVE)

===  **  14. 結論**

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

いつものように、サンプルソースコードhttps://github.com/eugenp/tutorials/tree/master/spring-state-machine[over on GitHub]を見つけることができます。