これは、なにをしたくて書いたもの?
Spring Statemachineのトリガーには、イベントとタイマーの2種類があります。
これまでSpring Statemachineを使ってきた時にはイベントトリガーしか扱ってこなかったので、タイマートリガーを試してみたいなと思います。
トリガー
そもそもトリガーとは?というところですが、クラッシュコースに載っています。
A trigger begins a transition. Triggers can be driven by either events or timers.
遷移を開始させるもので、イベントとトリガーの2種類があります。
イベントトリガーは、こんな感じでStateMachine#sendEvent
で送り込むことで起動します。
stateMachine
.sendEvent(Mono.just(MessageBuilder
.withPayload("E1").build()))
.subscribe();
イベントをステートマシンに送信するトリガーですね。
EventTrigger is the most useful trigger, because it lets you directly interact with a state machine by sending events to it.
Spring Statemachine / Using Spring Statemachine / Triggering Transitions / Using EventTrigger
イベントトリガーの定義は、以下のようにTransitionConfigurer#event
で設定します。
@Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.SI).target(States.S1).event(Events.E1) .and() .withExternal() .source(States.S1).target(States.S2).event(Events.E2); }
trigger
とは出てこないので、あんまり「トリガー」という印象になりにくい感じがしますね。
タイマートリガーは、ユーザーの操作なしで自動的にトリガーが起動する必要がある場合に便利なものだそうです。
TimerTrigger is useful when something needs to be triggered automatically without any user interaction.
Spring Statemachine / Using Spring Statemachine / Triggering Transitions / Using TimerTrigger
タイマートリガーは2種類で、継続的に起動するものと1度だけ起動するものがあります。
それぞれ、TransitionConfigurer#timer
およびTransitionConfigurer#timerOnce
で設定します。
transitions .withExternal() .source("S1").target("S2").event("E1") .and() .withExternal() .source("S1").target("S3").event("E2") .and() .withInternal() .source("S2") .action(timerAction()) .timer(1000) .and() .withInternal() .source("S3") .action(timerAction()) .timerOnce(1000);
では、実際に使っていってみましょう。
環境
今回の環境はこちら。
$ 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プロジェクトを作成する
それでは、Spring Bootプロジェクトを作成します。
$ curl -s https://start.spring.io/starter.tgz \ -d bootVersion=2.6.7 \ -d javaVersion=17 \ -d name=statemachine-timer-triger \ -d groupId=org.littlewings \ -d artifactId=statemachine-timer-triger \ -d version=0.0.1-SNAPSHOT \ -d packageName=org.littlewings.spring.statemachine \ -d baseDir=statemachine-timer-triger | tar zxvf -
Spring Bootが2.6.7なのは、ドキュメントに習っています。
Spring Statemachine / Getting started / Using Maven
プロジェクト内に移動。
$ cd statemachine-timer-triger
自動生成されたソースコードは削除しておきます。
$ rm src/main/java/org/littlewings/spring/statemachine/StatemachineTimerTrigerApplication.java src/test/java/org/littlewings/spring/statemachine/StatemachineTimerTrigerApplicationTests.java
Maven依存関係など。
<properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
spring-boot-starter
をspring-statemachine-starter
に変更します。
<dependencies> <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-starter</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
では、ソースコードを作成していきます。
ステートを定義したenum
。
src/main/java/org/littlewings/spring/statemachine/States.java
package org.littlewings.spring.statemachine; public enum States { INITIAL_STATE, STATE1, STATE2, END_STATE }
イベントを定義したenum
。
src/main/java/org/littlewings/spring/statemachine/Events.java
package org.littlewings.spring.statemachine; public enum Events { EVENT1, EVENT2, EVENT3 }
ステートマシンの定義。
src/main/java/org/littlewings/spring/statemachine/StateMachineConfig.java
package org.littlewings.spring.statemachine; import java.time.LocalDateTime; import java.util.concurrent.TimeUnit; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; import org.springframework.statemachine.trigger.TimerTrigger; @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) .end(States.END_STATE); } @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.INITIAL_STATE).target(States.STATE1) .timer(TimeUnit.SECONDS.toMillis(2L)) .action(loggingAction()) .and() .withExternal() .source(States.STATE1).target(States.STATE2) .timerOnce(TimeUnit.SECONDS.toMillis(4L)) .action(loggingAction()) .and() .withExternal() .source(States.STATE2).target(States.END_STATE) .timer(TimeUnit.SECONDS.toMillis(6L)) .action(loggingAction()); } @Bean public Action<States, Events> loggingAction() { return stateContext -> System.out.printf( "[%s] state action, stage = %s, state = %s, is timer trigger = %b, event = %s%n", LocalDateTime.now(), stateContext.getStage(), stateContext.getTarget().getId(), stateContext.getTransition().getTrigger() instanceof TimerTrigger, stateContext.getMessage() != null ? stateContext.getMessage().getPayload() : "[none]" ); } }
ステートマシンを使うクラス。
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.statemachine.StateMachine; import org.springframework.stereotype.Component; @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 { TimeUnit.SECONDS.sleep(15L); System.out.printf("StateMachine complete? %b%n", stateMachine.isComplete()); } }
といっても、今回はなにもしませんが。アプリケーションがすぐに終了してしまわないようにスリープを入れているだけです。
main
メソッドを持ったクラス。
src/main/java/org/littlewings/spring/statemachine/App.java
package org.littlewings.spring.statemachine; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } }
アプリケーションを実行する前に、ステートマシンの設定を見返してみます。タイマートリガーの部分ですね。
TransitionConfigurer#timer
、TransitionConfigurer#timerOnce
、TransitionConfigurer#timer
の順で設定しています。
@Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.INITIAL_STATE).target(States.STATE1) .timer(TimeUnit.SECONDS.toMillis(2L)) .action(loggingAction()) .and() .withExternal() .source(States.STATE1).target(States.STATE2) .timerOnce(TimeUnit.SECONDS.toMillis(4L)) .action(loggingAction()) .and() .withExternal() .source(States.STATE2).target(States.END_STATE) .timer(TimeUnit.SECONDS.toMillis(6L)) .action(loggingAction()); }
2秒後 → 4秒後 → 6秒後で起動するように仕掛けています。
では、起動してみましょう。
$ mvn spring-boot:run
結果。
2022-09-14 23:59:23.477 INFO 19904 --- [ main] org.littlewings.spring.statemachine.App : Started App in 1.183 seconds (JVM running for 1.615) [2022-09-14T23:59:25.342022008] state action, stage = TRANSITION, state = STATE1, is timer trigger = true, event = [none] [2022-09-14T23:59:29.348587936] state action, stage = TRANSITION, state = STATE2, is timer trigger = true, event = [none] [2022-09-14T23:59:35.335503356] state action, stage = TRANSITION, state = END_STATE, is timer trigger = true, event = [none] StateMachine complete? true
イベントは発生せず、トリガーはTimerTrigger
のインスタンスで行われていることが確認できました。
ステートマシンもしっかり終了しています。
timeとtimerOnceの違い
ところで、timer
とtimerOnce
の違いが確認できていません。
ソースコードで確認することにします。
遷移のうちexternalの定義を見てみます。
@Override public ExternalTransitionConfigurer<S, E> timer(long period) { setPeriod(period); return this; } @Override public ExternalTransitionConfigurer<S, E> timerOnce(long period) { setPeriod(period); setCount(1); return this; }
両者の差は、count
の指定有無になっています。
そして、TimerTrigger
に以下のように反映されます。
private void schedule() { long initialDelay = count > 0 ? period : 0; Flux<Long> interval = Flux.interval(Duration.ofMillis(initialDelay), Duration.ofMillis(period)) .doOnNext(c -> { notifyTriggered(); }); if (count > 0) { interval = interval.take(count); } disposable = interval.subscribe(); }
というわけで、こんな感じのコードを書いてみました。
src/main/java/org/littlewings/spring/statemachine/TimerTriggerEmulator.java
package org.littlewings.spring.statemachine; import java.time.Duration; import java.time.LocalDateTime; import java.util.concurrent.TimeUnit; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; @Component public class TimerTriggerEmulator implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { long period = 3000L; // timerOnce System.out.printf("[%s] timer trigger once start.%n", LocalDateTime.now()); Flux<Long> timerOnceTriger = Flux.interval(Duration.ofMillis(period), Duration.ofMillis(period)) .doOnNext(c -> System.out.printf("[%s] timer trigger once execute.%n", LocalDateTime.now())); timerOnceTriger = timerOnceTriger.take(1); timerOnceTriger.subscribe(); // timer System.out.printf("[%s] timer trigger start.%n", LocalDateTime.now()); Flux<Long> timerTriger = Flux.interval(Duration.ofMillis(0), Duration.ofMillis(period)) .doOnNext(c -> System.out.printf("[%s] timer trigger execute.%n", LocalDateTime.now())); timerTriger.subscribe(); TimeUnit.SECONDS.sleep(15L); } }
3秒おきに実行、timeOnce
、time
の順で似せて定義。
実行すると、以下のようにtimerOnce
は1回、timer
は継続で実行されます。
[2022-09-15T00:22:33.947264112] timer trigger once start. [2022-09-15T00:22:34.000809450] timer trigger start. [2022-09-15T00:22:34.001413086] timer trigger execute. [2022-09-15T00:22:37.000953420] timer trigger once execute. [2022-09-15T00:22:37.001310650] timer trigger execute. [2022-09-15T00:22:40.001455757] timer trigger execute. [2022-09-15T00:22:43.001376982] timer trigger execute. [2022-09-15T00:22:46.001450416] timer trigger execute. [2022-09-15T00:22:49.001499904] timer trigger execute.
なのですが、timer
の方はすぐに起動してしまっているので、この処理以外になにか細工があるんでしょうね…。
まあ、そこまでは追わないことにします。
が、timer
はどう使うと効果的なんでしょうか…?1回のタイマートリガーの起動で、ステートが遷移しない可能性がある場合、とかに
使うんでしょうか。エラー発生時の考慮とかもですかね。
まとめ
Spring Statemachineのタイマートリガーを試してみました。
遷移を起こすトリガーの種類を確認しようと思って見てみたのですが、あっさり使えましたね。
timer
とtimerOnce
の使い分けがちょっと気になりますが、他の要素を勉強していくとわかるかもしれません。