最近有一段时间没写博客了,本来打算写写对于工作流的心得,但是工作时间比较饱和只好延后。最初接触工作流是上一家公司工作,具体我不透露哪家公司,只是感受到人情冷暖,或许公司都是这样,当你的价值被用完了也就是你走人的时候。好了,废话不多说,我们直接进入主题。
前言
对于框架的选型,我推荐使用flowable框架,在最初的项目选型是选择activiti的,但是深入去了解框架的时候发现activiti还是有一些坑的,而flowable正是activiti框架的修正版,据了解flowable的背景是activiti原班人马开发出来的框架,而主导这个框架上更是得心应手,也修复了activiti的诸多bug。可能很多开发人员在框架选型上比较困扰,一方面出于对学习成本的考虑,一方面对框架性能、稳定性的考虑。在这里,我认为一个框架是否优秀的评判标准,不能取决于用的人多,而在于它的功能是否丰富,性能是否优越,后期扩展是否灵活等等,综合考虑下去选择。
入门详解
考虑到读者会先了解一下flowable框架是否满足自身的项目需求,所以我会先入门讲解一下flowable框架的大致功能,后续再进行框架搭建。
flowable官方手册
对于flowable的了解也是来源于flowable的中文版手册,里面介绍得很详细,而我下面对一些常用的功能进行归纳总结。
应用
- 那么在什么场景下可以使用工作流呢?
比如,你实际的业务需求比较流程化,可能每一个步骤都需要有相关的人进行审批,直到最后结束,就像一条生产线一样,那么你就要考虑应用工作流框架了。举个例子:请假审批流程、财务审批流程、入职流程等等。
- 使用这个工作流有什么好处(优势)?
其实工作流框架在功能的应用上可能看不出来有什么明显的变化,但是对于内部代码上却可以很好的解耦,使得业务代码只需要关心当前的业务,而不需要进行流程的逻辑判断,一切交由工作流框架进行流转。除此之外,如果项目的流程化业务多那么接入工作流的好处就是可以将多个流程归结起来统一管理进行监控、性能分析等等。
举个例子:某业务上有三个步骤,每个步骤需要有相关人员进行审核,才能流转到下一步骤,每个步骤都有自己的业务逻辑。遇到上面的情况,第一种不整合工作流的做法就是三个步骤连在一起写,包括流转到下一步骤的判断,审核人员也需要根据在哪一步骤去判断获取相关人员,那么这种做法可行吗?可行,但是写起来代码臃肿,在代码上我们可以想办法把每个步骤抽离出来,但是如果步骤很多而且很多都是相同的逻辑,那这个代码看起来就不优雅了。有没有更加优雅的做法?有的,那就是使用工作流。
工作流的做法如下:
1. 流程画图,主要是通过可视化工具进行画图生成一个xml。
2. 流程部署,将流程图部署起来,写入到数据库中
3. 编写每个步骤的业务逻辑,如果多个步骤中的业务逻辑有相同的,则只写一个就可以了
4. 将写好的业务逻辑挂在在流程节点上
5. 启动项目,执行相关业务功能
对于上面两种做法,你可以理解为第一种做法有点类似于面向过程,就是事无大小都是自己亲力亲为去执行,第二种做法有点类似于面向对象,工作流充当指挥者,指挥着每个碎片化的业务逻辑,孰优孰略显而易见。
- 接入工作流难吗?
不难,建议多参考官方手册,后面会说说搭建的问题,还有一些需要注意的点。
- 如何画流程图?
首先你需要下载一个flowable的插件,画图也容易,都是组件式拖拽完成画图,然后在里面配置参数。
功能
我先大体介绍一下flowable里面到底有哪些功能,后面再去详细地整合flowable框架,下面就先看看功能点欣赏一下就好~
- 流程部署
Deployment deployment = repositoryService.createDeployment)
.nameprocess_name)
.addClasspathResourceresource_path)
.deploy);
-
process_name 传入一个流程定义名称,这个名称代表着这个流程定义模版的唯一命名,在发起流程需要根据唯一命名进行关联。
-
resource_path 传入部署流程图的路径,该文件是一个流程定义的xml文件
-
上述代码是一个部署流程图的代码片段,主要作用是将画好的流程图写入到数据库中,一切的流程运行都是在读取数据库中进行操作
-
启动流程
ProcessInstance processInstance = runtimeService.startProcessInstanceByKeyprocess_name, map);
-
process_name 流程定义名称
-
map 流程变量,根据这些变量实现流程节点的流转,如金额达到10000,才可以给财务人员审批,那么这个金额就是流程变量。
-
上述代码作用是启动一个流程实例,也就是说一个流程定义的模版可以启动多个流程实例。举个例子:如果一个请假流程有大概步骤:写请假条–>找上级领导申请 –> 经理审批 –> 结束。 那么,这个流程我们称为流程定义模版,而张三实际发起的一个流程,称为流程实例。
-
流程从当前节点跳转下一节点
taskService.completetaskId, map);
-
taskId 当前任务ID,这个下面会介绍到怎么获取,一个流程实例当前执行到某一个节点,而执行到当前节点会有一个ID,通过这个ID才能流程到下一节点
-
map 流程变量,传入这个流程变量的作用就是,传入一些参数进去,比如工作流上画了一个分叉条件,它自身是不知道下一步要走到哪边的,是走A节点还是B节点,但是分叉上会设置一些条件判断,这时候就是流程变量发挥作用了,根据传入的流程变量,工作流框架可以识别这些变量并代入到流程条件中进行判断,从而分析出走哪个节点。
-
查询待办列表
上面流程节点跳转下一节点操作里需要传入一个taskId,而这个ID的获取来源于待办列表。待办列表这个概念,往往跟当前操作人的角色挂钩。假设我是财务审批人,那么有些任务是需要我当前办理的,那我就可以看到这个待办任务。你可能很疑惑,怎么才能跟角色挂钩呢?别着急,往下会详细介绍其绑定关系。
List<Task> tasks = taskService.createTaskQuery).processVariableValueEquals"business_type", business_type).processVariableValueEquals"business_id", business_id).orderByTaskCreateTime).desc).list);
- 工作流框架调用方式比较特殊,使用链式调用
- createTaskQuery) 创建一个任务查询
- processVariableValueEqualskey,value) 流程变量判断函数,查询待办列表的条件判断
- orderByTaskCreateTime) 排序节点跳转创建时间
-
获取未结束的流程实例变量
前面有提及到流程变量这个概念,那么我们想在流程还未结束的情况下,在任何时候都能获取到流程变量,可以使用下面的操作:
Map<String, Object> variables = runtimeService.getVariablesexecutionId);
- executionId 流程执行ID, 这个id也在待办的task上获取,也就是说想要拿流程变量还需要调用待办列表,而且必须是这个流程没走完,拿的是当前的executionId,走过的节点的executionId是获取不到的。
-
获取流程历史节点
List<HistoricActivityInstance> activities = historyService.createHistoricActivityInstanceQuery) .processInstanceIdprocessId).activityType"userTask").finished) .orderByHistoricActivityInstanceEndTime).desc).list);
获取流程的历史节点列表,一般应用在提供给用户查看流程的审批历史上。
- createHistoricActivityInstanceQuery) 创建一个活动历史查询
- processInstanceIdprocessId) 查询条件,传入一个流程实例ID
- orderByHistoricActivityInstanceEndTime) 查询条件,根据活动结束时间排序
-
流程跳转任意节点
假设你现在的节点走错了想要退回去上一个节点,或者这个节点你想走到某一个节点,你可以调用一个跳转任意节点的方式实现。
runtimeService.createChangeActivityStateBuilder).processInstanceIdtask.getProcessInstanceId)).moveActivityIdTo"approve1", "apply").changeState);
-
processInstanceIdprocessId) 根据流程实例ID去定位到该流程上实现节点跳转
-
moveActivityIdTo“approve1”, “apply”) 传入两个节点ID,节点ID在流程画图的时候设置好
-
显示流程图
BpmnModel bpmnModel = repositoryService.getBpmnModelpi.getProcessDefinitionId));
ProcessEngineConfiguration engconf = processEngine.getProcessEngineConfiguration);
ProcessDiagramGenerator diagramGenerator = engconf.getProcessDiagramGenerator);
InputStream in = diagramGenerator.generateDiagrambpmnModel,"bmp", activityIds,flows,"宋体","宋体","宋体",null,1.0);
流程图部分代码片段,flowable框架自带的根据流程定义图生成的流程实例图,对当前正在进行的节点标红,可以很好的嵌入到前端给用户展示。
搭建flowable
flowable搭建整理
角色权限
接上述待办列表提及到的角色绑定关系,这里来详细介绍如何做到查询待办可以只让当前操作人看到。
工作流的流程节点上是有用户和用户组的概念的,那么我们在流程画图的时候需要先提前指定该节点是由用户组操作还是由用户操作,这里的用户组其实说的就是“一群人“可以完成这个节点的操作,如何代表”一群人“,那么你需要有一个key值去映射多用户的关系。而在节点指定的是用户,那么就只有这个用户可以操作。
在实际的项目中,我们会有自己的一套<用户-角色-权限>体系,那么我们可以根据这套体系去对接工作流中的节点权限,所谓节点权限就是指这个节点处于当前操作的情况下有哪些用户可以去操作。
- 那么怎么在节点上指定一个权限呢?
首先,如果你实际的项目中是有一套权限体系的,那么需要看你是根据权限来还是根据角色来,因为一个权限可以对应多个用户,一个角色也可以对应多个用户。本人当时是根据角色code去对接的,你可以理解为一个角色的一个唯一标识。
示例:
<userTask id="approve1" name="AP Settlement Leader" activiti:category="FINANCE-BOOKING-REPORT-AUDIT-1" xmlns:flowable="http://flowable.org/bpmn" flowable:candidateGroups="FINANCE-BOOKING-REPORT-AUDIT-1"></userTask>
-
FINANCE-BOOKING-REPORT-AUDIT-1 上面是一个流程图xml文件里的一个流程节点,而这个“FINANCE-BOOKING-REPORT-AUDIT-1”就是我们业务的角色code.
-
如果我们想指定一个用户,但是这个用户事先不知道是谁,一般应用在流程发起人,那么怎么指定呢?
这里有个配置的示例:
<userTask id="apply" name="Settlement Operator" xmlns:flowable="http://flowable.org/bpmn" flowable:assignee="${user_account}"></userTask>
这里的“user_account”是一个流程变量的一个key,只要在调用流程发起的时候传入一个map映射一个用户名,那么流程在运行的时候就可以根据流程变量解析出用户,动态地将用户指定到节点上。当然,flowable:assignee这里也可以直接指定一个写死的用户名,那么这个节点就只有这个用户能够操作了
-
上面只说了怎么为节点添加权限,那我们需要思考一下,如何查询出来待办,我只加了一个key在节点上,直接查询出来就可以吗?
不是的,就算添加了节点权限,但是待办查询上还是需要有个条件过滤出来对应的角色或者用户的待办,这样才能将待办信息输出给当前操作人看到。示例:
//查询角色code的待办 TaskQuery query = taskService.createTaskQuery).taskCandidateGroupInroleCodes);
-
roleCodes 上面这个代码片里传入的变量是一个角色code的list集合,可以理解为这是一个sql查询语句,而 taskCandidateGroupIn)函数是一个in条件操作,指定多个角色code,因为用户本身是可以存在多个角色的。
-
至于角色code的集合怎么来,那要看你怎么设计,我的查询待办接口上是会带一个token,根据这个token可以解析出用户,再根据用户去调用方法获取到对应的角色code。
总结
有几个地方需要注意,也是我自身亲身经历总结出来的:
-
在整合工作流框架上你需要考虑一下自己是否也需要有一套持久层去记录一下节点的流转,如果你的流程设计较为复杂,对于数据存取现有的flowable的api不能很好的支持操作,那么你需要另外建立几个数据表去记录节点的流转,方便数据存取;如果你的流程本身就依照flowable的规范去设计,那么你大可使用flowable的api去操作数据。
我结合以前的事例谈谈我的看法,当时的业务还没整合工作流的时候,我们是有一些数据表去记录流程的流转的,但是这些表对业务的粘合性太强,举个例子,就是对账单审核流程这些表上都是对账单的业务,当初设计的数据表完全不通用。所以在建表的时候是建议考虑通用的方式去建表,而不是针对某一个业务上的流程就建立某一业务的流程表,这样可以对多个不同类型的流程归集在一起统一管理,方便想要做流程监控,日志采集什么的都很方便。
如果是在原有功能上整合加上工作流,在这种尴尬的场景下,应该也是多数人遇到的情况,那么长痛不如短痛,将数据迁移,数据表的设计上保持通用,考虑多个流程的记录。你可能需要建立两张主要的表,一张记录每个流程实例,一张记录流程流转节点。
其次,我也遇到过比较特殊的场景,就是要根据业务条件去搜索查询待办任务,既然是根据业务条件去查询待办,那么单纯去工作流服务查询获取数据那是不可能的,工作流服务里面存放的都是流程信息,很少存放一些业务信息,除非是控制流程流转条件的业务数据才存放进去。
那么怎么做到可以在业务服务上查询待办信息呢?
首先,会有人说可以把业务数据都放在工作流服务上呀,这样不就可以根据条件去查询待办信息。这样做是可以,但是成本很高,假设你的项目有几十个流程,每个流程的业务数据很多,那么流程变量的创建就很多,表与表之间的关联就非常复杂了,关键是每个流程的业务本身就不是相互通用的,所以这无疑就增加了实现的复杂度。那么,我当时的做法是,在业务服务上针对这一个业务建立一个存放该业务的流程实例的表(因为只有这个业务有这种需求去根据条件查询待办信息),这个表里面会记录当前流转到哪个节点ID,在查询待办信息的时候根据用户角色,获取对应可以审核的节点,从而过滤出需要待办的列表。
-
需要实现一套在产线执行错误的情况下可以回滚节点的机制
比如,产线上执行金额审批上出错了,造成这一笔资金迟迟不能运作,这是个很严重的问题,如果没有一套回滚的策略,那么你的功能在用户体验上大打折扣。结合我本身的经历,需要提供一套在节点出错的情况下可以回滚的接口进行人工操作回退,又或者第二种,当业务上发生错误的情况,流程审核也要保持一致进行回滚。
前者是人为操作,一般出现这种情况都是由于业务服务上出错了,但是继续调用执行了工作流服务,这种是分布式事务的问题。那么你可以在界面上提供一个人工修复流程节点的功能,方便用户操作。后者是自动操作,一般用户没有察觉流程上出错了,而是业务提示报错,流程操作跟着业务一起回滚,后者是完全实现了分布式事务的回滚。实现分布式事务的回滚,可以采用TCC机制,也可以通过可靠消息实现最终一致性。具体看场景应用来选择。
-
记录流程实例在数据量大的情况下要考虑数据备份
我说一个例子,我们有个业务订单流程,每个订单都会对应一个流程实例,每天都有很多订单,不到几个月就有几百万的订单,数据量是会很多的,如果双十一那数据会更加恐怖,那么有两种做法,如果这些单都是走完流程的,也无需进行统计,那么就不需要进行记录,直接物理删除,及时削减数据量,防止数据量过大影响数据读写。如果业务本身记录这些数据是有意义的,需要用来做统计,那么那些流程数据已经走完成为历史的需要进行迁移,可以建立一套数据仓库方便后期数据统计。后者对于设备有一定要求,需要搭建一套数据仓库来存储,然后进行数据分析。