实现一个简易的流程引擎(含源码)

写在前面

​ 前面剖析了状态机的源码之后,怎能不实操一番呢?如果前面对COLA状态机的源码有所了解,那么对其的使用不能说是信手拈来,也可以说手拿把攥。因而我实现了一个小的,简易的流程引擎,能支持的操作有限,但若是能看清其中的原理,定制化仿写也相当简单。

​ 在该案例中,数据库我使用的是sqlite,项目架构springboot3,编码语言Java17,ORM框架使用mybatis-plus,没有前台页面与接口暴露。因此如果在该案例中你没有看懂该状态机是如何实现流程驱动的,那么将之看作sqlite+springboot+mybatisplus的整合方案,我相信也能有所收获。

一.流程流转

​ 既然是实现一个简易的流程引擎,流程图必不可少。先看看我自己画的流程图,了解其状态改变逻辑。

在这里插入图片描述

整个图形很简单,该流程引擎除了支持正常流程流转外,我只实现了退回,退回上一步,挂起与取消挂起的操作。

​ 因为在这里我的想法是用一个状态机实现,所以流程需求并不复杂。在实际使用中,如果涉及到复杂需求,状态机又不好定义时,我的建议是定义多个状态机搭配使用,共同驱动流程。比如上图我就可以优化为一个外部状态机和一个内部状态机,外部的做异常状态驱动,内部的做正常状态驱动。

​ 但我这里的实现是整合了一个状态机。先看看状态机定义的代码:

/**
 * @author : singularity
 * @date : 2023/12/12 19:34
 * @description : 状态机配置类
 */
@Configuration
public class MyFlowApiStateMachineConfig {
    @Resource
    FlowApiCheckCondition flowApiCheckCondition;
    @Resource
    FindFirstNodeAction findFirstNodeAction;
    @Resource
    PassAndFindNextNodeAction passAndFindNextNodeAction;
    @Resource
    PassAndDoneAction passAndDoneAction;
    @Resource
    PassAndBackAction passAndBackAction;
    @Resource
    PassAndReturnAction passAndReturnAction;
    @Resource
    PassAndHangupAction passAndHangupAction;
    @Resource
    PassAndCancelHangupAction passAndCancelHangupAction;

    @Bean
    public StateMachine<BillState, BillSateEvent, BillEntity> billEntityFlowApiStateMachine(){
        String machineId = "FlowApiStateMachine_0001";
        //参照test的样例实例化一个状态机
        StateMachineBuilder<BillState,BillSateEvent,BillEntity> builder = StateMachineBuilderFactory.create();
        //创建
        builder.externalTransition().from(BillState.CS).to(BillState.CL).on(BillSateEvent.CJ).when(flowApiCheckCondition.isAllowCreateBill()).perform(findFirstNodeAction.doAction());
        //通过
        builder.internalTransition().within(BillState.CL).on(BillSateEvent.TJ).when(flowApiCheckCondition.isPassAndFindNextNode()).perform(passAndFindNextNodeAction.doAction());
        //结束
        builder.externalTransition().from(BillState.CL).to(BillState.JS).on(BillSateEvent.TJJS).when(flowApiCheckCondition.isPassAndDone()).perform(passAndDoneAction.doAction());
        //退回
        builder.externalTransition().from(BillState.CL).to(BillState.TH).on(BillSateEvent.TH).when(flowApiCheckCondition.isAllowReturn()).perform(passAndBackAction.doAction());
        //退回上一步
        builder.internalTransition().within(BillState.CL).on(BillSateEvent.THSYB).when(flowApiCheckCondition.isAllowReturnLastNode()).perform(passAndReturnAction.doAction());
        //挂起
        builder.externalTransition().from(BillState.CL).to(BillState.GQ).on(BillSateEvent.GQ).when(flowApiCheckCondition.isAllowHangup()).perform(passAndHangupAction.doAction());
        //取消挂起
        builder.externalTransition().from(BillState.GQ).to(BillState.CL).on(BillSateEvent.QXGQ).when(flowApiCheckCondition.isAllowCancelHangup()).perform(passAndCancelHangupAction.doAction());
        //重新提起
        builder.externalTransition().from(BillState.TH).to(BillState.CL).on(BillSateEvent.CJ).when(flowApiCheckCondition.isAllowCreateBill()).perform(findFirstNodeAction.doAction());

        StateMachine<BillState, BillSateEvent, BillEntity> stateMachine = builder.build(machineId);
        System.out.println(stateMachine.generatePlantUML());
        return stateMachine;
    }

}

我们看看绘制好的uml图形是怎样的。

在这里插入图片描述

如果觉得不是很好理解,我这里贴上掩码。

package com.singularity.flowapi.bean.billenum;

/**
 * @author : singularity
 * @date : 2023/12/12 19:39
 * @description : 状态事件
 */
public enum BillSateEvent {

    CJ("创建"),
    TJ("提交"),
    TJJS("提交结束"),
    TH("退回"),
    GQ("挂起"),
    CXFQ("重新发起"),
    QXGQ("取消挂起"),
    THSYB("退回上一步")
    ;
    private String name;
    BillSateEvent(String name){
        this.name = name;
    }
}

/**
 * @author : singularity
 * @date : 2023/12/12 19:37
 * @description : 状态
 */
public enum BillState {
    CS("CS"),
    CL("CL"),
    TH("TH"),
    GQ("GQ"),
    JS("JS");

    private String name;
    BillState(String name){
        this.name = name;
    }
}

​ 至此,我们这台状态机是怎么运作的,就基本说完了。

二.状态机填充完善

​ 如果说,上面的状态机定义,是一个大骨架。那么接下来的操作,就需要为其填充血肉。

​ 在状态机定义类里面可以看见引入了一个condition类和无数的action类。简单来说,condition类为我们明确是否允许进行状态变更,而action类则是状态变更了,需要做的操作是什么。(例子中有一些mapper和service可以先不用管,涉及业务下一节会讲)

/**
 * @author : singularity
 * @date : 2023/12/12 19:59
 * @description : description
 */
@Repository
public class FlowApiCheckCondition {

    @Resource
    FlowMessageService flowMessageService;

    public Condition<BillEntity> isPassAndFindNextNode(){
        return new Condition<BillEntity>() {
            @Override
            public boolean isSatisfied(BillEntity bill) {
                return  BillSateEvent.TJ.name().equals(bill.getOpinion());
            }
        };
    }
    public Condition<BillEntity> isPassAndDone(){
        return new Condition<BillEntity>() {
            @Override
            public boolean isSatisfied(BillEntity bill) {
                List<FlowMessage> flowMessages = flowMessageService.queryAllFlowMessageById(bill.getFlowId());
                return  BillSateEvent.TJ.name().equals(bill.getOpinion()) && Objects.equals(bill.getNodeId(), flowMessages.get(flowMessages.size() - 1).getFlowNode());
            }
        };
    }

    public Condition<BillEntity> isAllowReturnLastNode(){
        return new Condition<BillEntity>() {
            @Override
            public boolean isSatisfied(BillEntity bill) {
                return  BillSateEvent.THSYB.name().equals(bill.getOpinion());
            }
        };
    }

    public Condition<BillEntity> isAllowReturn(){
        return new Condition<BillEntity>() {
            @Override
            public boolean isSatisfied(BillEntity bill) {
                return  BillSateEvent.TH.name().equals(bill.getOpinion());
            }
        };
    }

    public Condition<BillEntity> isAllowHangup(){
        return new Condition<BillEntity>() {
            @Override
            public boolean isSatisfied(BillEntity bill) {
                return  BillSateEvent.GQ.name().equals(bill.getOpinion());
            }
        };
    }

    public Condition<BillEntity> isAllowCancelHangup(){
        return new Condition<BillEntity>() {
            @Override
            public boolean isSatisfied(BillEntity bill) {
                return  BillSateEvent.QXGQ.name().equals(bill.getOpinion());
            }
        };
    }

    public  Condition<BillEntity> isAllowCreateBill() {
        return new Condition<BillEntity>() {
            @Override
            public boolean isSatisfied(BillEntity bill) {
                List<FlowMessage> flowMessages = flowMessageService.queryAllFlowMessageById(bill.getFlowId());
                return !flowMessages.isEmpty();
            }
        };
    }
}

condition我做的比较简单,就是单据处理意见和要处理的事件相符合,那么就允许状态变更。

action太多,我就不放完了,放一个示例说明一下。

/**
 * @author : singularity
 * @date : 2023/12/12 19:58
 * @description : description
 */
@Repository
public class PassAndReturnAction{
    @Resource
    FlowMessageServiceImpl flowMessageService;
    @Resource
    BillStateMapper billStateMapper;

    public Action<BillState, BillSateEvent, BillEntity> doAction() {
        return (from, to, event, ctx) -> {
            List<FlowMessage> flowMessages = flowMessageService.queryAllFlowMessageById(ctx.getFlowId());
            if ("01".equals(ctx.getNodeId()) || StringUtils.isBlank(ctx.getNodeId())) {
                throw new RuntimeException("PassAndReturnAction:[初始节点] 不允许退回上一步。");
            }
            AtomicInteger i = new AtomicInteger();
            flowMessages.forEach(flowMessage -> {
                if (flowMessage.getFlowNode().equals(ctx.getNodeId())) {
                    i.set(flowMessages.indexOf(flowMessage));
                }
            });
            UpdateWrapper<BillEntity> updateWrapper = new UpdateWrapper<>();
            updateWrapper.set("node_id",flowMessages.get(i.get()-1)).set("bill_status",BillState.TH.name()).eq("bill_id",ctx.getBillId());
            billStateMapper.update(ctx, updateWrapper);
            System.out.println("doAction");
        };
    }
}

​ 这个示例就是退回的action。

三.带入实际业务

​ 如果说上面两部分状态机的定义看的你一脸懵的话,无妨,这很正常。关于上面的内容,非常好解释,只需记住这样一个过程就行:状态A经过事件B转换为状态C,其中校验条件为Condition,状态变更成功后的行为是action。因此可以猜测出来,有很多的condition和action,因为各个状态间变换的condition和action都不一样,实际上也是这样,为了更好理解这一点,我们带入实际业务。

​ 假定工单A,需要走流程,别人来处理流程。最基础的也需要两个表,一个工单表,一个流程信息表。

-- 先忽略员工,岗位,部门,工单日志等等一系列表
create table bill_base_message
(
    bill_id     CHAR(36)  not null
        primary key,
    bill_title  CHAR(100) not null,
    bill_status CHAR(30),
    node_id     CHAR(20),
    flow_id     CHAR(20),
    opinion     CHAR(20)
);



create table base_flow_message
(
    flow_id   CHAR(20) not null,
    flow_name CHAR(60),
    flow_node CHAR(10) not null,
    node_name CHAR(60)
);

create unique index base_flow_message_u01
    on base_flow_message (flow_id, flow_node);


随即插入流程数据,这个是基准数据,工单数据测试时再插入。

在这里插入图片描述

按照mybatisplus框架将相关类及文件定义好

在这里插入图片描述

接下来再定义一个接口,这个接口的意义在于,减少业务代码,事实上这个接口的代码也可以写在状态机调用之前的各个业务模块,但整合到这个接口可以解耦。

/**
 * @author : singularity
 * @date : 2023/12/13 20:48
 * @description : description
 */
public interface FlowService {

     void createBill(BillEntity bill);
     void submitBill(BillEntity bill);
     void backBill(BillEntity bill);
     void returnBill(BillEntity bill);
     void hangUp(BillEntity bill);
     void cancelHangup(BillEntity bill);


}

实现类这里也写的较简单:

/**
 * @author : singularity
 * @date : 2023/12/12 20:48
 * @description : description
 */
@Slf4j
@Service
public class FlowServiceImpl implements FlowService {
    @Resource
    StateMachine<BillState, BillSateEvent, BillEntity> flowApiStateMachine;
    @Resource
    FlowMessageServiceImpl flowMessageService;
    @Override
    public void createBill(BillEntity bill) {
         log.info("订单"+bill.getBillId()+"已成功更新状态为:"+flowApiStateMachine.fireEvent(BillState.valueOf(bill.getBillStatus()), BillSateEvent.CJ, bill));
    }

    @Override
    public void submitBill(BillEntity bill) {
        List<FlowMessage> flowMessages = flowMessageService.queryAllFlowMessageById(bill.getFlowId());
        if (flowMessages.get(flowMessages.size()-1).getFlowNode().equals(bill.getNodeId())) {
            log.info("订单"+bill.getBillId()+"已成功更新状态为:"+flowApiStateMachine.fireEvent(BillState.valueOf(bill.getBillStatus()), BillSateEvent.TJJS, bill));
        }else {
            log.info("订单"+bill.getBillId()+"已成功更新状态为:"+flowApiStateMachine.fireEvent(BillState.valueOf(bill.getBillStatus()), BillSateEvent.TJ, bill));
        }

    }

    @Override
    public void backBill(BillEntity bill) {
        log.info("订单"+bill.getBillId()+"已成功更新状态为:"+flowApiStateMachine.fireEvent(BillState.valueOf(bill.getBillStatus()), BillSateEvent.TH, bill));
    }

    @Override
    public void returnBill(BillEntity bill) {
        log.info("订单"+bill.getBillId()+"已成功更新状态为:"+flowApiStateMachine.fireEvent(BillState.valueOf(bill.getBillStatus()), BillSateEvent.THSYB, bill));
    }

    @Override
    public void hangUp(BillEntity bill) {
        log.info("订单"+bill.getBillId()+"已成功更新状态为:"+flowApiStateMachine.fireEvent(BillState.valueOf(bill.getBillStatus()), BillSateEvent.GQ, bill));
    }

    @Override
    public void cancelHangup(BillEntity bill) {
        log.info("订单"+bill.getBillId()+"已成功更新状态为:"+flowApiStateMachine.fireEvent(BillState.valueOf(bill.getBillStatus()), BillSateEvent.QXGQ, bill));
    }


}

​ 看到这里总结一下可以在哪些位置进行判断能否变换状态,到数据持久化(最终目的)之前,我们一共会有三个位置,调用的业务代码(上文的flowserviceimpl),状态机condition,状态机的action。我的看法是,基准的,不涉及业务数据的判断,放在condition中,action中是最终校验,很可能出现状态机通过,但在action不给予数据持久化。业务代码中就判断涉及到业务数据的。

​ 举个例子。工单A为员工差旅报销业务(flowId),需要判断的条件有3个。1.超额报销不属于当前业务,应退回重新发起。2.输入金额与系统计算金额不符(税率,汇率等影响)。3.收款人与发单人不符(不允许代提)。在该例子中,我个人认为,条件1的判断应放在业务代码中,因为这是进入状态机之前的大基准,方向错了,再怎么试都是徒劳无功。条件2应放在condition中,输入金额与系统计算金额和是哪个业务,是谁提起的无关,它只关注这两个金额相等与否,第三点类似,但我却认为第三点可以放到action中去。因为前文我提到过,很有可能状态机通过了,但实际业务不通过的情况。这里,就可以这么理解(可以看作实际业务就是付款),纸面上一切条件都符合,状态已经变更,就等付款了,但在付款的action中,条件判断有误,立即终止。

​ 但其实这里也不是什么重要的点,只是我个人认为的逻辑规范。大前提 -> 正常条件判断 -> 兜底条件判断。

四.效果测试

​ 该定义的都定义了,我们直接看效果。先插入一条初始状态的工单。

在这里插入图片描述

在这里插入图片描述

4.1发起

直接创建提上流程。

在这里插入图片描述

状态机通过。

在这里插入图片描述

持久化到数据库。

4.2 提交

模拟提交操作

在这里插入图片描述
在这里插入图片描述

状态机内部流转,无状态改变(状态机通过)。

在这里插入图片描述

流转到下一节点。

4.3退回

模拟退回

在这里插入图片描述

在这里插入图片描述

状态机通过

在这里插入图片描述

退回状态,无法处理。

4.4重新发起

退回单据模拟重新发起

在这里插入图片描述

在这里插入图片描述

状态机通过。

在这里插入图片描述

数据成功持久化。

4.5挂起

模拟挂起

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

状态机通过

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

成功持久化

4.6取消挂起

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.6结束

现将工单一直提交到第四节点(共4个节点)。注:节点任意定义,CL(处理)状态视为内部流转,可一直循环。

在这里插入图片描述

在这里插入图片描述

如果再submit,是否会报错呢?

答案是不会。
在这里插入图片描述

在这里插入图片描述

因为在业务处理代码中做了不同处理,走的是不同状态变换。

在这里插入图片描述

4.7异常情况

举个例子,单据无节点信息,就要挂起。

在这里插入图片描述

20231213201641952.png&pos_id=img-DFoa3ZE7-1702822221147)
在这里插入图片描述

这个错是我在代码中手抛的,此外状态机已经能帮我们避免很多错误。

比如:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这里插入图片描述

在这里插入图片描述

这个组合虽然没有报错,但是实际上是没有执行代码的,因为我们需要更改的状态是CS->GQ,没有这样的组合,状态机就不会执行代码。比如GQ ->GQ这样的状态变换,就不用手动去做了。

写在最后

​ 这个demo作为我学习COLA的一个小demo,以及整合sqllits及springboot3及mybatis-plus的小demo。还有不少不足之处,比如模型太过简易,还没有支持跳过节点,会签等情况。但单从状态机学习来说,这个demo练手足矣。

​ 源代码:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
SpringBoot是一个开发框架,能够简化Java应用的开发过程,而Vue是一种用于构建用户界面的JavaScript框架。OA(Office Automation)则是办公自动化的缩写,是指利用信息技术来实现办公流程的自动化,包括工作流程、协同办公、文档管理等。 SpringBoot与Vue可以结合使用来开发OA系统。首先,我们可以使用SpringBoot来构建后端的服务,提供API接口给前端调用。SpringBoot可以帮助我们快速搭建项目结构,集成数据库访问、认证授权、消息队列等常用功能。同时,SpringBoot还有丰富的第三方依赖库和插件,可以方便地集成其他的组件和工具。 而Vue则可以作为前端的框架,用于构建用户界面和处理用户交互。Vue有着良好的响应式设计和组件化开发模式,可以提高开发效率和代码复用性。Vue可以与SpringBoot通过API进行数据交互,实现前后端的数据传输和状态管理。 对于OA流程实现,可以使用工作流引擎来管理流程,如Activiti。Activiti是一个开的BPM(Business Process Management)平台,可以帮助我们实现流程定义、流程实例管理和任务分配等功能。通过Activiti,我们可以将整个OA流程进行建模,包括流程图的设计、任务节点的定义和流程变量的设置。 OA流程码可以通过使用SpringBoot和Vue来进行开发。后端可以使用SpringBoot来构建API接口,并集成Activiti来实现流程管理。前端可以使用Vue来构建用户界面,并通过API调用后端的服务。通过这种方式,我们可以利用SpringBoot和Vue的优势来快速开发和部署OA流程应用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值