从零开始学习和改造activiti流程引擎的13天,自己记录一下

day#1(11.13)

尝试通过spring boot 集成最新版activiti 7,但是苦于官方的文档基本为空,无法完成spring boot的配置,最终按照activiti 6的文档,手工初始化ProcessEngine以及完成deploy测试。

在eclipse中安装流程模型设计器,并画简单的流程。

day#2(11.14)

想要开启activiti对数据库操作的SQL日志打印,研究了好一番功夫,终于得以实现。实现方式如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss} %-5p [%c] - %m%n</pattern>
        </encoder>
    </appender>

    <logger name="org.springframework" level="ERROR" />
    <logger name="org.mybatis" level="ERROR" />
    <logger name="com.baomidou.mybatisplus" level="ERROR" />
    <logger name="org.apache" level="ERROR" />

    <!-- Activiti日志 -->
    <logger name="org.activiti" level="ERROR" />
    <logger name="org.activiti.engine.impl.persistence.entity" level="DEBUG" />

    <!--myibatis log configure -->
    <logger name="com.ibatis" level="DEBUG" />
    <logger name="com.ibatis.common.jdbc.SimpleDataSource" level="DEBUG" />
    <logger name="com.ibatis.common.jdbc.ScriptRunner" level="DEBUG" />
    <logger name="com.ibatis.sqlmap.engine.impl.SqlMapClientDelegate" level="DEBUG" />
    <logger name="java.sql.Connection" level="DEBUG" />
    <logger name="java.sql.Statement" level="DEBUG" />
    <logger name="java.sql.PreparedStatement" level="DEBUG" />

    <root level="DEBUG">
        <appender-ref ref="STDOUT" />
    </root>

</configuration>
logback.xml
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.1.8</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
            <version>1.7.22</version>
        </dependency>
maven dependency

简单的讲,就是resources下放logback.xml,maven添加对logback的依赖即可,然后activiti执行的sql就会被自动打印,无需其他额外设置。

下载activiti 6 全部压缩包(110MB左右),将官方的在线模型设计器运行起来,发现前端使用angularJS做的。对官方demo稍作研究后决定,完全替换官方的rest API,方法就是获取官方的后端API源码,然后一个接口一个接口增加到自建spring mvc项目中。

新建spring mvc空项目,新建简单controller,运行成功。

maven引入activiti engine,复制第一个API代码以及editor前端代码到自己的项目中,发现需要引入另一个jar包(activiti-app-logic)。maven引入之后,编译能通过,但是无法启动,原因是spring 启动时检查引入的jar包冲突。在maven中增加exclusion修复完jar包冲突之后,总算运行起来。

运行起来之后,为了避开身份认证,直接修改JS源码,终于得以直接运行activiti-app/editor/#/editor目录。

day#3(11.15)

开始修复第一个API(GET rest/models),发现需要引用或完全替换activiti-app-logic包中的数据库访问层。考虑了一会,考虑到modeler只涉及3张表,决定用我们之前项目的hibernate框架完全重写modeler的数据库访问层代码。

非常顺利,很快实现获取models,创建model,生成model缩略图,获取model编辑器json数据,保存模型(完成编译)等接口。唯一麻烦的事就是需要从activiti-app-logic包中搬代码,解决各种依赖问题。

day#3(11.16)

先是修复了保存模型的接口,并且进行了大量测试,各种复杂模型数据都可以顺利保存。

接下来体验了modeler的大部分功能,发现非常的复杂,得需要完全理解activiti的基础上才能开发出适用的流程引擎啊。接下来的几天可能是学习activiti引擎了。

day#4(11.19/周一)

周末的时候花了些时间阅读文档,完成了大部分内容的阅读,特别是对BPMN2.0规范有了较深刻的认识。

上午继续花了约2小时完成文档的粗略阅读,接着制定了后面的计划:

  • (1)完成流程模型的部署(之前已完成模型创建和保存);
  • (2)发起流程;
  • (3)查看任务列表;
  • (4)完成任务;
  • (5)完成流程;
  • (6)下一阶段:复杂自定义流程。

接下来首先是研究官方demo,找到并体验发布流程以及任务列表的功能,然后就是从官方的源码中拷贝需要的代码到自建工程里面。遇到了一点问题,就是官方demo中发布流程的业务过于复杂不适合我们项目,但是一开始代码是按照官方demo写的,所以运行起来之后总是报json解析错误,后来慢慢慢慢跟代码,发现可以完全移除官方发布流程中的很多步骤,这样才会更加适合我们的项目。然后就顺利完成了模型的部署。

接下来为了后面的测试更加方便,自己新增了几个rest接口将引擎下面的服务方法通过浏览器暴露,这样便于后期的调试。

为了从全新的简单流程开始测试,就需要把之前创建的错误流程删除,并且正式开发时也需要删除流程的功能,所以接着就重新开发删除流程的接口。 

day#5(11.20) 

清除之前的数据之后,准备创建一个较为真实的请假流程,流程图如下:

流程图画好之后,发现流程图及相关属性乱码。然后百度,一开始以为很好解决就直接从网上copy了一些代码,最后发现都没有解决问题,然后才开始修改editor_json接口,然后定位到造成问题的原因是因为JsonNode对象在restcontroller返回时序列化造成的乱码问题。尝试将JsonNode替换为普通object,中文乱码消失。但是普通java object有一个动态的model对象,如果使用fasterxml的JsonNode发现无法正常序列化,改换为fastjson的JSONObject对象,居然可以正常序列化!至此,中文乱码问题已解决。

接下来便是发起流程。发起流程之后,获取流程实例,获取taskService,获取当前task,尝试设置task的owner和表单数据(通过流程变量),然后完成任务。

代码虽然简单,但是发现任务的owner根本设置不上去,后来尝试了各种方法,才发现问题所在:taskService.complete方法调用之后,当前Task对象已从数据库删除了,如果再用此task对象的id去数据库查询将查不到该task,这时(complete方法调用之后),其实当前流程实例下面包含的任务已经是新创建的task了(有不同的id),之前那个complete的task已经到history表里面去了(owner也设置成功了的)。

由于我们将要开发方便普通工作人员使用的自定义流程设计器,所以,activiti官方的流程设计器的属性编辑器基本不能用了,因为那个编辑器几乎没有人会用的。要替换这个编辑器,最重要的部分就是选择assignee了,设计的原型图如下:

设计思路:

(1)在填写表单环节,增加自定义环节属性:是否需要用户指定下一环节的审批人。如果下一环节只有一个,并且业务需求确实是需要指定审批人,那么在用户提交表单之前,需要选择下一环节审批人。数据传到服务器端之后,服务器首先保存任务的表单数据,然后完成任务,然后获取下一个环节任务,然后设置下一环节的审批人(来自参数);

(2)如果下一环节审批人不需要流程发起人指定,那么提交表单后,服务器端处理逻辑:开始流程 -> 获取第一个任务 -> 设置owner和表单数据 -> 完成任务。在下一环节的create event listener代码中早已设置好assignee的计算逻辑。只要进入listener,就会根据环节定义的审批人进行计算得到当前任务的指派者。

(3)下一环节的assignee已设置好,只要该用户打开任务并进行审批就可以让流程运转起来了。

day#6(11.21) 

 开始改造模型设计器:

(1)简化工具箱,只保留空开始事件、用户任务、并行网关、排他网关、空结束任务、文本备注;

(2)简化流程属性编辑器;

(3)简化其他控件属性编辑器;

(4)用户任务控件增加自定义角色配置;

通过跟踪分析js代码,发现工具箱数据源来自stencilset_bpmn.json,然后备份好后删除不需要的控件,运行,成功。

继续通过跟踪分析js代码,发现属性编辑器的数据源同样来自stencilset_bpmn.json,然后修改英文为中文,运行,乱码,同样是fasterxml的JsonNode对象造成,替换为fastjson的JSONObject,顺利解决中文乱码问题。

首先修改User Task控件,将额外属性移除,运行,发现设计图上面多了两个图标,然后一个属性一个属性移除,终于发现"multiinstance_typepackage","isforcompensationpackage"这两个属性不能移除,说白了这就是activiti的这两个属性的默认值的bug。但是这两个属性不移除的话,编辑器上面就会显示,这显然是不符合需求的。于是开始找代码看哪里用到这两个属性了。尝试修改多个地方的代码之后,虽然可以实现但终觉不妥,后来突然发现属性object有个popular属性,将其修改为false,编辑器上就直接隐藏了,甚是方便。

后来有个员工要离职,工作交接花了3小时左右。

接着完成稍微复杂一点的流程设计,如图:

先从简单的控件开始吧。首先需要改造的就是排他网关出去的两个条件顺序流。思路:

(1)为每个顺序流增加flowtype字段,如果该字段有值,那么在保存模型的时候,就将该字段的值获取出来并按照格式填充到conditionsequenceflow属性中,并删除模型的flowtype属性;

(2)设置条件的表达式大致为:$(APPROVAL_RESULT=='"+flowtype+"'),这样在上一环节完成审批时,会添加一个名称为APPROVAL_RESULT的流程变量,从而实现条件的跳转;

(3)后期再将flowtype的编辑器改为下拉列表,这样用户只需要选择:“审批同意”、“审批拒绝”、“无条件”、“其他表单条件。。。”就可以完成条件的设置,无需输入表达式了。

修改代码之后运行,直接成功。下班了,第二天可以测试环节是否可以自动跳转了。

day#7(11.22) 

 由于找了很久的文档没有找到UEL表达式如何判断字符串相等,所以索性改成判断bool相等,因为有demo嘛。然后更新流程模型,更新代码。最终修复editor_json的代码如下:

 1 public static void fix(JSONObject json) {
 2     JSONArray shapes = json.getJSONArray("childShapes");
 3     JList<JSONObject> flows = JList.from(shapes).select(x -> (JSONObject) x).where(x -> {
 4         JSONObject stencil = x.getJSONObject("stencil");
 5         if (!stencil.getString("id").equals("SequenceFlow")) {
 6             return false;
 7         }
 8         JSONObject properties = x.getJSONObject("properties");
 9         String flowtype = properties.getString("flowtype");
10         if (StringHelper.isNullOrWhitespace(flowtype)) {
11             return false;
12         }
13         if (flowtype.equals("agreed") || flowtype.equals("rejected")) {
14             JSONObject condition = new JSONObject();
15             JSONObject expression = new JSONObject();
16             expression.put("staticValue", "${APPROVED==" + flowtype.equals("agreed") + "}");
17             expression.put("type", "static");
18             condition.put("expression", expression);
19             properties.put("conditionsequenceflow", condition);
20         }
21         //properties.remove("flowtype");
22         return true;
23     });
24 }
View Code

开始测试:

(1)填写请假表单,complete流程开始后的默认任务(将流程推进到经理审核环节),返回流程实例ID;

(2)查询该流程实例下面的全部活跃任务;

(3)找到经理审核的任务(只有这一个任务);

(4)根据经理审核任务ID对请假进行审核(approved=true/false),complete任务时传入APPROVED流程变量(值来自于controller方法参数);

(5)再次根据流程实例ID查询活跃任务,发现流程已经根据approved参数自动选择分支进行流转了。

(6)至此,顺序流条件控制测试成功。

 接下来开始扩展UserTask的自定义属性了。

首先在stencilset_bpmn.json中增加一个complexassigneepackage,然后将此package添加到UserTask的属性列表中,然后在properties.js中增加响应配置,以及创建相应的html模板代码和angularJS的controller。

下图则是这个自定义属性(复杂指派者属性)的编辑器原型:

对应到服务器端的数据结构为:

1 public class DataDto {
2     private String displayText;
3     private JList<InitiatorType> initiators;
4     private JList<Long> roleIds;
5     private JList<Long> departmentIds;
6     private JList<Long> userIds;
7 }

前期为了快速测试流程的运转流程,并不需要去开发复杂的前端交互界面,最简单的方式,莫过于直接修改编辑器JS源码,然后直接从浏览器控制台里面将JS对象存入对应的属性值里面即可。这个构想进行的非常顺利,可以顺利的显示和保存这个DataDto的数据。

但是,这个自定义属性在导出流程模型为XML的时候,生成的XML却不包含这个自定义属性,于是开始各种百度谷歌官方文档,网上有一些例子,但大多都是复制来复制去,并且相比原文有些错漏,而官方文档,不管是5还是6版的都没有提及自定义属性的事,最后在大概两三小时的挫败中终于找到了“原文链接”,虽然不是完全适用,但原文毕竟没有错漏,提供了关键示例代码,终于在XML中显示出了自定义属性。

然后测试发布流程模型,没有问题。然后修改UserTask的create event listener,从该listener中获取环节定义中的自定义属性,然后根据自定义属性的值修改任务的候选人/组/指派者。

改好代码之后就开始进入测试,发现在UserTask的extensionelements节点中的数据不完整,被截断了,然后修改XML增加CDATA包裹,再测试,无效,然后修改XML中属性为URL编码,结果可以正确解析了。

至此,自定义属性已扩展成功。等着第二天来测试我们这个复杂的指派系统了吧。

day#8(11.23) 

首先完成UserTask的create event listener的指派代码的编写,非常顺利。

然后从填写表单开始测试,加上断点,顺利完成指派者、候选人、候选组的测试,非常成功。

下面是对activiti指派者/候选人测试的一些总结,官方文档中不曾提及的:

(1)当调用taskService.addCandidateUser和addCandidateGroup之后,可以通过TaskQuery查询出任务;

(2)在上一步之后,如果调用了taskService.claim认领任务之后,再调用第一步的根据候选人/组进行查询就查不出结果了,但是从该任务的identityLinks中仍然可以看到该任务的候选人/组并没有被清空,还是以前的值,并且claim之后,任务的assignee就是认领者了;

(3)任务认领之后如果再次钓claim任务就会报错;

(4)任务认领者可以是任何人,也就是说不一定非要是候选人或候选组里面的人。

接下来便是开始开发表单了,另一个重要节点。

首先再次仔细阅读了官方的关于表单的知识,没有太大用处。然后又在网上找了一些自定义表单的文章,用处也不大。因为我的目标很明确,就是要搞清楚表单是如何存储在流程模型XML中的,以及如何在任务详情页面呈现。

既然网上的资源质量有限,又只得把官方的demo运行起来,创建流程,创建表单,发起流程,一步一步测试,都可以跑通。为了知道表单定义在XML中的体现,直接下载已定义好的流程XML,发现表单数据在XML中只有form-properties列表和form-key两个属性,那表单的定义数据就一定在其他地方存储了。

day#9(11.26) 

花了近一半的时间开会以及投入到其他项目。

由于之前已经研究清楚,流程模型XML并不会包含表单信息,因此表单信息只能用另一张数据库表来存储。

为了让流程设计器更加友好方便使用,我设计了如下的表单定制化界面。

如此,用户只需要输入表单设计器编辑好的表单key,程序便会自动加载表单信息,然后用户只需要勾选当前环节哪些字段需要显示,哪些字段可以编辑。这可能是目前市面上最好的流程自定义表单设计器了。

思路清楚,接下来便是跟改造自定义指派的自定义属性一样,增加自定义表单属性的编辑器。同样,在前期,只通过代码生成XML,先不做界面。

表单设计器的环节自定义属性开发很快完成。接着便是完成表单相关表/数据结构设计,保存数据库,开发很快完成。

day#10(11.27) 

由于activiti官方将表单数据保存到跟流程模型同一张表里面,我觉得这并不是太好,还是新建一张表用来存放表单数据更好。

表单表包含简单的key/name/css/fields/buttons字段,其中fields和buttons都是json格式的数据,用json而不是子表是因为考虑到后期扩展表单域的灵活性。

然后就是开始开发自定义表单模块了,当然,为了更快让整个流程跑起来,先就做个简单版本的表单设计器吧,如下图:

 这个表单设计器虽然简单,但是工具箱以及表单实时展示部分已经完成了非常容易扩展的架构方式,这样对于优化上图中的控件,以及增加复杂控件都会变得非常简单。该模块采用vuejs渲染。

到下班前,已基本完成了工具箱及表单的呈现部分。

day#11(11.28) 

 花了点时间优化表单设计器的体验及增加功能,然后就是保存数据库,以及从数据库加载呈现表单,上午就结束了。

总的来讲,表单设计器从界面设计到保存数据库到WEB前端,花了一个下午和一个上午,已经可以正常运行了。

接下来便是将表单设计器整合到流程设计器里面。

本来想继续用vuejs来渲染表单部分,这样的话可以重用表单设计器的渲染代码,但是尝试了一下发现根本不行,因为angularjs和vuejs都是实时渲染,angularjs的html只要被vuejs接管之后,我们看到的html已经是vuejs重新渲染生成的了,反之亦然。

所以,还是老老实实用angularjs做吧。

先花了一个小时左右把angularjs的文档大致看了下,发现远没有vuejs的文档好读。比如,我想找一个如何渲染列表以及如何渲染动态属性,硬是找了很久没有找到。最后不得不在官方文档中通过找对应的例子一个一个完成渲染。

至此,流程设计器里面的表单属性编辑器已能基本上呈现出来了,在右侧勾选属性时就显示,未勾选时则隐藏,从而做到实时的表单预览。如下图:

接下来便是将这个表单的数据存到数据库,以及从数据库加载数据并还原表单了。

day#12(11.29) 

 非常顺利的完成了表单属性的保存以及从数据库还原表单,然后将整个流程的所有用户环节的数据进行了更新,发现现在的整个表单设计确实非常易用,简单直观!

接下来为了更好地进行后边的测试,先把之前落下的顺序流条件编辑器给优化一下。

按照官方提供的bool值模板,非常顺利的完成了自定义的流程条件编辑器,最终界面如下:

接着,为了让流程真正的跑起来,还需要对指派属性进行改造。

首先是定义弹出打开后加载数据的数据结构,然后生成一些假的数据方便测试。然后就是利用angularjs对前端进行绑定。最终实现效果如下图:

 

 实现的功能包括:

(1)部门跟用户采用树形数据结构生成,支持实时名称过滤,且只要有下级节点,上级节点就一定会显示;

(2)4个tab内容区点击复选框就会实时更新到右边已选列表;

(3)已选列表点击X就会取消选中对应复选框;

(4)根据4个tab已选内容生成合理的显示文本(红色框框区);

(5)点击保存将已选项保存到数据库。

 所有这些编码工作在一个下午完成。接下来第二天便是从数据库恢复表单了。

day#13(11.30) 

首先完成了从数据库还原表单已选数据,接着删除了UserTask属性列表中的TaskListeners属性编辑器,改为保存流程模型时对所有UserTask默认添加我们的自定义任务指派管理器(在任务创建时运行)。这2步都完成的非常顺利。

接下来便可以真正测试流程运转了,在这个过程中还需要还原表单设计器完成的表单,以及控制显示隐藏和可编辑性。

在任务详情页还原表单,以及添加简单的表单认证,以及控制显示隐藏表单字段和按钮,这些开发工作都进展的非常顺利。

接下来便可以测试流程的运转了:

(1)发起流程,同时将任务指派给当前用户;

(2)用户进入任务详情,能够显示表单,填写表单,能够正常提交和保存草稿;

(3)提交表单后,新创建的任务顺利指派给了下一级节点。

总的来讲,整个流程的运转跟预期的差不多,只是在审批人选择拒绝时让流程回到第一个节点时,流程发起者无法再次看到该任务,因为任务的指派者变成空了。所以之前的方案得改了。改造后,流程开始的第一个环节即必须设置指派者或候选人,这样也更加灵活了,然后startProcessInstanceByKey之后就不需要设置指派者了,因为流程自动进入我们自定义的复杂指派者监听器模块,该监听器会自动根据流程定义对指派者进行设定,这样,不管时流程刚刚开启还是以后任何时候回退到该环节,该环节都一定会有指派者了。

OK,至此整个自定义流程已经全部完成了,接下来改下界面最后再完整的测试一遍吧。

最终的测试过程

第0步:设计流程图和表单,以及准备工作

下面对部分环节进行说明:

(1)填写请假单环节的指派者设置为流程发起人;

(2)部门经理审批设置为发起者所在部门负责人(个人);

(3)CEO审批设置为指定的个人(陈丽);

(4)HR备份指派了3个人作为候选人;

(5)财务备份选择的是财务处管理员,一种系统角色。

表单配置还是跟之前的一样:

下面是整个测试过程用到的一个简单界面:

第1步:发起流程

测试例子都使用user05进行流程的发起。发起之后,可以看到指派给user05的任务列表:

第2步 填写申请单

点击上一步的查看任务详情,进入任务详情页:

记住流程实例ID为:145001

填写表单后,点击保存草稿。然后再刷新页面,发现任务数据已保存,证明保存草稿按钮工作正常。然后再点击提交。

第3步:部门经理审批

 回到测试界面,输入流程实例ID,加载该流程实例的活动任务,如下图:

 可以看到流程已经运转到部门经理审批环节了,指派者也已经正确的设置为了user05-department_manager。点击查看任务详情,如下图:

这里我们点击拒绝。 

第4步:重新提交申请表单

再次回到测试界面,加载流程实例的活动任务,如下图:

可以看到流程已经正确退回到第一个填写请假单的环节了,指派者也正确的设置为了流程发起人user05。点击查看任务详情,再次进入填写表单界面,如下图:

再次点击提交。

第5步:部门经理再次审批

再次回到测试界面,加载该流程实例的活动任务,如下图:

再次看到流程已经运转到部门经理审批环节。点击查看任务详情,如下如:

这次点同意。

第6步:CEO审批

回到测试界面,再次加载流程实例的活动任务,如下图:

可以看到流程已经运转到CEO审批环节。点击查看任务详情,如下图:

然后点击同意按钮。

第7步:HR备份

回到测试界面,再次加载流程实例的活动任务,如下图:

可以看到指派为空,候选组为空,候选用户有3个。这里我就不去测试领取任务的功能了,直接点击查看任务详情,然后点提交(就不截图了)。

 

第8步:财务备份

回到测试界面,再次加载流程实例的活动任务,如下图:

可以看到指派者为空,候选用户为空,候选组为一个角色的ID。点击查看任务详情,如下图:

在任务详情页面可以看到指派者仍然为空,没关系,直接点击提交。

第9步:完成

这时再次回到测试界面,输入流程实例ID,可以看到已经没有任何活动任务了,该流程实例已经顺利完成了。

总结

整个13天的学习和改造activiti的工作中,没有什么大的技术难题,主要还是一个学习别人框架源码的一个过程。最痛苦的莫过于官方的文档实在太不全了,对activiti的改造几乎全靠读官方demo的前后端源码。

THE END

原文地址:https://www.cnblogs.com/leotsai/p/activiti-learning-days.html