1. はじめに
今回は所用によりspring-statementmachine 1.2.12を調べる機会があったので、その際に試したことを記事にしてみました。
基本的に内容は公式ドキュメントのサンプルである7. Developing your first Spring Statemachine applicationのステートマシンと同じです。
ただし、サンプルはspringbootのアプリ、かつコンフィグがメインになっているため、今回はステートマシンのインスタンスを直接作成するビルダーパターン「13.2 State Machine via Builder」の方法と合わせて、単純なコンソールアプリとして実装することにします。
2. .m2/settings.xml の変更(必要に応じて)
インターネットのMavenリポジトリから直接取得するようにPROXYを経由するようにsettings.xmlを変更します。
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<proxies>
<proxy>
<active>true</active>
<protocol>http</protocol>
<host>127.0.0.1</host>
<port>8080</port>
<username>xxxxxx</username>
<password>password</password>
<nonProxyHosts>10.*</nonProxyHosts>
</proxy>
</proxies>
</settings>
3. mavenでブランクプロジェクトを作成
mavenのquickstartでブランクプロジェクトを作成します。
mvn archetype:generate ^
-DinteractiveMode=false ^
-DarchetypeArtifactId=maven-archetype-quickstart ^
-DgroupId=com.example.statemachie.demo ^
-DartifactId=statemachine-demo
4. pom.xml の修正
上記で作成したプロジェクトにspring-statementmachine 1.2.12を設定した最小のpom.xmlを作成します。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.statemachie.demo</groupId>
<artifactId>statemachine-demo</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>statemachine-demo</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- spring-statemachine -->
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>1.2.12.RELEASE</version>
</dependency>
</dependencies>
<!-- java8 -->
<properties>
<java.version>1.8</java.version>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.source>${java.version}</maven.compiler.source>
</properties>
</project>
5. 依存関係にあるライブラリの一覧
mvn dependency:copy-dependencies -DoutputDirectory=lib
でjarファイルを取得してみました。
実装に必要な作業ではありませんが、依存関係を把握するために実施しました。
commons-logging-1.2.jar
junit-3.8.1.jar
spring-aop-4.3.3.RELEASE.jar
spring-beans-4.3.3.RELEASE.jar
spring-context-4.3.3.RELEASE.jar
spring-core-4.3.3.RELEASE.jar
spring-expression-4.3.3.RELEASE.jar
spring-messaging-4.3.3.RELEASE.jar
spring-statemachine-core-1.2.12.RELEASE.jar
spring-tx-4.3.3.RELEASE.jar
6. ファイル構成(src/main/java)
実装したファイルの構成(src/main/java)です。
statemachine-demo\src\main\java
└─com
└─example
└─statemachie
└─demo
App.java
Events.java
SampleListener.java
States.java
7. 実装
実装したファイルの内容です。
7.1. States.java
package com.example.statemachie.demo;
public enum States {
SI, S1, S2
}
ステートマシンのステートとして利用する単純なEnumです。
7.2. Events.java
package com.example.statemachie.demo;
public enum Events {
E1, E2
}
ステートマシンのステートを変更するイベントとして利用する単純なEnumです。
7.3. SampleListener.java
package com.example.statemachie.demo;
import org.springframework.messaging.Message;
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
import org.springframework.statemachine.state.State;
public class SampleListener extends
StateMachineListenerAdapter<States, Events> {
@Override
public void stateChanged(State<States, Events> from,
State<States, Events> to) {
System.out.println("State change to " + to.getId());
}
@Override
public void eventNotAccepted(Message<Events> event) {
System.out.println("event not accepted : " + event.getPayload());
}
}
spring-statemachineの機能です。
ステートを変更をチェックするリスナーです。
StateMachineListenerAdapter
を継承したクラスを定義します。
stateChanged
メソッドがステートが変更した際の呼ばれるメソッド、eventNotAccepted
メソッドがイベントを受け付けなかった際(定義したステートとイベントの関係になっていない、つまり不正なイベント)に呼ばれるメソッドです。
ステートの遷移を確認するため、今回はこの二つのメソッドをオーバーライドしました。
7.4. App.java
package com.example.statemachie.demo;
import java.util.EnumSet;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.config.StateMachineBuilder;
import org.springframework.statemachine.config.StateMachineBuilder.Builder;
public class App {
public static void main(String[] args) {
correctStateFlow();
System.out.println("-----------------------");
incorrectStateFlow();
}
private static void correctStateFlow() {
StateMachine<States, Events> stateMachine = null;
try {
stateMachine = buildMachine();
} catch (Exception e) {
e.printStackTrace();
}
stateMachine.start();
stateMachine.sendEvent(Events.E1);
stateMachine.sendEvent(Events.E2);
System.out.println("isComplete() before call stop : "
+ stateMachine.isComplete());
stateMachine.stop();
System.out.println("isComplete() after call stop : "
+ stateMachine.isComplete());
}
private static void incorrectStateFlow() {
StateMachine<States, Events> stateMachine = null;
try {
stateMachine = buildMachine();
} catch (Exception e) {
e.printStackTrace();
}
stateMachine.start();
stateMachine.sendEvent(Events.E2);
}
private static StateMachine<States, Events> buildMachine() throws Exception {
Builder<States, Events> builder = StateMachineBuilder.builder();
builder.configureStates()
.withStates()
.initial(States.SI) // set initial state
.states(EnumSet.allOf(States.class));
builder.configureTransitions()
.withExternal() // add SI -(E1)-> S1
.source(States.SI).target(States.S1).event(Events.E1)
.and()
.withExternal() // add S1 -(E2)-> S2
.source(States.S1).target(States.S2).event(Events.E2);
SampleListener sampleLister = new SampleListener();
builder.configureConfiguration()
.withConfiguration()
.listener(sampleLister); // set listener
return builder.build();
}
}
buildMachine()
メソッドでorg.springframework.statemachine.StateMachine
のインスタンスを生成しています。この処理はJavaコンフィグやXMLファイルで定義することができますが、「はじめに」で説明したとおり今回はビルダーパターンでオンデマンドに生成しています。
StateMachine
のインスタンスが生成できてしまえば、後はstart()
メソッドでステートマシンを開始し、sendEvent()
メソッドでイベントを実行するだけです。
correctStateFlow()
は定義した正しい順序でイベントを発行しますが、incorrectStateFlow()
は正しくない順序でイベントを発行(イニシャルの後にスグにE2のイベントを発行)しています。これは不正なイベント遷移の場合にどうなるかを確認するため、このようにしてみました。
8. 実行結果
実際に動かしてみた際のコンソールログです。
State change to SI
10 27, 2018 11:48:33 午前 org.springframework.statemachine.support.LifecycleObjectSupport start
情報: started org.springframework.statemachine.support.DefaultStateMachineExecutor@768debd
10 27, 2018 11:48:33 午前 org.springframework.statemachine.support.LifecycleObjectSupport start
情報: started S2 S1 SI / SI / uuid=af9655e4-645d-4dae-9f69-fd0514cd3a9a / id=null
State change to S1
State change to S2
isComplete() before call stop : false
10 27, 2018 11:48:34 午前 org.springframework.statemachine.support.LifecycleObjectSupport stop
情報: stopped org.springframework.statemachine.support.DefaultStateMachineExecutor@768debd
10 27, 2018 11:48:34 午前 org.springframework.statemachine.support.LifecycleObjectSupport stop
情報: stopped S2 S1 SI / / uuid=af9655e4-645d-4dae-9f69-fd0514cd3a9a / id=null
isComplete() after call stop : true
-----------------------
State change to SI
10 27, 2018 11:48:34 午前 org.springframework.statemachine.support.LifecycleObjectSupport start
情報: started org.springframework.statemachine.support.DefaultStateMachineExecutor@387c703b
10 27, 2018 11:48:34 午前 org.springframework.statemachine.support.LifecycleObjectSupport start
情報: started S2 S1 SI / SI / uuid=d075b6b9-8cf5-49fb-bc8e-226bb19600f2 / id=null
event not accepted : E2
イベントリスナで出力した標準出力とstart()
とstop()
メソッドを呼んだ時のデバッグログが出力されています。
ステートマシンをstart()
で開始すると、まず最初にinitialで定義した初期状態であるSI
のステートに変更されます。
ステートがSI
の状態でイベントE1
を実行するとステートがS1
に変更されます。
ステートがS1
の状態でイベントE2
を実行するとステートがS2
に変更されます。
ステートがS2
でもステートマシンは起動中のためisComplete()
はfalse
を返します。
ステートマシンをstop()
で終了するとステートマシンは終了するためisComplete()
はtrue
を返します。
ステートとイベントの関係で、定義していない遷移状態にしようとしてもステートは変更されません。
サンプルではステートが初期状態のSI
の状態でイベントE2
を実行しました。
結果はイベントリスナが出力していますが、イベントは受付られませんでした。
9. さいごに
今回はSpring Statemachineの公式ドキュメントのサンプルをビルダーパターンで試してみました。なんとなくSpring Statemachineの概要が理解できたかと思います。
ただSpring Statemachineを実際のシステムで利用するには、さらなる検証が必要そうです。
そもそもStateMachine
のインスタンスで状態を管理するため、ロングタームの状態遷移(ユーザの承認が入る決裁フロー等)はどうやって管理するのか不明です。
ドキュメントにはREDISによる永続化(28.3 Using Redis)やWebシステムにおける状態管理の例(47. Event Service)が載っていますが、これはBeanのスコープを変更して対応しているだけです。
ただ使ってみた感じは非常に簡単だったので、Spring Statemachineの適用方法が見つかれば有益かなと思いました。