背景介绍
本系列通过学习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