CLOVER🍀

That was when it all began.

Spring Statemachineのアクションを試す

これは、なにをしたくて書いたもの?

以前に、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.

Spring Statemachine / Appendices / Appendix B: State Machine Concepts / A State Machine Crash Course / Actions

また、アクションはステートのコンテキストにアクセスできるため、アクションを構成するコード内でステートマシンとやり取りすることが
できます。

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を使って定義するか、というところですね。
実際にアクションを定義するインターフェースはこちらのようですが。

ここまで見ていると、アクションというのは大きく以下の2つのものに分けられそうですね。

  • 遷移に対するアクション … ソースとなるステートから、ターゲットとなるステートへの遷移時に実行される
  • ステートに対するアクション … あるステートの開始、終了時に実行される

ドキュメントをもう少し追ってみましょう。

遷移に対するアクション

遷移に対してアクションを定義した場合は、状態変化をトリガーとして発生した遷移の結果として、常にアクションが実行されます。

An action is always run as a result of a transition that originates from a trigger.

遷移に紐付けたアクションでエラーが発生した場合は、actionメソッドの第2引数に渡したアクションでハンドリングできるようです。

Spring Statemachine / Using Spring Statemachine / Statemachine Configuration / Configuring ActionsTransition Action Error Handling

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)

アクションのエラーハンドリングに対する記述は、こちら。

Spring Statemachine / Using Spring Statemachine / Statemachine Configuration / Configuring Actions / State Action Error Handling

こちらは、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のタイミングがドキュメントに出てきていない気がしますね…。あとで確認してみましょう…。

ステートに対するアクションは紐付けを行うバリエーションが多く、コレクションで複数のアクションを紐付けることができます。

https://github.com/spring-projects/spring-statemachine/blob/v3.2.0/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java#L173-L282

とりあえず、ドキュメントを眺めるのはこんなところでしょうか。次は、実際に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でスタックトレースが
現れるかというと、エラー用のアクションを呼び出した後で例外を再スローしているからのようです…。

https://github.com/spring-projects/spring-statemachine/blob/v3.2.0/spring-statemachine-core/src/main/java/org/springframework/statemachine/action/Actions.java#L82

では、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();
    }

遷移に紐付けたアクションの場合は、例外をハンドリングしても停止してしまうんですね…。

ちなみに、アクションが例外をハンドリングした後も例外を再スローするのは、やっぱり以下の部分です。

https://github.com/spring-projects/spring-statemachine/blob/v3.2.0/spring-statemachine-core/src/main/java/org/springframework/statemachine/action/Actions.java#L82

ステートと遷移の両方にアクションを紐付ける

最後に、ステートと遷移の両方にアクションを紐付けた時の動作を見てみましょう。今回は、例外は使いません。

    @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種類あることがわかり、
それぞれ考え方が微妙に違ったので、ドキュメントを読んでおくのは大事だなという気にはなりました。

とりあえず、雰囲気はわかったので今回はこんなところで。