SpringBoot集成spring-statemachine状态机实现业务流程
一、前言
在工作中经常会遇到业务流程
的实现(事件审批、请假任务审批流程等),常见的实现方式是简单的业务流程使用硬编码,通过简单状态status实现,复杂的业务流程使用流程引擎实现(Flowable、Activiti、Camunda等),但是流程引擎实现比较复杂,对一些不常变更的和简单的业务流程使用流程引擎比较大材小用;
在一些比较简单的业务流程中,经常使用硬编码的方式实现,即多个if(){}else if(){}....else if(){}
,代码耦合性比较强,并在流程变更时,需要修改的代码比较多,整体逻辑比较混乱,不利于维护;所以需要寻一种硬编码和流程引擎之外的实现方式;
在设计模式中有状态设计模式,能够使业务流程更加清晰一些,但是业务流程的流向存在多种,不能完全满足,另一种方式就是状态机。
在实际的软件开发中,状态模式
并不是很常见,但是在能够用到的场景里,可以发挥比较大的作用,状态模式一般用来实现状态机,而状态机常用在游戏、工作引擎等系统开发中,不过,状态机
的实现方式有很多种,除了状态模式,比较常见的还有分支逻辑法和查表法。
二、状态机概念
状态机,就是当一个对象状态转换的条件表达式过于复杂的时候,把状态的判断逻辑转换到不同状态的一系列类当中去;
状态机有 3 个组成部分:状态(State)
、事件(Event)
、动作(Action)
。其中,事件也称为转移条件(Transition Condition)
。事件触发状态的转移及动作的执行;
状态机可以通过状态设计模式实现,也可以使用已由的组件实现,实现更加方便,常见的有两种:
1、spring的spring-statemachine;
2、阿里的Cola-StateMachine;
本文重点介绍第一种实现方式,第二种实现方式的资料较少,后续再做介绍;
状态机概念不再赘述
;
三、spring-statemachine状态机
Spring State Machine
是 Spring 框架的一部分,提供了一套用于处理有限状态机(FSM)的API。它可以帮助开发者定义状态转换
、行为
、触发条件
等,适用于复杂的业务流程、工单系统、订单系统等场景。
Spring Statemachine 项目模块:
官网地址1
Spring Statemachine
的设计是有状态的,每个statemachine实例内部保存了当前状态机上下文相关的属性(比如当前状态、上一次状态等),因此是线程不安全的。因此,在实际场景中,基本上都是利用工厂创建状态机。
通过定义,我们很容易分析得到状态机应当具备一下几个要素:
1、当前状态:也就是状态流转的起始状态。
2、触发事件:引起状态之间流转的一些列动作。
3、响应函数:触发事件到下一个状态之间的规则。
4、目标状态:状态流转的目标状态。
注:可以使用使用 Zookeeper 的分布式状态机
四、审批流程(实例)
这里以请假审批流程为例,工单业务流程图,如下:
状态有:草稿、领导审批、HR审批、拒绝、完成
触发事件:提交、同意、拒绝
代码工程目录,如下:
pom.xml
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>2.3.1</version>
</dependency>
TaskState.java
package com.sk.statemachine.bean;
public enum TaskState {
// 未提交,领导审批中,HR审批中,完成
UN_SUBMIT,
LEADER_CHECK,
HR_CHECK,
FINISH;
}
TaskEvent.java
package com.sk.statemachine.bean;
public enum TaskEvent {
//提交,通过,拒绝
SUBMIT,
PASS,
REJECT;
}
TaskBean.java 工单信息
package com.sk.statemachine.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TaskBean {
private Long id;
private TaskState taskState;
}
TaskStatusMachineConfig.java
package com.sk.statemachine.config;
import com.alibaba.fastjson.JSONObject;
import com.sk.statemachine.bean.TaskBean;
import com.sk.statemachine.bean.TaskEvent;
import com.sk.statemachine.bean.TaskState;
import com.sk.statemachine.utils.RedisUtil;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.StateMachinePersist;
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;
import java.util.EnumSet;
/**
* @description: 订单状态机
*/
@Log4j2
@Configuration
@EnableStateMachine(name = "taskStateMachine")
public class TaskStatusMachineConfig extends StateMachineConfigurerAdapter<TaskState, TaskEvent> {
/**
* 配置状态
*/
@Override
public void configure(StateMachineStateConfigurer<TaskState, TaskEvent> states) throws Exception {
states.withStates()
.initial(TaskState.UN_SUBMIT)
//.end(TaskState.FINISH)
.states(EnumSet.allOf(TaskState.class));
}
/**
* 配置状态转换事件关系
*/
@Override
public void configure(StateMachineTransitionConfigurer<TaskState, TaskEvent> transitions) throws Exception {
transitions.withExternal().source(TaskState.UN_SUBMIT).target(TaskState.LEADER_CHECK)
.event(TaskEvent.SUBMIT)
.and()
.withExternal().source(TaskState.LEADER_CHECK).target(TaskState.HR_CHECK)
.event(TaskEvent.PASS)
.and()
.withExternal().source(TaskState.LEADER_CHECK).target(TaskState.UN_SUBMIT)
.event(TaskEvent.REJECT)
.and()
.withExternal().source(TaskState.HR_CHECK).target(TaskState.FINISH)
.event(TaskEvent.PASS)
.and()
.withExternal().source(TaskState.HR_CHECK).target(TaskState.UN_SUBMIT)
.event(TaskEvent.REJECT);
}
}
TaskService.java
package com.sk.statemachine.service;
import com.sk.statemachine.bean.TaskBean;
import com.sk.statemachine.bean.TaskEvent;
import com.sk.statemachine.bean.TaskState;
import com.sk.statemachine.utils.RedisUtil;
import lombok.SneakyThrows;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.persist.DefaultStateMachinePersister;
import org.springframework.statemachine.persist.StateMachinePersister;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @description: 订单服务
*/
@Service
public class TaskService {
@Resource
private StateMachine<TaskState, TaskEvent> taskStateMachine;
@Resource
private StateMachinePersister<TaskState,TaskEvent,TaskBean> stateMachineMemPersister;
//@Resource
//private DefaultStateMachinePersister<TaskState,TaskEvent,TaskBean> defaultStateMachinePersister;
public Boolean process(TaskBean taskBean, TaskEvent taskEvent){
Message<TaskEvent> message = MessageBuilder.withPayload(taskEvent)
.setHeader("task",taskBean).build();
boolean flag = sendEvent(message);
return flag;
}
/**
* 发送状态转换事件
* @param message
* @return
*/
@SneakyThrows
private boolean sendEvent(Message<TaskEvent> message) {
TaskBean taskBean = (TaskBean) message.getHeaders().get("task");
taskStateMachine.start();
stateMachineMemPersister.restore(taskStateMachine,taskBean);
boolean result = taskStateMachine.sendEvent(message);
stateMachineMemPersister.persist(taskStateMachine, taskBean);
return result;
}
}
TaskStateListener.java
package com.sk.statemachine.service;
import com.sk.statemachine.bean.TaskBean;
import com.sk.statemachine.bean.TaskState;
import lombok.extern.log4j.Log4j2;
import org.springframework.messaging.Message;
import org.springframework.statemachine.annotation.OnTransition;
import org.springframework.statemachine.annotation.WithStateMachine;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* @description: 状态监听
*/
@Log4j2
@Component
@WithStateMachine(name = "taskStateMachine")
public class TaskStateListener {
// private long id = 0L;
@OnTransition(source = "UN_SUBMIT", target = "LEADER_CHECK")
public boolean submitTransition(Message message) {
TaskBean taskBean = (TaskBean) message.getHeaders().get("task");
//taskBean.setId(id++);
taskBean.setTaskState(TaskState.LEADER_CHECK);
log.info("提交请假工单:" + message.getHeaders().toString());
return true;
}
@OnTransition(source = "LEADER_CHECK", target = "HR_CHECK")
public boolean passLeadTransition(Message message) {
TaskBean taskBean = (TaskBean) message.getHeaders().get("task");
taskBean.setTaskState(TaskState.HR_CHECK);
log.info("领导审核工单通过:" + message.getHeaders().toString());
return true;
}
@OnTransition(source = "LEADER_CHECK", target = "UN_SUBMIT")
public boolean rejectLeadTransition(Message message) {
TaskBean taskBean = (TaskBean) message.getHeaders().get("task");
taskBean.setTaskState(TaskState.UN_SUBMIT);
log.info("领导驳回工单:" + message.getHeaders().toString());
return true;
}
@OnTransition(source = "HR_CHECK", target = "FINISH")
public boolean passHRTransition(Message message) {
TaskBean taskBean = (TaskBean) message.getHeaders().get("task");
taskBean.setTaskState(TaskState.FINISH);
log.info("HR审核工单通过:" + message.getHeaders().toString());
return true;
}
@OnTransition(source = "HR_CHECK", target = "UN_SUBMIT")
public boolean rejectHRTransition(Message message) {
TaskBean taskBean = (TaskBean) message.getHeaders().get("task");
taskBean.setTaskState(TaskState.UN_SUBMIT);
log.info("HR驳回工单:" + message.getHeaders().toString());
return true;
}
}
RedisTaskPersist.java 工单数据持久化,这里使用redis,正常场景使用数据库
package com.sk.statemachine.config.persist.redis;
import com.alibaba.fastjson.JSONObject;
import com.sk.statemachine.bean.TaskBean;
import com.sk.statemachine.bean.TaskState;
import com.sk.statemachine.utils.RedisUtil;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.StateMachinePersist;
import org.springframework.statemachine.persist.DefaultStateMachinePersister;
import org.springframework.statemachine.support.DefaultStateMachineContext;
import javax.annotation.Resource;
@Log4j2
@Configuration
public class RedisTaskPersist<E, S> {
@Resource
private RedisUtil redisUtil;
@Bean(name = "stateMachineMemPersister")
public DefaultStateMachinePersister persister() {
return new DefaultStateMachinePersister<>(new StateMachinePersist<Object, Object, TaskBean>() {
@Override
public void write(StateMachineContext<Object, Object> stateMachineContext, TaskBean taskBean) throws Exception {
redisUtil.set(String.valueOf(taskBean.getId()), JSONObject.toJSON(taskBean).toString());
log.info("--------------persister-write:{}", taskBean.toString());
}
@Override
public StateMachineContext<Object, Object> read(TaskBean taskBean) throws Exception {
String task = (String) redisUtil.get(String.valueOf(taskBean.getId()));
TaskBean getTaskBean = JSONObject.parseObject(task, TaskBean.class);
//return new DefaultStateMachineContext<>(getTaskBean.getTaskState(), null, null, null);
return null == getTaskBean ?
new DefaultStateMachineContext<>(TaskState.UN_SUBMIT, null, null, null)
: new DefaultStateMachineContext<>(getTaskBean.getTaskState(), null, null, null);
}
});
}
}
InitAction.java测试调用
package com.sk.statemachine.init;
import com.sk.statemachine.bean.TaskBean;
import com.sk.statemachine.bean.TaskEvent;
import com.sk.statemachine.bean.TaskState;
import com.sk.statemachine.service.TaskService;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
@Log4j2
@Configuration
public class InitAction implements ApplicationRunner {
@Resource
private TaskService taskService;
@Override
public void run(ApplicationArguments args) throws Exception {
TaskBean taskBean = new TaskBean();
//boolean result = taskService.process(new TaskBean(1L, TaskState.UN_SUBMIT), TaskEvent.SUBMIT);
boolean result = taskService.process(new TaskBean(1L, TaskState.HR_CHECK), TaskEvent.REJECT);
log.info("----------------------result:{}", result);
}
}
五、总结
Spring Statemachine 目前在生产中运用尤其是高并发场景下使用的案例不多,迭代版本也较少,如下:
最新一次更新时间是2023年12月份,迭代比较慢,但是spring社区资料还是比较全的;Cola-StateMachine的资料更少,社区相关内容更少,基于Cola-StateMachine
开发的cmt-statemachine可借鉴
----------------------------------👇👇👇注:源码请关注公众号获取
👇👇👇--------------------------------------------