これは、なにをしたくて書いたもの?
最近Spring Statemachineに関する勉強をちょっとずつしていましたが、そろそろこれでひと区切りにしようかなと思います。
今回は、チョイスとジャンクションを扱ってみます。
チョイス/ジャンクション
チョイスとジャンクションは、「疑似ステート」の一種です。
用語集には以下のように紹介されています。
- Choice State
- たとえばイベントのヘッダーやステート変数といったものに基づき、遷移の選択を行える疑似ステート
- Junction State
- チョイスによく似た疑似ステートで、チョイスではひとつの入力遷移のみ許可するが、ジャンクションは複数の入力遷移を許可する
Spring Statemachine / Appendices / Appendix B: State Machine Concepts / Glossary
ジャンクションは複数の入力遷移を〜とありますが、ソースコードを見ていると本当だろうか…?という気はしますが。
クラッシュコースにも説明があります。
こちらには、ガードを使いif/elseif/else構造を実現するものだ、と書かれています。また、定義とガードの評価結果、いずれかの
遷移が選択されるようにしておかないとステートマシンがデッドロックすることが注意事項として書かれています。
ステートマシン対して、チョイスとジャンクションを定義する説明はこちらに記載があります。
ここでも、やはりガードとともに使ってif/elseif/else構造を定義できることが解説されています。
そしてジャンクションの説明を見ると、チョイスとジャンクションはコードレベルではほぼ同じ機能であることが書かれています。
チョイスを例にして、ドキュメントからコード例を読み取ってみましょう。
@Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.SI) .choice(States.S1) .end(States.SF) .states(EnumSet.allOf(States.class)); } @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withChoice() .source(States.S1) .first(States.S2, s2Guard()) .then(States.S3, s3Guard()) .last(States.S4); }
StateConfigurer#choice
およびChoiceTransitionConfigurer#source
で、同じステートを指定する必要があります。
first
、then
、last
がif
/else if
/else
構造にそれぞれ対応します。first
およびthen
にはガードを組み合わせ、最初にtrue
を返したステートに
遷移します。then
は複数回定義できます。first
、then
のいずれもtrue
になるケースがなかった場合は、last
で指定したステートに
遷移します。
後続の遷移を決める際には、チョイスの結果がどの遷移になってもいいようにステートの遷移を定義する必要があります。遷移の途切れた
箇所があってそこに遷移してしまった場合は、ステートマシンが終わらなくなってしまいます。
といっても、今回は使う時にはそこまで網羅的には定義しませんが。ソースコードがちょっと長くなってしまうので。
では、試していってみましょう。
環境
今回の環境は、こちら。
$ java --version openjdk 17.0.4 2022-07-19 OpenJDK Runtime Environment (build 17.0.4+8-Ubuntu-120.04) OpenJDK 64-Bit Server VM (build 17.0.4+8-Ubuntu-120.04, mixed mode, sharing) $ mvn --version Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 17.0.4, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-126-generic", arch: "amd64", family: "unix"
Spring Bootプロジェクトを作成する
Spring Bootプロジェクトを作成します。
$ curl -s https://start.spring.io/starter.tgz \ -d bootVersion=2.6.7 \ -d javaVersion=17 \ -d name=statemachine-choice-junction \ -d groupId=org.littlewings \ -d artifactId=statemachine-choice-junction \ -d version=0.0.1-SNAPSHOT \ -d packageName=org.littlewings.spring.statemachine \ -d baseDir=statemachine-choice-junction | tar zxvf -
Spring Bootが2.6.7なのは、ドキュメントに記載のバージョンと合わせているからです。
Spring Statemachine / Getting started / Using Maven
プロジェクト内に移動。
$ cd statemachine-choice-junction
自動生成されたソースコードは、今回は削除しておきます。
$ rm src/main/java/org/littlewings/spring/statemachine/StatemachineChoiceJunctionApplication.java src/test/java/org/littlewings/spring/statemachine/StatemachineChoiceJunctionApplicationTests.java
Maven依存関係など。
<properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
このうち、spring-boot-starter
をspring-statemachine-starter
に変更します。
<dependencies> <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-starter</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
続いて、ソースコードを作成していきます。
ステートを定義したenum
。
src/main/java/org/littlewings/spring/statemachine/States.java
package org.littlewings.spring.statemachine; public enum States { INITIAL_STATE, STATE1, STATE1_1, STATE1_2, STATE1_3, STATE1_4, STATE2, STATE2_1, STATE2_2, STATE3, STATE3_1, STATE3_2, STATE3_3, STATE3_4, END_STATE }
枝番のようにしているもの(_
が入っているもの)は、チョイスやジャンクションで使います。
イベントを定義したenum
。
src/main/java/org/littlewings/spring/statemachine/Events.java
package org.littlewings.spring.statemachine; public enum Events { EVENT1, EVENT2, EVENT3, EVENT4 }
ステートマシンの定義。
src/main/java/org/littlewings/spring/statemachine/StateMachineConfig.java
package org.littlewings.spring.statemachine; import java.time.LocalDateTime; import java.util.EnumSet; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; import org.springframework.statemachine.guard.Guard; @Configuration @EnableStateMachine public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception { config .withConfiguration() .autoStartup(true) .machineId("my-statemachine"); } @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.INITIAL_STATE) .choice(States.STATE1) .choice(States.STATE2) .junction(States.STATE3) .end(States.END_STATE) .states(EnumSet.allOf(States.class)); } @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.INITIAL_STATE).event(Events.EVENT1).target(States.STATE1) .action(loggingAction()) .and() // choice .withChoice() .source(States.STATE1) .first(States.STATE1_1, falseGuard(), loggingAction()) .then(States.STATE1_2, falseGuard(), loggingAction()) .then(States.STATE1_3, trueGuard(), loggingAction()) .last(States.STATE1_4, loggingAction()) .and() // normal .withExternal() .source(States.STATE1_3).event(Events.EVENT2).target(States.STATE2) .action(loggingAction()) .and() // choice .withChoice() .source(States.STATE2) .first(States.STATE2_1, falseGuard(), loggingAction()) .last(States.STATE2_2, loggingAction()) .and() // normal .withExternal() .source(States.STATE2_2).event(Events.EVENT3).target(States.STATE3) .action(loggingAction()) .and() // junction .withJunction() .source(States.STATE3) .first(States.STATE3_1, falseGuard(), loggingAction()) .then(States.STATE3_2, falseGuard(), loggingAction()) .then(States.STATE3_3, trueGuard(), loggingAction()) .last(States.STATE3_4, loggingAction()) .and() // normal .withExternal() .source(States.STATE3_3).event(Events.EVENT4).target(States.END_STATE) .action(loggingAction()); } @Bean public Guard<States, Events> trueGuard() { return context -> { System.out.printf( "[%s] true guard, stage = %s, source = %s, state = %s, pseudo state = %s, trigger type = %s, event = %s%n", LocalDateTime.now(), context.getStage(), context.getSource().getId(), context.getTarget().getId(), context.getTarget().getPseudoState().getKind(), context.getTransition().getTrigger() != null ? context.getTransition().getTrigger().getClass().getSimpleName() : "[no trigger]", context.getMessage() != null ? context.getMessage().getPayload() : "[none]" ); return true; }; } @Bean public Guard<States, Events> falseGuard() { return context -> { System.out.printf( "[%s] false guard, stage = %s, source = %s, state = %s, pseudo state = %s, trigger type = %s, event = %s%n", LocalDateTime.now(), context.getStage(), context.getSource().getId(), context.getTarget().getId(), context.getTarget().getPseudoState().getKind(), context.getTransition().getTrigger() != null ? context.getTransition().getTrigger().getClass().getSimpleName() : "[no trigger]", context.getMessage() != null ? context.getMessage().getPayload() : "[none]" ); return false; }; } @Bean public Action<States, Events> loggingAction() { return stateContext -> System.out.printf( "[%s] state action, stage = %s, source = %s, state = %s, pseudo state = %s, trigger type = %s, event = %s%n", LocalDateTime.now(), stateContext.getStage(), stateContext.getSource().getId(), stateContext.getTarget().getId(), stateContext.getTarget().getPseudoState().getKind(), stateContext.getTransition().getTrigger() != null ? stateContext.getTransition().getTrigger().getClass().getSimpleName() : "[no trigger]", stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); } }
チョイスとジャンクションの部分、そしてガードの部分は実行結果と合わせて見ていくことにします。
ステートマシンを使うクラス。
src/main/java/org/littlewings/spring/statemachine/Runner.java
package org.littlewings.spring.statemachine; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.messaging.support.MessageBuilder; import org.springframework.statemachine.StateMachine; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; @Component public class Runner implements ApplicationRunner { StateMachine<States, Events> stateMachine; public Runner(StateMachine<States, Events> stateMachine) { this.stateMachine = stateMachine; } @Override public void run(ApplicationArguments args) throws Exception { System.out.println("send event EVENT1"); stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT1).build())) .blockFirst(); System.out.println("send event EVENT2"); stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT2).build())) .blockFirst(); System.out.println("send event EVENT3"); stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT3).build())) .blockFirst(); System.out.println("send event EVENT4"); stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT4).build())) .blockFirst(); System.out.printf("StateMachine complete? %b%n", stateMachine.isComplete()); } }
イベントを同期的に送りますが、その前にこれからイベントを送ることをログ出力しています。
main
クラス。
src/main/java/org/littlewings/spring/statemachine/App.java
package org.littlewings.spring.statemachine; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } }
動かしてみる
では、動かしてみます。
$ mvn spring-boot:run
こんなログが出力されました。
send event EVENT1 [2022-09-30T23:59:50.438003981] state action, stage = TRANSITION, source = INITIAL_STATE, state = STATE1, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT1 [2022-09-30T23:59:50.439263641] false guard, stage = STATE_CHANGED, source = INITIAL_STATE, state = STATE1, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT1 [2022-09-30T23:59:50.439656433] false guard, stage = STATE_CHANGED, source = INITIAL_STATE, state = STATE1, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT1 [2022-09-30T23:59:50.439885672] true guard, stage = STATE_CHANGED, source = INITIAL_STATE, state = STATE1, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT1 [2022-09-30T23:59:50.440655575] state action, stage = STATE_CHANGED, source = INITIAL_STATE, state = STATE1, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT1 send event EVENT2 [2022-09-30T23:59:50.448411104] state action, stage = TRANSITION, source = STATE1_3, state = STATE2, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT2 [2022-09-30T23:59:50.448991593] false guard, stage = STATE_CHANGED, source = STATE1_3, state = STATE2, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT2 [2022-09-30T23:59:50.449371375] state action, stage = STATE_CHANGED, source = STATE1_3, state = STATE2, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT2 send event EVENT3 [2022-09-30T23:59:50.451593945] state action, stage = TRANSITION, source = STATE2_2, state = STATE3, pseudo state = JUNCTION, trigger type = EventTrigger, event = EVENT3 [2022-09-30T23:59:50.452477335] false guard, stage = STATE_CHANGED, source = STATE2_2, state = STATE3, pseudo state = JUNCTION, trigger type = EventTrigger, event = EVENT3 [2022-09-30T23:59:50.452786880] false guard, stage = STATE_CHANGED, source = STATE2_2, state = STATE3, pseudo state = JUNCTION, trigger type = EventTrigger, event = EVENT3 [2022-09-30T23:59:50.453079612] true guard, stage = STATE_CHANGED, source = STATE2_2, state = STATE3, pseudo state = JUNCTION, trigger type = EventTrigger, event = EVENT3 [2022-09-30T23:59:50.453582227] state action, stage = STATE_CHANGED, source = STATE2_2, state = STATE3, pseudo state = JUNCTION, trigger type = EventTrigger, event = EVENT3 send event EVENT4 [2022-09-30T23:59:50.455591432] state action, stage = TRANSITION, source = STATE3_3, state = END_STATE, pseudo state = END, trigger type = EventTrigger, event = EVENT4 StateMachine complete? true
チョイス、ジャンクションともに動いているのですが、このログをソースコードと合わせて説明していきます。
今回作成したステートマシンの定義を確認する
今回作成したステートマシンの定義と、ログを見ながらどのような定義、動作になるかを確認していきます。
まず、チョイスにしてもジャンクションにしても条件分岐になるので、ガードを定義します。
@Bean public Guard<States, Events> trueGuard() { return context -> { System.out.printf( "[%s] true guard, stage = %s, source = %s, state = %s, pseudo state = %s, trigger type = %s, event = %s%n", LocalDateTime.now(), context.getStage(), context.getSource().getId(), context.getTarget().getId(), context.getTarget().getPseudoState().getKind(), context.getTransition().getTrigger() != null ? context.getTransition().getTrigger().getClass().getSimpleName() : "[no trigger]", context.getMessage() != null ? context.getMessage().getPayload() : "[none]" ); return true; }; } @Bean public Guard<States, Events> falseGuard() { return context -> { System.out.printf( "[%s] false guard, stage = %s, source = %s, state = %s, pseudo state = %s, trigger type = %s, event = %s%n", LocalDateTime.now(), context.getStage(), context.getSource().getId(), context.getTarget().getId(), context.getTarget().getPseudoState().getKind(), context.getTransition().getTrigger() != null ? context.getTransition().getTrigger().getClass().getSimpleName() : "[no trigger]", context.getMessage() != null ? context.getMessage().getPayload() : "[none]" ); return false; }; }
今回は、シンプルにtrue
またはfalse
を決め打ちで返すガードにしています。
ステートの変化を見るために、ログ出力を行うアクションも定義。
@Bean public Action<States, Events> loggingAction() { return stateContext -> System.out.printf( "[%s] state action, stage = %s, source = %s, state = %s, pseudo state = %s, trigger type = %s, event = %s%n", LocalDateTime.now(), stateContext.getStage(), stateContext.getSource().getId(), stateContext.getTarget().getId(), stateContext.getTarget().getPseudoState().getKind(), stateContext.getTransition().getTrigger() != null ? stateContext.getTransition().getTrigger().getClass().getSimpleName() : "[no trigger]", stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); }
まず、StateConfigurer#choice
およびStateConfigurer#junction
で、遷移を条件分岐するステートを指定します。
@Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.INITIAL_STATE) .choice(States.STATE1) .choice(States.STATE2) .junction(States.STATE3) .end(States.END_STATE) .states(EnumSet.allOf(States.class)); }
次に、StateMachineTransitionConfigurer
を使用した遷移の定義を行います。
@Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.INITIAL_STATE).event(Events.EVENT1).target(States.STATE1) .action(loggingAction()) .and() // choice .withChoice() .source(States.STATE1) .first(States.STATE1_1, falseGuard(), loggingAction()) .then(States.STATE1_2, falseGuard(), loggingAction()) .then(States.STATE1_3, trueGuard(), loggingAction()) .last(States.STATE1_4, loggingAction()) .and() // normal .withExternal() .source(States.STATE1_3).event(Events.EVENT2).target(States.STATE2) .action(loggingAction()) .and() // choice .withChoice() .source(States.STATE2) .first(States.STATE2_1, falseGuard(), loggingAction()) .last(States.STATE2_2, loggingAction()) .and() // normal .withExternal() .source(States.STATE2_2).event(Events.EVENT3).target(States.STATE3) .action(loggingAction()) .and() // junction .withJunction() .source(States.STATE3) .first(States.STATE3_1, falseGuard(), loggingAction()) .then(States.STATE3_2, falseGuard(), loggingAction()) .then(States.STATE3_3, trueGuard(), loggingAction()) .last(States.STATE3_4, loggingAction()) .and() // normal .withExternal() .source(States.STATE3_3).event(Events.EVENT4).target(States.END_STATE) .action(loggingAction()); }
この部分は、ふつうの遷移の定義ですね。EVENT1
をトリガーにして、INITIAL_STATE
からSTATE1
に遷移します。
.withExternal() .source(States.INITIAL_STATE).event(Events.EVENT1).target(States.STATE1) .action(loggingAction()) .and()
イベントを送り込んだら、すぐに遷移が行われます。
System.out.println("send event EVENT1");
そして、STATE1
に遷移したらこちらをソースにしてfirst
/then
/then
/last
の条件分岐を定義しています。
if
、else if
、else if
、else
が並んでいる状態ですね。
// choice
.withChoice()
.source(States.STATE1)
.first(States.STATE1_1, falseGuard(), loggingAction())
.then(States.STATE1_2, falseGuard(), loggingAction())
.then(States.STATE1_3, trueGuard(), loggingAction())
.last(States.STATE1_4, loggingAction())
.and()
定義の開始がStateMachineTransitionConfigurer#withChoice
になっているところがポイントです。
今回はfirst
から見て、false
→ false
→ true
となっているので、この分岐はSTATE1_3
に遷移します。
ログを見ると、イベントを送り込んだ後にすぐさまチョイスの遷移に移っています。
※通常の遷移の部分とチョイスの部分がわかりやすいように、改行を入れています
send event EVENT1 [2022-09-30T23:59:50.438003981] state action, stage = TRANSITION, source = INITIAL_STATE, state = STATE1, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT1 [2022-09-30T23:59:50.439263641] false guard, stage = STATE_CHANGED, source = INITIAL_STATE, state = STATE1, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT1 [2022-09-30T23:59:50.439656433] false guard, stage = STATE_CHANGED, source = INITIAL_STATE, state = STATE1, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT1 [2022-09-30T23:59:50.439885672] true guard, stage = STATE_CHANGED, source = INITIAL_STATE, state = STATE1, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT1 [2022-09-30T23:59:50.440655575] state action, stage = STATE_CHANGED, source = INITIAL_STATE, state = STATE1, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT1
この部分が連続で動いている感じですね。
.withExternal()
.source(States.INITIAL_STATE).event(Events.EVENT1).target(States.STATE1)
.action(loggingAction())
.and()
// choice
.withChoice()
.source(States.STATE1)
.first(States.STATE1_1, falseGuard(), loggingAction())
.then(States.STATE1_2, falseGuard(), loggingAction())
.then(States.STATE1_3, trueGuard(), loggingAction())
.last(States.STATE1_4, loggingAction())
.and()
ガードの評価は、true
を返したところで停止するようです。
完全にif
文的な動きですね。
再掲しますが、StateMachineTransitionConfigurer#withChoice
に続く`ChoiceTransitionConfigurer#source
に指定したステートは、
StateMachineStateConfigurer#choice
でも指定しておく必要があることに注意です。
@Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.INITIAL_STATE) .choice(States.STATE1) .choice(States.STATE2) .junction(States.STATE3) .end(States.END_STATE) .states(EnumSet.allOf(States.class)); .and() }
これはチョイスでもジャンクションでも同じです。以降はあらためてこの対称性を説明しません。
次はふつうの遷移の定義。EVENT2
をトリガーにして、STATE1_3
からSTATE2
に遷移します。
// normal
.withExternal()
.source(States.STATE1_3).event(Events.EVENT2).target(States.STATE2)
.action(loggingAction())
.and()
ひとつ前の遷移の定義が、STATE1_3
になっていることを前提にしています。
なお、今回はステートSTATE1_3
からの遷移しか定義していませんが、その前のチョイスに定義されているfirst
やthen
、last
の評価結果、
STATE1_3
以外のステートに遷移した場合は対応する遷移の定義がないためステートマシンが終了しなくなります。
本来は分岐先のステートのパターンに対して、網羅的に遷移を定義する必要があることに注意しましょう。
続いてのチョイスの定義。今回はfirst
/last
のみにしました。if
とelse
だけが定義されている状態です。
// choice
.withChoice()
.source(States.STATE2)
.first(States.STATE2_1, falseGuard(), loggingAction())
.last(States.STATE2_2, loggingAction())
.and()
first
はfalse
を返すガードを指定しているので、last
が有効になりSTATE2_2
に遷移することになります。
そして、このような動作に。
send event EVENT2 [2022-09-30T23:59:50.448411104] state action, stage = TRANSITION, source = STATE1_3, state = STATE2, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT2 [2022-09-30T23:59:50.448991593] false guard, stage = STATE_CHANGED, source = STATE1_3, state = STATE2, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT2 [2022-09-30T23:59:50.449371375] state action, stage = STATE_CHANGED, source = STATE1_3, state = STATE2, pseudo state = CHOICE, trigger type = EventTrigger, event = EVENT2
最初のチョイスの結果、STATE1_3
に遷移していることがここで確認できます。そしてガードが1回評価され、ステートが遷移しています。
次の遷移の定義。EVENT3
をトリガーにして、STATE2_2
からSTATE3
に遷移します。
// normal
.withExternal()
.source(States.STATE2_2).event(Events.EVENT3).target(States.STATE3)
.action(loggingAction())
.and()
その次はジャンクションにします。StateMachineTransitionConfigurer#withJunction
からJunctionTransitionConfigurer#source
を指定して、
first
/then
/last
を定義。
ここでは、then
を2回使ってif
、else if
、else if
、else
の構造にしました。最初のチョイスと同じですね。
// junction
.withJunction()
.source(States.STATE3)
.first(States.STATE3_1, falseGuard(), loggingAction())
.then(States.STATE3_2, falseGuard(), loggingAction())
.then(States.STATE3_3, trueGuard(), loggingAction())
.last(States.STATE3_4, loggingAction())
.and()
ガードの結果がtrue
を返す、STATE3_3
が次の遷移先ステートです。
結果。
send event EVENT3 [2022-09-30T23:59:50.451593945] state action, stage = TRANSITION, source = STATE2_2, state = STATE3, pseudo state = JUNCTION, trigger type = EventTrigger, event = EVENT3 [2022-09-30T23:59:50.452477335] false guard, stage = STATE_CHANGED, source = STATE2_2, state = STATE3, pseudo state = JUNCTION, trigger type = EventTrigger, event = EVENT3 [2022-09-30T23:59:50.452786880] false guard, stage = STATE_CHANGED, source = STATE2_2, state = STATE3, pseudo state = JUNCTION, trigger type = EventTrigger, event = EVENT3 [2022-09-30T23:59:50.453079612] true guard, stage = STATE_CHANGED, source = STATE2_2, state = STATE3, pseudo state = JUNCTION, trigger type = EventTrigger, event = EVENT3 [2022-09-30T23:59:50.453582227] state action, stage = STATE_CHANGED, source = STATE2_2, state = STATE3, pseudo state = JUNCTION, trigger type = EventTrigger, event = EVENT3
チョイスと同じように、ガードがtrue
になるまで評価されていることがわかります。
最後は、通常の遷移で終了です。
// normal
.withExternal()
.source(States.STATE3_3).event(Events.EVENT4).target(States.END_STATE)
.action(loggingAction());
ログはこちら。
send event EVENT4 [2022-09-30T23:59:50.455591432] state action, stage = TRANSITION, source = STATE3_3, state = END_STATE, pseudo state = END, trigger type = EventTrigger, event = EVENT4
こう見ていると、動きがわかりますね。
チョイスとジャンクションの違いは?
ところで、チョイスとジャンクションはどう違うのか、使い分けのようなところがわからなかったので、少し見てみました。
ドキュメントに記載されているサンプルも少ないので。
テストコードでは差がわかりませんでした。
チョイスとジャンクションの実装であるChoicePseudoState
とJunctionPseudoState
を見ていると、ほぼ差がありません。
呼び出し方も同じです。
こう見ていると、チョイスとジャンクションには違いがなさそうですね。ドキュメントにはチョイスとジャンクションはコードレベルでは
ほぼ同じ機能であると書かれていましたが、確かにそのとおりのようです。
まとめ
Spring Statemachineのチョイスとジャンクションを試してみました。
遷移にif
/else if
/else
を持ち込め、その条件はガードで定義できるので便利ですね。
最初はドキュメントを読んでいても、ソースコードを書いてみてもピンとこなかったのですが、動かしてみてやっと感覚がつながったので
やっぱり動かして確認するのは大事だなぁと思いました…。