これは、なにをしたくて書いたもの?
以前に、Spring Statemachineをとりあえず動かしてみました。
Spring Statemachineを試してみる - CLOVER🍀
今回は、「アクション」というものを扱ってみたいと思います。
Spring Statemachineにおけるアクション
アクションがどういうものなのかは、Spring Statemachineの用語集を確認してみましょう。
「アクション」は、遷移の最中にトリガーされ、実行されるものみたいです。
Action
A action is a behavior run during the triggering of the transition.
Spring Statemachine / Appendices / Appendix B: State Machine Concepts / Glossary
クラッシュコースでのアクションの説明を見てみましょう。
アクションは、ステートマシンの状態変化をユーザーのコードに結びつけるものということになっています。ステートマシンは、ステートマシンに
おける様々な変化やステップ(ステートの開始、終了など)、状態遷移において、アクションを実行できます。
Actions really glue state machine state changes to a user’s own code. A state machine can run an action on various changes and on the steps in a state machine (such as entering or exiting a state) or doing a state transition.
また、アクションはステートのコンテキストにアクセスできるため、アクションを構成するコード内でステートマシンとやり取りすることが
できます。
Actions usually have access to a state context, which gives running code a choice to interact with a state machine in various ways. State context exposes a whole state machine so that a user can access extended state variables, event headers (if a transition is based on an event), or an actual transition (where it is possible to see more detailed about where this state change is coming from and where it is going).
というわけで、アクションとはステートマシンの状態変化などに合わせて、なにか処理を実行するもののようですね。
こう書くと、近いものとしてリスナーがあった気がします。リスナーは、ステートマシンの開始、終了なども含めて、ステートマシンに
起こったイベントに対して紐付けるものです。アクションよりも、もうちょっと範囲が広そうですね。
Spring Statemachine / Using Spring Statemachine / Listening to State Machine Events
一方で、こちらを見るとアクションに対してもリスナーと似たようなことが書かれていたりもしますが…。
You can run actions in various places in a state machine and its states lifecycle
Spring Statemachine / Using Spring Statemachine / Using Actions
この箇所では、ステートの開始、終了にSpringのBeanとして定義したアクションを紐付けている例になっています。
@Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.SI) .state(States.S1, action1(), action2()) .state(States.S2, action1(), action2()) .state(States.S3, action1(), action3()); }
Bean定義。
@Bean public Action<States, Events> action1() { return new Action<States, Events>() { @Override public void execute(StateContext<States, Events> context) { } }; } @Bean public BaseAction action2() { return new BaseAction(); } @Bean public SpelAction action3() { ExpressionParser parser = new SpelExpressionParser(); return new SpelAction( parser.parseExpression( "stateMachine.sendEvent(T(org.springframework.statemachine.docs.Events).E1)")); } public class BaseAction implements Action<States, Events> { @Override public void execute(StateContext<States, Events> context) { } } public class SpelAction extends SpelExpressionAction<States, Events> { public SpelAction(Expression expression) { super(expression); } }
というわけで、SpringのBeanをアクションとして使えるようです。内容は、SpELでも書けるようですが。記述できることには、自由度が
ありそうですね。
アクションの構成に関する記述を見てみましょう。
Spring Statemachine / Using Spring Statemachine / Statemachine Configuration / Configuring Actions
アクションは、遷移(Transition)とステートに対して定義できると書かれています。
You can define actions to be executed with transitions and states.
こちらは、遷移に対する定義の例になっています。
@Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.S1) .target(States.S2) .event(Events.E1) .action(action()); }
こちらはステートに対する定義の例。
@Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.S1, action()) .state(States.S1, action(), null) .state(States.S2, null, action()) .state(States.S2, action()) .state(States.S3, action(), action()); }
StateMachineTransitionConfigurer
を使って定義するか、StateMachineStateConfigurer
を使って定義するか、というところですね。
実際にアクションを定義するインターフェースはこちらのようですが。
- TransitionConfigurer (Spring State Machine 3.2.0 API)
- StateConfigurer (Spring State Machine 3.2.0 API)
ここまで見ていると、アクションというのは大きく以下の2つのものに分けられそうですね。
- 遷移に対するアクション … ソースとなるステートから、ターゲットとなるステートへの遷移時に実行される
- ステートに対するアクション … あるステートの開始、終了時に実行される
ドキュメントをもう少し追ってみましょう。
遷移に対するアクション
遷移に対してアクションを定義した場合は、状態変化をトリガーとして発生した遷移の結果として、常にアクションが実行されます。
An action is always run as a result of a transition that originates from a trigger.
遷移に紐付けたアクションでエラーが発生した場合は、action
メソッドの第2引数に渡したアクションでハンドリングできるようです。
TransitionConfigurer (Spring State Machine 3.2.0 API)
こちらの例ですね。
@Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.S1) .target(States.S2) .event(Events.E1) .action(action(), errorAction()); }
エラーハンドリング用のアクションには、発生した例外が含まれたStateContext
が渡されるようです。
なお、Actions#errorCallingAction
を使用することで、通常のアクションとエラー用のアクションを合成してひとつのアクションとして
設定することもできるようです。
@Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.S1) .target(States.S2) .event(Events.E1) .action(Actions.errorCallingAction(action(), errorAction())); }
ステートに対するアクション
ステートに対するアクションは、こちらに記述があります。
アクションの定義は、こちらで行います。
StateConfigurer (Spring State Machine 3.2.0 API)
ステートに対するアクションは、ステートの開始と終了に関連付けられたアクションがあり、それぞれ異なる方法で実行されます。
これは、ステートの開始で実行され、特定のアクションが完了する前にステートが終了するとそのアクションをキャンセルする可能性が
あるからです。
State actions are run differently compared to entry and exit actions, because execution happens after state has been entered and can be cancelled if state exit happens before a particular action has been completed.
アクションはReactorのスケジューラーを使ってサブスクライブすることで実行されます。このため、スレッドの割り込みをハンドリングする
必要があることを意味しています。
State actions are executed using normal reactive flow by subscribing with a Reactor’s default parallel scheduler. This means that, whatever you do in your action, you need to be able to catch InterruptedException or, more generally, periodically check whether Thread is interrupted.
アクションに対する実行ポリシーも指定できるようです。状態が完了した時、またはタイムアウトした時にキャンセルするかどうか、ですね。
StateDoActionPolicy (Spring State Machine 3.2.0 API)
アクションのエラーハンドリングに対する記述は、こちら。
こちらは、3種類のタイミングすべてにアクションおよびエラーアクションを紐付けた例です。
@Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.S1) .stateEntry(States.S2, action(), errorAction()) .stateDo(States.S2, action(), errorAction()) .stateExit(States.S2, action(), errorAction()) .state(States.S3); }
エラーアクションに渡されるStateContext
には、例外情報が含まれることは遷移に対するアクションと同じですね。
ところで、Do
のタイミングがドキュメントに出てきていない気がしますね…。あとで確認してみましょう…。
ステートに対するアクションは紐付けを行うバリエーションが多く、コレクションで複数のアクションを紐付けることができます。
とりあえず、ドキュメントを眺めるのはこんなところでしょうか。次は、実際にSpring Statemachineでアクションを動かしてみましょう。
環境
今回の環境は、こちらです。
$ 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-125-generic", arch: "amd64", family: "unix"
プロジェクトを作成する
Spring Bootプロジェクトを作成します。
$ curl -s https://start.spring.io/starter.tgz \ -d bootVersion=2.6.7 \ -d javaVersion=17 \ -d name=statemachine-action \ -d groupId=org.littlewings \ -d artifactId=statemachine-action \ -d version=0.0.1-SNAPSHOT \ -d packageName=org.littlewings.spring.statemachine \ -d baseDir=statemachine-action | tar zxvf -
少し古めですが、ドキュメントに習ってSpring Bootのバージョンは2.6.7にしておきます。
Spring Statemachine / Getting started / Using Maven
プロジェクト内へ移動。
$ cd statemachine-action
自動生成されたソースコードは削除しておきます。
$ rm src/main/java/org/littlewings/spring/statemachine/StatemachineActionApplication.java src/test/java/org/littlewings/spring/statemachine/StatemachineActionApplicationTests.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>
では、ソースコードを作成していきます。
ステートを定義したenum
。
src/main/java/org/littlewings/spring/statemachine/States.java
package org.littlewings.spring.statemachine; public enum States { INITIAL_STATE, STATE1, STATE2, STATE3, STATE4, STATE5, END_STATE }
ちょっと多めですが、アクションを紐付けるバリエーションをいろいろ試そうとした結果です…。
イベントを定義したenum
。
src/main/java/org/littlewings/spring/statemachine/Events.java
package org.littlewings.spring.statemachine; public enum Events { EVENT1, EVENT2, EVENT3, EVENT4, EVENT5, EVENT6 }
ステートマシンの定義。ちょっと長いですが、これは後で説明します。
src/main/java/org/littlewings/spring/statemachine/StateMachineConfig.java
package org.littlewings.spring.statemachine; import java.util.List; 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; @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) //.initial(States.INITIAL_STATE, stateAction()) .state(States.STATE1) //.state(States.STATE1, stateAction()) .state(States.STATE2) //.state(States.STATE2, stateAction(), stateAction()) .state(States.STATE3) //.stateEntry(States.STATE3, stateEntryAction()) //.stateDo(States.STATE3, stateDoAction()) // synonym state(state, action) //.stateExit(States.STATE3, stateExitAction()) .state(States.STATE4) //.state(States.STATE4, stateAction(), stateAction()) //.stateEntry(States.STATE4, stateActionThrowException(), stateActionHandleError()) //.stateDo(States.STATE4, stateActionThrowException(), stateActionHandleError()) //.stateExit(States.STATE4, stateActionThrowException(), stateActionHandleError()) .state(States.STATE5) //.state(States.STATE5, List.of(stateAction(), stateAction()), List.of(stateAction(), stateAction())) // collection .end(States.END_STATE); // endにはactionなし } @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.INITIAL_STATE) .target(States.STATE1) .event(Events.EVENT1) //.action(transitionAction()) .and() .withExternal() .source(States.STATE1) .target(States.STATE2) .event(Events.EVENT2) //.action(transitionAction()) //.action(transitionActionThrowException(), transitionActionHandleError()) .and() .withExternal() .source(States.STATE2) .target(States.STATE3) .event(Events.EVENT3) //.action(transitionAction()) .and() .withExternal() .source(States.STATE3) .target(States.STATE4) .event(Events.EVENT4) //.action(transitionAction()) //.action(transitionActionThrowException(), transitionActionHandleError()) .and() .withExternal() .source(States.STATE4) .target(States.STATE5) .event(Events.EVENT5) //.action(transitionAction()) .and() .withExternal() .source(States.STATE5) .target(States.END_STATE) .event(Events.EVENT6); //.action(transitionAction()); } @Bean public Action<States, Events> stateAction() { return stateContext -> System.out.printf( "state action, stage = %s, state = %s, event = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); } @Bean public Action<States, Events> stateEntryAction() { return stateContext -> System.out.printf( "state entry action, stage = %s, state = %s, event = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); } @Bean public Action<States, Events> stateDoAction() { return stateContext -> System.out.printf( "state do action, stage = %s, state = %s, event = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); } @Bean public Action<States, Events> stateExitAction() { return stateContext -> System.out.printf( "state exit action, stage = %s, state = %s, event = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); } @Bean public Action<States, Events> stateActionThrowException() { return stateContext -> { System.out.printf( "state action throw exception, stage = %s, state = %s, event = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); throw new RuntimeException("state action error!!"); }; } @Bean public Action<States, Events> stateActionHandleError() { return stateContext -> System.out.printf( "state action handling exception, stage = %s, state = %s, event = %s, exception message = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]", stateContext.getException().getMessage() ); } @Bean public Action<States, Events> transitionAction() { return stateContext -> System.out.printf( "transition action, stage = %s, state = %s, event = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); } @Bean public Action<States, Events> transitionActionThrowException() { return stateContext -> { System.out.printf( "transition action throw exception, stage = %s, state = %s, event = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); throw new RuntimeException("transition action error!!"); }; } @Bean public Action<States, Events> transitionActionHandleError() { return stateContext -> System.out.printf( "transition action handling exception, stage = %s, state = %s, event = %s, exception message = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]", stateContext.getException().getMessage() ); } }
ステートマシンを使うクラス。
src/main/java/org/littlewings/spring/statemachine/Runner.java
package org.littlewings.spring.statemachine; import java.util.concurrent.TimeUnit; 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 { stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT1).build())) .blockFirst(); stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT2).build())) .blockFirst(); stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT3).build())) .blockFirst(); stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT4).build())) .blockFirst(); stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT5).build())) .blockFirst(); stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT6).build())) .blockFirst(); } }
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); } }
今回のメインはもちろんステートマシンに定義したステート、遷移に紐付けたアクションの確認なのですが。
最初に載せたものがいろいろとごちゃごちゃしていたので、まずはステートと遷移のベースの定義を載せましょう。
@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) .state(States.STATE1) .state(States.STATE2) .state(States.STATE3) .state(States.STATE4) .state(States.STATE5) .end(States.END_STATE); } @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.INITIAL_STATE) .target(States.STATE1) .event(Events.EVENT1) .and() .withExternal() .source(States.STATE1) .target(States.STATE2) .event(Events.EVENT2) .and() .withExternal() .source(States.STATE2) .target(States.STATE3) .event(Events.EVENT3) .and() .withExternal() .source(States.STATE3) .target(States.STATE4) .event(Events.EVENT4) .and() .withExternal() .source(States.STATE4) .target(States.STATE5) .event(Events.EVENT5) .and() .withExternal() .source(States.STATE5) .target(States.END_STATE) .event(Events.EVENT6); } 〜省略〜 }
まだいずれにもアクションは紐付けていません。
この状態で、アプリケーションを実行。
$ mvn spring-boot:run
まあ、特になにも表示されないのですが。
2022-09-02 00:48:08.062 INFO 25583 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.statemachine.config.configuration.StateMachineAnnotationPostProcessorConfiguration' of type [org.springframework.statemachine.config.configuration.StateMachineAnnotationPostProcessorConfiguration$$EnhancerBySpringCGLIB$$95adde42] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2022-09-02 00:48:08.450 INFO 25583 --- [ main] org.littlewings.spring.statemachine.App : Started App in 1.26 seconds (JVM running for 1.549) 2022-09-02 00:48:08.498 INFO 25583 --- [ionShutdownHook] o.s.s.support.LifecycleObjectSupport : destroy called
ここから、ステートや遷移にアクションを紐付けていきます。
ステートにアクションを紐付ける
まずは、ステートにアクションを紐付けてみます。
以下のようにアクションを紐付けました。
@Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.INITIAL_STATE, stateAction()) .state(States.STATE1, stateAction()) .state(States.STATE2, stateAction(), stateAction()) .stateEntry(States.STATE3, stateEntryAction()) .stateDo(States.STATE3, stateDoAction()) // synonym state(state, action) .stateExit(States.STATE3, stateExitAction()) .state(States.STATE4, stateAction(), stateAction()) .state(States.STATE5, List.of(stateAction(), stateAction()), List.of(stateAction(), stateAction())) // collection .end(States.END_STATE); // endにはactionなし }
ステートに対してひとつアクションを紐付けた場合は、ステートの開始時への紐付け
.state(States.STATE1, stateAction())
2つの場合は開始と終了、
.state(States.STATE2, stateAction(), stateAction())
開始と終了のタイミング別の紐付(Do
はEntry
の別名のようです)、
.stateEntry(States.STATE3, stateEntryAction())
.stateDo(States.STATE3, stateDoAction()) // synonym state(state, action)
.stateExit(States.STATE3, stateExitAction())
開始と終了のタイミングで、複数のアクションを紐付というバリエーションです。
.state(States.STATE5, List.of(stateAction(), stateAction()), List.of(stateAction(), stateAction())) // collection
最後のステートには、アクションは紐付けられないみたいです。
.end(States.END_STATE); // endにはactionなし
紐付けたアクションの定義はこちらで、ステージやステート、発生したイベントの情報を標準出力に書き出しています。
@Bean public Action<States, Events> stateAction() { return stateContext -> System.out.printf( "state action, stage = %s, state = %s, event = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); } @Bean public Action<States, Events> stateEntryAction() { return stateContext -> System.out.printf( "state entry action, stage = %s, state = %s, event = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); } @Bean public Action<States, Events> stateDoAction() { return stateContext -> System.out.printf( "state do action, stage = %s, state = %s, event = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); } @Bean public Action<States, Events> stateExitAction() { return stateContext -> System.out.printf( "state exit action, stage = %s, state = %s, event = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); }
では、動かしてみます。
$ mvn spring-boot:run
System.out.println
していた部分を抜き出すと、こういう出力が得られました。
state action, stage = TRANSITION, state = INITIAL_STATE, event = [none] state action, stage = STATE_ENTRY, state = STATE1, event = EVENT1 state action, stage = STATE_ENTRY, state = STATE2, event = EVENT2 state action, stage = STATE_EXIT, state = STATE3, event = EVENT3 state entry action, stage = STATE_ENTRY, state = STATE3, event = EVENT3 state do action, stage = STATE_ENTRY, state = STATE3, event = EVENT3 state exit action, stage = STATE_EXIT, state = STATE4, event = EVENT4 state action, stage = STATE_ENTRY, state = STATE4, event = EVENT4 state action, stage = STATE_EXIT, state = STATE5, event = EVENT5 state action, stage = STATE_ENTRY, state = STATE5, event = EVENT5 state action, stage = STATE_ENTRY, state = STATE5, event = EVENT5 state action, stage = STATE_EXIT, state = END_STATE, event = EVENT6 state action, stage = STATE_EXIT, state = END_STATE, event = EVENT6
ソースコードと紐付けると、こんな感じでしょうか。
// .initial(States.INITIAL_STATE, stateAction()) state action, stage = TRANSITION, state = INITIAL_STATE, event = [none] // .state(States.STATE1, stateAction()) state action, stage = STATE_ENTRY, state = STATE1, event = EVENT1 .state(States.STATE2, stateAction(), stateAction()) state action, stage = STATE_ENTRY, state = STATE2, event = EVENT2 state action, stage = STATE_EXIT, state = STATE3, event = EVENT3 // .stateEntry(States.STATE3, stateEntryAction()) // .stateDo(States.STATE3, stateDoAction()) // synonym state(state, action) // .stateExit(States.STATE3, stateExitAction()) state entry action, stage = STATE_ENTRY, state = STATE3, event = EVENT3 state do action, stage = STATE_ENTRY, state = STATE3, event = EVENT3 state exit action, stage = STATE_EXIT, state = STATE4, event = EVENT4 // .state(States.STATE4, stateAction(), stateAction()) state action, stage = STATE_ENTRY, state = STATE4, event = EVENT4 state action, stage = STATE_EXIT, state = STATE5, event = EVENT5 // .state(States.STATE5, List.of(stateAction(), stateAction()), List.of(stateAction(), stateAction())) state action, stage = STATE_ENTRY, state = STATE5, event = EVENT5 state action, stage = STATE_ENTRY, state = STATE5, event = EVENT5 state action, stage = STATE_EXIT, state = END_STATE, event = EVENT6 state action, stage = STATE_EXIT, state = END_STATE, event = EVENT6
endにはアクションは紐付けられませんでしたね。
次に、一部のアクションの紐付けを変更してみます。
states .withStates() .initial(States.INITIAL_STATE, stateAction()) .state(States.STATE1, stateAction()) .state(States.STATE2, stateAction(), stateAction()) .stateEntry(States.STATE3, stateEntryAction()) .stateDo(States.STATE3, stateDoAction()) // synonym state(state, action) .stateExit(States.STATE3, stateExitAction()) .stateEntry(States.STATE4, stateActionThrowException(), stateActionHandleError()) .stateDo(States.STATE4, stateActionThrowException(), stateActionHandleError()) .stateExit(States.STATE4, stateActionThrowException(), stateActionHandleError()) .state(States.STATE5, List.of(stateAction(), stateAction()), List.of(stateAction(), stateAction())) // collection .end(States.END_STATE); // endにはactionなし
変わったのは、STATE4
ですね。
こちらには、例外を投げるアクションと、アクションから投げられた例外をハンドリングするアクションを紐付けています。
アクションの定義は、こちら。
@Bean public Action<States, Events> stateActionThrowException() { return stateContext -> { System.out.printf( "state action throw exception, stage = %s, state = %s, event = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); throw new RuntimeException("state action error!!"); }; } @Bean public Action<States, Events> stateActionHandleError() { return stateContext -> System.out.printf( "state action handling exception, stage = %s, state = %s, event = %s, exception message = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]", stateContext.getException().getMessage() ); }
実行。
$ mvn spring-boot:run
スタックトレースを出力しつつも、すべてのステートを経て終了しました。
state action, stage = TRANSITION, state = INITIAL_STATE, event = [none] 2022-09-02 01:05:35.735 INFO 26623 --- [ main] org.littlewings.spring.statemachine.App : Started App in 1.017 seconds (JVM running for 1.256) state action, stage = STATE_ENTRY, state = STATE1, event = EVENT1 state action, stage = STATE_ENTRY, state = STATE2, event = EVENT2 state action, stage = STATE_EXIT, state = STATE3, event = EVENT3 state entry action, stage = STATE_ENTRY, state = STATE3, event = EVENT3 state do action, stage = STATE_ENTRY, state = STATE3, event = EVENT3 state exit action, stage = STATE_EXIT, state = STATE4, event = EVENT4 state action throw exception, stage = STATE_ENTRY, state = STATE4, event = EVENT4 state action handling exception, stage = STATE_ENTRY, state = STATE4, event = EVENT4, exception message = state action error!! 2022-09-02 01:05:35.777 WARN 26623 --- [ main] o.s.statemachine.state.ObjectState : Entry action execution error java.lang.RuntimeException: state action error!! at org.littlewings.spring.statemachine.StateMachineConfig.lambda$stateActionThrowException$4(StateMachineConfig.java:159) ~[classes/:na] at org.springframework.statemachine.action.Actions$2.execute(Actions.java:71) ~[spring-statemachine-core-3.2.0.jar:3.2.0] at org.springframework.statemachine.action.Actions.lambda$null$0(Actions.java:98) ~[spring-statemachine-core-3.2.0.jar:3.2.0] at reactor.core.publisher.MonoRunnable.call(MonoRunnable.java:73) ~[reactor-core-3.4.17.jar:3.4.17] at reactor.core.publisher.MonoRunnable.call(MonoRunnable.java:32) ~[reactor-core-3.4.17.jar:3.4.17] at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:252) ~[reactor-core-3.4.17.jar:3.4.17] at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51) ~[reactor-core-3.4.17.jar:3.4.17] at reactor.core.publisher.Mono.subscribe(Mono.java:4400) ~[reactor-core-3.4.17.jar:3.4.17] at reactor.core.publisher.FluxFlatMap.trySubscribeScalarMap(FluxFlatMap.java:203) ~[reactor-core-3.4.17.jar:3.4.17] 〜省略〜 at reactor.core.publisher.Flux.subscribe(Flux.java:8455) ~[reactor-core-3.4.17.jar:3.4.17] at reactor.core.publisher.Flux.blockFirst(Flux.java:2599) ~[reactor-core-3.4.17.jar:3.4.17] at org.littlewings.spring.statemachine.Runner.run(Runner.java:33) ~[classes/:na] at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:768) ~[spring-boot-2.6.7.jar:2.6.7] at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:758) ~[spring-boot-2.6.7.jar:2.6.7] at org.springframework.boot.SpringApplication.run(SpringApplication.java:310) ~[spring-boot-2.6.7.jar:2.6.7] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1312) ~[spring-boot-2.6.7.jar:2.6.7] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1301) ~[spring-boot-2.6.7.jar:2.6.7] at org.littlewings.spring.statemachine.App.main(App.java:9) ~[classes/:na] state action throw exception, stage = STATE_ENTRY, state = STATE4, event = EVENT4 state action handling exception, stage = STATE_ENTRY, state = STATE4, event = EVENT4, exception message = state action error!! state action throw exception, stage = STATE_EXIT, state = STATE5, event = EVENT5 state action handling exception, stage = STATE_EXIT, state = STATE5, event = EVENT5, exception message = state action error!! 2022-09-02 01:05:35.784 WARN 26623 --- [ main] o.s.statemachine.state.ObjectState : Exit action execution error java.lang.RuntimeException: state action error!! at org.littlewings.spring.statemachine.StateMachineConfig.lambda$stateActionThrowException$4(StateMachineConfig.java:159) ~[classes/:na] at org.springframework.statemachine.action.Actions$2.execute(Actions.java:71) ~[spring-statemachine-core-3.2.0.jar:3.2.0] at org.springframework.statemachine.action.Actions.lambda$null$0(Actions.java:98) ~[spring-statemachine-core-3.2.0.jar:3.2.0] at reactor.core.publisher.MonoRunnable.call(MonoRunnable.java:73) ~[reactor-core-3.4.17.jar:3.4.17] at reactor.core.publisher.MonoRunnable.call(MonoRunnable.java:32) ~[reactor-core-3.4.17.jar:3.4.17] at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:252) ~[reactor-core-3.4.17.jar:3.4.17] at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51) ~[reactor-core-3.4.17.jar:3.4.17] at reactor.core.publisher.Mono.subscribe(Mono.java:4400) ~[reactor-core-3.4.17.jar:3.4.17] at reactor.core.publisher.FluxFlatMap.trySubscribeScalarMap(FluxFlatMap.java:203) ~[reactor-core-3.4.17.jar:3.4.17] 〜省略〜 at reactor.core.publisher.Flux.subscribe(Flux.java:8469) ~[reactor-core-3.4.17.jar:3.4.17] at reactor.core.publisher.FluxFlatMap.trySubscribeScalarMap(FluxFlatMap.java:200) ~[reactor-core-3.4.17.jar:3.4.17] at reactor.core.publisher.MonoFlatMapMany.subscribeOrReturn(MonoFlatMapMany.java:49) ~[reactor-core-3.4.17.jar:3.4.17] at reactor.core.publisher.Flux.subscribe(Flux.java:8455) ~[reactor-core-3.4.17.jar:3.4.17] at reactor.core.publisher.Flux.blockFirst(Flux.java:2599) ~[reactor-core-3.4.17.jar:3.4.17] at org.littlewings.spring.statemachine.Runner.run(Runner.java:36) ~[classes/:na] at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:768) ~[spring-boot-2.6.7.jar:2.6.7] at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:758) ~[spring-boot-2.6.7.jar:2.6.7] at org.springframework.boot.SpringApplication.run(SpringApplication.java:310) ~[spring-boot-2.6.7.jar:2.6.7] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1312) ~[spring-boot-2.6.7.jar:2.6.7] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1301) ~[spring-boot-2.6.7.jar:2.6.7] at org.littlewings.spring.statemachine.App.main(App.java:9) ~[classes/:na] state action, stage = STATE_ENTRY, state = STATE5, event = EVENT5 state action, stage = STATE_ENTRY, state = STATE5, event = EVENT5 state action, stage = STATE_EXIT, state = END_STATE, event = EVENT6 state action, stage = STATE_EXIT, state = END_STATE, event = EVENT6
Spring StatemachineとReactorが例外を拾っていますが、実行はそのまま続いていますね。
例外を投げ、ハンドリングしているアクションとソースコードの対比を載せるとこんな感じです。
// .stateEntry(States.STATE4, stateActionThrowException(), stateActionHandleError()) state action throw exception, stage = STATE_ENTRY, state = STATE4, event = EVENT4 state action handling exception, stage = STATE_ENTRY, state = STATE4, event = EVENT4, exception message = state action error!! // .stateDo(States.STATE4, stateActionThrowException(), stateActionHandleError()) state action throw exception, stage = STATE_ENTRY, state = STATE4, event = EVENT4 state action handling exception, stage = STATE_ENTRY, state = STATE4, event = EVENT4, exception message = state action error!! // .stateExit(States.STATE4, stateActionThrowException(), stateActionHandleError()) state action throw exception, stage = STATE_EXIT, state = STATE5, event = EVENT5 state action handling exception, stage = STATE_EXIT, state = STATE5, event = EVENT5, exception message = state action error!!
だいたい雰囲気はわかりましたね。
ちなみに、エラーハンドリング用のアクションで例外は扱っているはずなのに、どうしてSpring StatemachineとReactorでスタックトレースが
現れるかというと、エラー用のアクションを呼び出した後で例外を再スローしているからのようです…。
では、1度ステートの定義からアクションの紐付けを外します。
@Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.INITIAL_STATE) .state(States.STATE1) .state(States.STATE2) .state(States.STATE3) .state(States.STATE4) .state(States.STATE5) .end(States.END_STATE); }
遷移にアクションを紐付ける
次は、遷移に対してアクションを紐付けてみます。
@Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.INITIAL_STATE) .target(States.STATE1) .event(Events.EVENT1) .action(transitionAction()) .and() .withExternal() .source(States.STATE1) .target(States.STATE2) .event(Events.EVENT2) .action(transitionAction()) .and() .withExternal() .source(States.STATE2) .target(States.STATE3) .event(Events.EVENT3) .action(transitionAction()) .and() .withExternal() .source(States.STATE3) .target(States.STATE4) .event(Events.EVENT4) .action(transitionAction()) .and() .withExternal() .source(States.STATE4) .target(States.STATE5) .event(Events.EVENT5) .action(transitionAction()) .and() .withExternal() .source(States.STATE5) .target(States.END_STATE) .event(Events.EVENT6)//; .action(transitionAction()); }
今回は、すべての遷移に対して同じアクションを紐付けています。
アクションの定義は、こちら。
@Bean public Action<States, Events> transitionAction() { return stateContext -> System.out.printf( "transition action, stage = %s, state = %s, event = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); }
実行してみます。
$ mvn spring-boot:run
結果は、とてもシンプルです。
transition action, stage = TRANSITION, state = STATE1, event = EVENT1 transition action, stage = TRANSITION, state = STATE2, event = EVENT2 transition action, stage = TRANSITION, state = STATE3, event = EVENT3 transition action, stage = TRANSITION, state = STATE4, event = EVENT4 transition action, stage = TRANSITION, state = STATE5, event = EVENT5 transition action, stage = TRANSITION, state = END_STATE, event = EVENT6
では、ここで遷移の2つに例外を扱うアクションを追加してみましょう。
@Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.INITIAL_STATE) .target(States.STATE1) .event(Events.EVENT1) .action(transitionAction()) .and() .withExternal() .source(States.STATE1) .target(States.STATE2) .event(Events.EVENT2) .action(transitionActionThrowException(), transitionActionHandleError()) .and() .withExternal() .source(States.STATE2) .target(States.STATE3) .event(Events.EVENT3) .action(transitionAction()) .and() .withExternal() .source(States.STATE3) .target(States.STATE4) .event(Events.EVENT4) .action(transitionActionThrowException(), transitionActionHandleError()) .and() .withExternal() .source(States.STATE4) .target(States.STATE5) .event(Events.EVENT5) .action(transitionAction()) .and() .withExternal() .source(States.STATE5) .target(States.END_STATE) .event(Events.EVENT6)//; .action(transitionAction()); }
例外を扱うアクションは、こちら。内容自体はステートの時と同じです。
@Bean public Action<States, Events> transitionActionThrowException() { return stateContext -> { System.out.printf( "transition action throw exception, stage = %s, state = %s, event = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); throw new RuntimeException("transition action error!!"); }; } @Bean public Action<States, Events> transitionActionHandleError() { return stateContext -> System.out.printf( "transition action handling exception, stage = %s, state = %s, event = %s, exception message = %s%n", stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]", stateContext.getException().getMessage() ); }
実行してみます。
$ mvn spring-boot:run
こちらは、ステートの時とは異なった結果になりました。
transition action, stage = TRANSITION, state = STATE1, event = EVENT1 transition action throw exception, stage = TRANSITION, state = STATE2, event = EVENT2 transition action handling exception, stage = TRANSITION, state = STATE2, event = EVENT2, exception message = transition action error!!
アクションで発生した例外をハンドリングした時点で、ステートマシンが終了してしまいました。
こちらの処理自体はすべて実行されているのですが、EVENT2
を送信したところでそれ以降は受け付けなくなっているようです。
@Override public void run(ApplicationArguments args) throws Exception { stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT1).build())) .blockFirst(); stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT2).build())) .blockFirst(); stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT3).build())) .blockFirst(); stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT4).build())) .blockFirst(); stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT5).build())) .blockFirst(); stateMachine .sendEvent(Mono.just(MessageBuilder.withPayload(Events.EVENT6).build())) .blockFirst(); }
遷移に紐付けたアクションの場合は、例外をハンドリングしても停止してしまうんですね…。
ちなみに、アクションが例外をハンドリングした後も例外を再スローするのは、やっぱり以下の部分です。
ステートと遷移の両方にアクションを紐付ける
最後に、ステートと遷移の両方にアクションを紐付けた時の動作を見てみましょう。今回は、例外は使いません。
@Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.INITIAL_STATE, stateAction()) .state(States.STATE1, stateAction()) .state(States.STATE2, stateAction(), stateAction()) .stateEntry(States.STATE3, stateEntryAction()) .stateExit(States.STATE3, stateExitAction()) .state(States.STATE4, stateAction(), stateAction()) .state(States.STATE5) .end(States.END_STATE); // endにはactionなし } @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.INITIAL_STATE) .target(States.STATE1) .event(Events.EVENT1) .action(transitionAction()) .and() .withExternal() .source(States.STATE1) .target(States.STATE2) .event(Events.EVENT2) .action(transitionAction()) .and() .withExternal() .source(States.STATE2) .target(States.STATE3) .event(Events.EVENT3) .action(transitionAction()) .and() .withExternal() .source(States.STATE3) .target(States.STATE4) .event(Events.EVENT4) .action(transitionAction()) .and() .withExternal() .source(States.STATE4) .target(States.STATE5) .event(Events.EVENT5) .action(transitionAction()) .and() .withExternal() .source(States.STATE5) .target(States.END_STATE) .event(Events.EVENT6)//; .action(transitionAction()); }
実行。
$ mvn spring-boot:run
結果は、こんな感じになりました。
state action, stage = TRANSITION, state = INITIAL_STATE, event = [none] transition action, stage = TRANSITION, state = STATE1, event = EVENT1 state action, stage = STATE_ENTRY, state = STATE1, event = EVENT1 transition action, stage = TRANSITION, state = STATE2, event = EVENT2 state action, stage = STATE_ENTRY, state = STATE2, event = EVENT2 transition action, stage = TRANSITION, state = STATE3, event = EVENT3 state action, stage = STATE_EXIT, state = STATE3, event = EVENT3 state entry action, stage = STATE_ENTRY, state = STATE3, event = EVENT3 transition action, stage = TRANSITION, state = STATE4, event = EVENT4 state exit action, stage = STATE_EXIT, state = STATE4, event = EVENT4 state action, stage = STATE_ENTRY, state = STATE4, event = EVENT4 transition action, stage = TRANSITION, state = STATE5, event = EVENT5 state action, stage = STATE_EXIT, state = STATE5, event = EVENT5 transition action, stage = TRANSITION, state = END_STATE, event = EVENT6
ソースコードと紐付けてみましょう。
// .initial(States.INITIAL_STATE, stateAction()) state action, stage = TRANSITION, state = INITIAL_STATE, event = [none] // .source(States.INITIAL_STATE) // .target(States.STATE1) // .event(Events.EVENT1) // .action(transitionAction()) transition action, stage = TRANSITION, state = STATE1, event = EVENT1 // .state(States.STATE1, stateAction()) state action, stage = STATE_ENTRY, state = STATE1, event = EVENT1 // .source(States.STATE1) // .target(States.STATE2) // .event(Events.EVENT2) // .action(transitionAction()) transition action, stage = TRANSITION, state = STATE2, event = EVENT2 // .state(States.STATE2, stateAction(), stateAction()) state action, stage = STATE_ENTRY, state = STATE2, event = EVENT2 // .source(States.STATE2) // .target(States.STATE3) // .event(Events.EVENT3) // .action(transitionAction()) transition action, stage = TRANSITION, state = STATE3, event = EVENT3 // .state(States.STATE2, stateAction(), stateAction()) state action, stage = STATE_EXIT, state = STATE3, event = EVENT3 // .stateExit(States.STATE3, stateExitAction()) state entry action, stage = STATE_ENTRY, state = STATE3, event = EVENT3 // .source(States.STATE3) // .target(States.STATE4) // .event(Events.EVENT4) // .action(transitionAction()) transition action, stage = TRANSITION, state = STATE4, event = EVENT4 // .stateExit(States.STATE3, stateExitAction()) state exit action, stage = STATE_EXIT, state = STATE4, event = EVENT4 // .state(States.STATE4, stateAction(), stateAction()) state action, stage = STATE_ENTRY, state = STATE4, event = EVENT4 // .source(States.STATE4) // .target(States.STATE5) // .event(Events.EVENT5) // .action(transitionAction()) transition action, stage = TRANSITION, state = STATE5, event = EVENT5 // .state(States.STATE4, stateAction(), stateAction()) state action, stage = STATE_EXIT, state = STATE5, event = EVENT5 // .source(States.STATE5) // .target(States.END_STATE) // .event(Events.EVENT6)//; // .action(transitionAction()); transition action, stage = TRANSITION, state = END_STATE, event = EVENT6
こう見ると、ステート開始・終了の間に遷移が挟まっている感じに見えますが、どうなんでしょうね?
まあ、なんとなくアクションをステートおよび遷移に紐付けると、どういう動作になるかはわかった気がします。
まとめ
Spring Statemachineのアクションを試してみました。
最初は雰囲気で初めてみたのですが、ちゃんとドキュメントを見ているとアクションの紐付け先はステートと遷移の2種類あることがわかり、
それぞれ考え方が微妙に違ったので、ドキュメントを読んでおくのは大事だなという気にはなりました。
とりあえず、雰囲気はわかったので今回はこんなところで。