Spring Statemachineを試してみた

### 1. はじめに

今回は所用により**spring-statementmachine 1.2.12**を調べる機会があったので、その際に試したことを記事にしてみました。

基本的に内容は公式ドキュメントのサンプルである[7. Developing your first Spring Statemachine application](https://docs.spring.io/spring-statemachine/docs/2.0.2.RELEASE/reference/htmlsingle/#state-machine-via-builder)のステートマシンと同じです。

ただし、サンプルはspringbootのアプリ、かつコンフィグがメインになっているため、今回はステートマシンのインスタンスを直接作成するビルダーパターン「[13.2 State Machine via Builder](https://docs.spring.io/spring-statemachine/docs/2.0.2.RELEASE/reference/htmlsingle/#state-machine-via-builder)」の方法と合わせて、単純なコンソールアプリとして実装することにします。

### 2. .m2/settings.xml の変更（必要に応じて）

インターネットのMavenリポジトリから直接取得するようにPROXYを経由するようにsettings.xmlを変更します。

```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でブランクプロジェクトを作成します。

```console
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を作成します。

```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ファイルを取得してみました。
実装に必要な作業ではありませんが、依存関係を把握するために実施しました。

```console
    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）です。

```console
statemachine-demo\src\main\java
└─com
    └─example
        └─statemachie
            └─demo
                    App.java
                    Events.java
                    SampleListener.java
                    States.java
```

### 7. 実装

実装したファイルの内容です。

#### 7.1. States.java

```java
package com.example.statemachie.demo;

public enum States {
    SI, S1, S2
}
```

ステートマシンのステートとして利用する単純なEnumです。

#### 7.2. Events.java

```java
package com.example.statemachie.demo;

public enum Events {
    E1, E2
}
```
ステートマシンのステートを変更するイベントとして利用する単純なEnumです。

#### 7.3. SampleListener.java

```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

```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. 実行結果

実際に動かしてみた際のコンソールログです。

```console
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](https://docs.spring.io/spring-statemachine/docs/2.0.2.RELEASE/reference/htmlsingle/#state-machine-via-builder)）やWebシステムにおける状態管理の例（[47. Event Service](https://docs.spring.io/spring-statemachine/docs/2.0.2.RELEASE/reference/htmlsingle/#statemachine-examples-eventservice)）が載っていますが、これはBeanのスコープを変更して対応しているだけです。
ただ使ってみた感じは非常に簡単だったので、Spring Statemachineの適用方法が見つかれば有益かなと思いました。