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 MachineSpring 框架的一部分,提供了一套用于处理有限状态机(FSM)的API。它可以帮助开发者定义状态转换行为触发条件等,适用于复杂的业务流程工单系统订单系统等场景。
Spring Statemachine 项目模块:
在这里插入图片描述
官网地址1
Spring Statemachine 的设计是有状态的,每个statemachine实例内部保存了当前状态机上下文相关的属性(比如当前状态、上一次状态等),因此是线程不安全的。因此,在实际场景中,基本上都是利用工厂创建状态机。

通过定义,我们很容易分析得到状态机应当具备一下几个要素:
1、当前状态:也就是状态流转的起始状态。
2、触发事件:引起状态之间流转的一些列动作。
3、响应函数:触发事件到下一个状态之间的规则。
4、目标状态:状态流转的目标状态。

注:可以使用使用 Zookeeper 的分布式状态机

spring-statemachine官网地址2

四、审批流程(实例)

这里以请假审批流程为例,工单业务流程图,如下:
在这里插入图片描述
状态有:草稿、领导审批、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可借鉴

----------------------------------👇👇👇注:源码请关注公众号获取👇👇👇--------------------------------------------

  • 17
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dylan~~~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值