通过实例学习SpringStateMachine之Persist

背景介绍

本系列通过学习SpringStateMachine中附带的10余个Sample来学习SpringStateMachine中的各个概念和用法。项目是使用的分支为2.2.0.RELEASE。项目参考文档也是2.2.0.RELEASE。

Persist简介

Persist这个例子演示了通过状态机来更新数据库中记录的状态。例如一条订单记录,有两个状态,支付与未支付。建立对应的订单状态机,当状态机状态变化后,数据库中的订单记录状态同时发生更新。

本例中描述的数据库记录的状态机如下图所示:
在这里插入图片描述

一共有4个状态PLACED、PROCESSING、SENT、DELIVERED。当触发相应事件后对应的状态发生变化。数据库中对应的这条记录的状态同时发生变化。

Persist 依赖

项目在实现上述功能时,需要依赖springshell,官方给出的demo[1]使用了spring-shell1.2,本文将其改为spring-shell 2.0.0.RELEASE。由于Persist需要操作数据库及处理数据库状态事件,因此我的这个例子中使用mysql。最终会依赖spring-boot-starter-jdbc,mysql-connector-java及spring-statemachine-recipes-common。

<dependency>
       <groupId>
            org.springframework.statemachine</groupId>
            <artifactId>spring-statemachine-core</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.shell</groupId>
            <artifactId>spring-shell-starter</artifactId>
            <version>2.0.0.RELEASE</version>
        </dependency>
        
       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.statemachine</groupId>
            <artifactId>spring-statemachine-recipes-common</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>

Persist实现

首先我们创建一个Order对象,state共有4中状态,分别是Persist简介中状态机对应的四种状态。

public static class Order {
	int id;
	String state;

	public Order(int id, String state) {
		this.id = id;
		this.state = state;
	}

	@Override
	public String toString() {
		return "Order [id=" + id + ", state=" + state + "]";
	}
}

接着创建mysql数据表orders。

CREATE TABLE `orders` (
  `id` int(11) NOT NULL,
  `state` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

配置数据库。

##数据库配置
##数据库地址
spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=false
##数据库用户名
spring.datasource.username=root
##数据库密码
spring.datasource.password=root
##数据库驱动
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

根据状态机图配置状态机。注意这里和前几节的差异是使用了字符串来描述状态,而不是枚举类型。

import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;

@Configuration
@EnableStateMachine
public class StateMachineConfig
        extends StateMachineConfigurerAdapter<String, String> {

    @Override
    public void configure(StateMachineStateConfigurer<String, String> states)
            throws Exception {
        states
                .withStates()
                .initial("PLACED")
                .state("PROCESSING")
                .state("SENT")
                .state("DELIVERED");
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<String, String> transitions)
            throws Exception {
        transitions
                .withExternal()
                .source("PLACED").target("PROCESSING")
                .event("PROCESS")
                .and()
                .withExternal()
                .source("PROCESSING").target("SENT")
                .event("SEND")
                .and()
                .withExternal()
                .source("SENT").target("DELIVERED")
                .event("DELIVER");
    }
}

我们通过Persist类来实现核心功能,触发事件,改变数据库中对应记录的状态。

  • listDbEntries:通过JDBCTemplate读取当前数据库中的所有记录。
  • change:用来改变Order对象、Order在数据库中对应的记录及状态机的状态。首先根据入餐id查询对应数据库记录。随后通过 PersistStateMachineHandler触发事件,同时通知PersistStateMachineHandler.PersistStateChangeListener状态发生变化,进行数据库记录状态更新操作。
  • PersistStateMachineHandler.PersistStateChangeListener:当onPersist被调用时,表示状态变化,db状态需要更新,执行数据库更新操作。

前几个例子都是直接通过stateMachine直接发送事件使状态变化。而设置数据库的状态变更时我们用到了PersistStateMachineHandler的handleEventWithState方法。因此每条记录的状态都不一样。因此一旦触发响应的事件,先要重置状态机,将其状态更新为当前记录对应的状态。随后根据发送事件,根据转换判断是否状态可以转换。如果可以转换,那么在PersistStateMachineHandler中注册的PersistStateChangeListener的onPersist方法会被调用,最终通过我们自己操作db的方法实现数据库记录的状态的变更。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.recipes.persist.PersistStateMachineHandler;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.transition.Transition;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

/**
 * Created on 2020/2/23.
 */
public class Persist {
    private final PersistStateMachineHandler handler;

    private final PersistStateMachineHandler.PersistStateChangeListener listener = new LocalPersistStateChangeListener();

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public Persist(PersistStateMachineHandler handler) {
        this.handler = handler;
        this.handler.addPersistStateChangeListener(listener);
    }

    public String listDbEntries() {
        List<Order> orders = jdbcTemplate.query("select id,state from orders",
                new RowMapper<Order>() {
                    @Nullable
                    @Override
                    public Order mapRow(ResultSet resultSet, int i) throws SQLException {
                        return new Order(resultSet.getInt("id"),resultSet.getString("state"));
                    }
                });

        StringBuilder builder = new StringBuilder();
        for (Order order : orders) {
            builder.append(order).append("\n");
        }
        return builder.toString();
    }

    public void change(int order,String event) {
        Order o = jdbcTemplate.queryForObject("select id,state from orders where id = ?", new Object[]{order},
                new RowMapper<Order>() {
                    @Nullable
                    @Override
                    public Order mapRow(ResultSet resultSet, int i) throws SQLException {
                        return new Order(resultSet.getInt("id"),resultSet.getString("state"));
                    }
                });
        handler.handleEventWithState(MessageBuilder.withPayload(event).setHeader("order", order).build(), o.state);
    }

    private class LocalPersistStateChangeListener implements PersistStateMachineHandler.PersistStateChangeListener {
        @Override
        public void onPersist(State<String, String> state, Message<String> message, Transition<String, String> transition, StateMachine<String, String> stateMachine) {
            if(message != null && message.getHeaders().containsKey("order")) {
                Integer order = message.getHeaders().get("order",Integer.class);

                jdbcTemplate.update("update orders set state = ? where id = ?",state.getId(),order);

            }
        }
    }
}

接着完成对象配置。PersistStateMachineHandler在控制数据库状态变更需要依赖StateMachine对象,以便更改其状态。这个类还提供了addPersistStateChangeListener方法,为其添加一个监听器,状态变化就是通知这个方法来进行db的状态变更操作。

@Configuration
public class PersistHandlerConfig {

    @Autowired
    private StateMachine<String, String> stateMachine;

    @Bean
    public Persist persist() {
        return new Persist(persistStateMachineHandler());
    }

    @Bean
    public PersistStateMachineHandler persistStateMachineHandler() {
        return new PersistStateMachineHandler(stateMachine);
    }
}

添加Persist命令配置。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
@ShellComponent
public class PersistCommands {

    @Autowired
    private Persist persist;

    @ShellMethod(key = "persist db", value = "List entries from db")
    public String listDbEntries() {
        return persist.listDbEntries();
    }

    @ShellMethod(key = "persist process", value = "Process order")
    public void process(@ShellOption(value = {"", "id"}, help = "Order id") int order) {
        persist.change(order, "PROCESS");
    }

    @ShellMethod(key = "persist send", value = "Send order")
    public void send(@ShellOption(value = {"", "id"}, help = "Order id") int order) {
        persist.change(order, "SEND");
    }

    @ShellMethod(key = "persist deliver", value = "Deliver order")
    public void deliver(@ShellOption(value = {"", "id"}, help = "Order id") int order) {
        persist.change(order, "DELIVER");
    }
}

import org.springframework.shell.standard.ShellMethod;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.state.State;
import org.springframework.util.StringUtils;

import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;


public class AbstractStateMachineCommands<S, E>{
    
    private StateMachine<S, E> stateMachine;

    protected StateMachine<S, E> getStateMachine() {
        return stateMachine;
    }

    @ShellMethod(key = "sm state", value = "Prints current state")
    public String state() {
        State<S, E> state = stateMachine.getState();
        if (state != null) {
            return StringUtils.collectionToCommaDelimitedString(state.getIds());
        } else {
            return "No state";
        }
    }

    @ShellMethod(key = "sm start", value = "Start a state machine")
    public String start() {
        stateMachine.start();
        return "State machine started";
    }

    @ShellMethod(key = "sm stop", value = "Stop a state machine")
    public String stop() {
        stateMachine.stop();
        return "State machine stopped";
    }


    @ShellMethod(key = "sm variables", value = "Prints extended state variables")
    public String variables() {
        StringBuilder buf = new StringBuilder();
        Set<Entry<Object, Object>> entrySet = stateMachine.getExtendedState().getVariables().entrySet();
        Iterator<Entry<Object, Object>> iterator = entrySet.iterator();
        if (entrySet.size() > 0) {
            while (iterator.hasNext()) {
                Entry<Object, Object> e = iterator.next();
                buf.append(e.getKey() + "=" + e.getValue());
                if (iterator.hasNext()) {
                    buf.append("\n");
                }
            }
        } else {
            buf.append("No variables");
        }
        return buf.toString();
    }

}

最后我们通过一组例子来进行验证。首先mysql中有4条记录。初始状态均是PLACED。

id  state
1	PLACED
2	PLACED
3	PLACED

当我们执行persist process 1命令时,记录1的状态会更新为PROCESSING。同理操作2,3对应的状态也会发生变化。每次在操作一条记录状态时handler都会先重置当前状态机的状态为对应记录的状态,然后在根据事件完成相应的转移。

总结

状态机控制数据库中对应记录的变化主要通过PersistStateMachineHandler与PersistStateMachineHandler.PersistStateChangeListener。PersistStateMachineHandler负责根据数据库记录重置当前状态机状态。当状态变化后通知PersistStateChangeListener完成数据库状态变化操作。此外涉及数据中记录变化的状态机配置时,我们通过字符串来描述状态即事件。

参考

[1].persist docs,https://docs.spring.io/spring-statemachine/docs/2.2.0.RELEASE/reference/#statemachine-examples-persist
[2].persist source code,https://github.com/spring-projects/spring-statemachine/tree/2.2.0.RELEASE/spring-statemachine-samples/persist/src/main/java/demo/persist
[3].JdbcTemplate,https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值