Help us understand the problem. What is going on with this article?

Spring Statemachineを試してみた

More than 1 year has passed since last update.

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の適用方法が見つかれば有益かなと思いました。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away