记录一下对flowable流程引擎创建自定义下拉框属性步骤方法

说明

Flowable 一款流行的bpmn2流程引擎,里面包含了绘制bpmn2的流程图工具。本次flowable 版本为 6.4.2 , 6.5开始flowable走商用路线了。6.4.2基本上满足了常用流程引擎的功能。

在绘制用户任务流程图的时候,除了bpmn2规定的一些规范属性值外,我们希望对流程属性有一些自定义的属性信息,例如这个流程是专门用于提交表单的还是用于审批的,这个流程是否可以被终止等等,虽然客户采用各种排他网关或并行网关解决,但流程图必会很复杂。

例如下图中的 【任务类型】 【可被撤销】【可终止流程】属性,就是自定义属性。
在这里插入图片描述

可以想象一些,如果没有自定义属性,所有的步骤都可能具备 终止,撤销操作,那么如果全部用网关来画,这个图一定成了蛛网形状了。

添加自定义属性

思路

可能你会不断百度或者其他搜索引擎查询相关博客
可能你会自告奋勇阅读源码
可能你会…

以上不管你怎么弄,其实都需要你花费一定的精力和毅力去做,中国的博客目前为止,至少关于flowable 的系统的、完整的、流畅的解决你的困扰,还很少。
因此,最后还得靠自己的思维方法解决自己的业务场景!

步骤

要在flowable 原有的画图网页上,添加自定义页面,

找到 flowable的静态画图页面文件

首先你的找到 flowable的静态画图页面, 下载源码,找画bpmn2的工程文件。。。 总之最后的工程文件路径是如下面截图所示:
在这里插入图片描述
如果你觉得很烦,或者你不想自己再搞一个画图工程,从flowable源码中copy页面文件,copy一系列springBoot 的依赖工程 等等,你可以下载下面的已经处理过的,免登录flowable-modeler工程:

https://gitee.com/banana6/flowable-bpmn2-modeler

当然其他人分享的,都大同小异,毕竟都是copy源代码修改的。

找到bpmnjson的数据文件

找到 stencilset_bpmn_*.json 文件,
我的这里有中英文国际化,因此有两个,并且放在了resources目录下,通过代码自定义访问加载。
不管什么加载方式,文件名称叫什么,文件的内容都一样,前端请求你给它一样的内容即可。
在这里插入图片描述

修改找到bpmnjson的数据

修改位置在开头json节点属性 propertyPackages中:


{
  "title" : "Process editor",
  "namespace" : "http://b3mn.org/stencilset/bpmn2.0#",
  "description" : "BPMN process editor",
  "propertyPackages" : [ {
    "name" : "process_idpackage",
    "properties" : [ {
      "id" : "process_id",
      "type" : "String",
      "title" : "流程标识",
      "value" : "process",
      "description" : "定义流程的唯一标识符.",
      "popular" : true
    } ]
  },

  ....  这里面添加  .....

里面添加属性内容:

{
    "name" : "revokeflagpackage",
    "properties" : [ {
      "id" : "revokeflag",
      "type" : "Boolean",
      "title" : "可被撤销",
      "value" : "false",
      "description" : "定义该步骤提交后是否可以撤销.",
      "popular" : true
    } ]
  },{
    "name" : "endflagpackage",
    "properties" : [ {
      "id" : "endflag",
      "type" : "Boolean",
      "title" : "可终止流程",
      "value" : "false",
      "description" : "定义该步骤提交后是否可以撤销.",
      "popular" : true
    } ]
  }, {
    "name" : "nodetypepackage",
    "properties" : [ {
      "id" : "nodetype",
      "type" : "nodetype",
      "title" : "任务类型",
      "value" : "commit",
      "description" : "节点分为普通节点和审核节点.",
      "popular" : true
    } ]
  }

关联到 userTask 节点, 就在刚才配置文件的底端 id = “UserTask” 节点,修改propertyPackages 属性值,将上面的3个自定义的属性id加入其中:


 {
    "type" : "node",
    "id" : "UserTask",
    "title" : "用户任务",
     ......
    "propertyPackages" : [...... , "nodetypepackage", "revokeflagpackage","endflagpackage", ...... ],
     ......
  }

英文配置文件同步修改,当然如果你没有英文环境,则可以不修改。

添加自定义下拉框属性类型

目前flowable 画图页面上属性中,有各种数据类型,文本类型,布尔类型、还有其他复合类型, 可以说每一个属性除了 文本、布尔、数字等基础类型外其他的每一个属性都对应一个数据类型(一系列 html、js文件)。
在这里插入图片描述

以上的这些内置的属性类型配置信息,可查看 配置文件 properties.js 得知:
在这里插入图片描述
我们上面的 自定义【任务类型】属性类型就是下拉框,里面包含两个值“commit” 和 “audit” ,这里找了很久,通过比对 【多实例类型】 属性相关配置文件,进行添加修改相应文件,步骤如下:

  1. 在 properties.js 中添加如下属性:
    "nodetype" : {
        "readModeTemplateUrl": "editor-app/configuration/properties/default-value-display-template.html",
        "writeModeTemplateUrl": "editor-app/configuration/properties/nodetype-property-write-template.html"
    },

看到 上面的 “nodetype” 了吗,和 之前在 bpmnjson 添加的内容中的 “type” : “nodetype” 对应上,名称保持一致。
在这里插入图片描述

  1. 创建对应的html 和 js文件

就是上面 添加的 nodetype-property-write-template.html 文件,路径在
static/modeler/editor-app/configuration/properties/

内容如下:

<div ng-controller="FlowableNodeTypeCtrl">
    <select ng-model="property.value" ng-change="nodeTypeChanged()">
    	<option>commit</option>
    	<option>audit</option>
    </select>
</div>

然后继续创建 js 文件:properties-nodetype-controller.js ,路径在
static/modeler/editor-app/configuration/

angular.module('flowableModeler').controller('FlowableNodeTypeCtrl', [ '$scope', function($scope) {

    if ($scope.property.value === undefined)
    {
    	$scope.property.value = 'commit';
    }

    $scope.nodeTypeChanged = function() {
        $scope.updatePropertyInModel($scope.property);
    };
}]);

其中 html中的 <div ng-controller=“FlowableNodeTypeCtrl”> 和 js 中的 .controller(‘FlowableNodeTypeCtrl’, 对应上即可,这里都是 angularJS语法,没学过也无妨,照抄多实例应用里面的属性文件即可。

  1. 最后在首页 index.html中加载js文件
<script src="editor-app/configuration/properties-nodetype-controller.js" type="text/javascript"></script>

在这里插入图片描述

添加自定义属性转换器

到目前为止,静态页面上的工作处理完毕,接下来就是后端java代码处理,

指导思想:

  • 页面上的属性 -----导出--------》 bpmn2 XML 文件
  • bpmn2 XML 文件 ------导入-------》 页面上的属性
  • 页面上的属性 -----保存--------》 Flowable java 对象 -----持久化—》 数据库表
  • 数据库表 -----查询--------》 Flowable java 对象 -----转换—》 页面上的属性

对于flowable 这种流程引擎架构,上面所列出的一系列数据格式的转换必定会有一个转换器角色的功能类来实现 !

而且 转换器角色的功能类 肯定不止一个, 必定都实现或者由哪一个控制类统一管控, flowable用的最多的应该是 用户任务,UserTask,转换器的名称一般编程中都喜欢用 Converter 来表示, 因此:

使用IDEA 强大的搜索功能来找一找: UserTaskConverter , 还真有!

org.flowable.editor.language.json.converter.UserTaskJsonConverter

在这里插入图片描述
当然一般 你可能是 根据前端 F12 查看,到底调用哪一个接口,然在顺藤摸瓜进行一番debug查找,最后一定能找到 UserTaskJsonConverter。

创建自定义属性转换器:
这里省略了一系列 debug的痛苦过程,直接说结果:

继承 UserTaskJsonConverter 创建名称为 CustomizeUserTaskJsonConverter 的转换器类。

package com.xxx.bbb.aaa.convert;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.BooleanNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.middol.flowable.modeler.utils.ExtensionAttributeUtils;
import org.flowable.bpmn.model.*;
import org.flowable.editor.language.json.converter.ActivityProcessor;
import org.flowable.editor.language.json.converter.BaseBpmnJsonConverter;
import org.flowable.editor.language.json.converter.UserTaskJsonConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * 新建自定义userTaskjson解析器CustomizeUserTaskJsonConverter
 *
 * @author guzt
 */
public class CustomizeUserTaskJsonConverter extends UserTaskJsonConverter {
    private static final Logger LOGGER = LoggerFactory.getLogger(CustomizeUserTaskJsonConverter.class);

    private static final String NODE_TYPE_KEY = "nodetype";
    private static final String REVOKE_FLAG_KEY = "revokeflag";
    private static final String END_FLAG_KEY = "endflag";

    /**
     * 覆盖原 UserTaskJsonConverter 中的 fillTypes 方法
     *
     * @param convertersToBpmnMap map
     * @param convertersToJsonMap map
     * @see org.flowable.editor.language.json.converter.BpmnJsonConverter  中的静态代码块
     */
    public static void fillTypes(Map<String, Class<? extends BaseBpmnJsonConverter>> convertersToBpmnMap,
                                 Map<Class<? extends BaseElement>, Class<? extends BaseBpmnJsonConverter>> convertersToJsonMap) {

        fillJsonTypes(convertersToBpmnMap);
        fillBpmnTypes(convertersToJsonMap);
    }

    /**
     * 覆盖原 UserTaskJsonConverter 中的 fillBpmnTypes 方法
     *
     * @param convertersToJsonMap map
     * @see org.flowable.editor.language.json.converter.BpmnJsonConverter  中的静态代码块
     */
    public static void fillBpmnTypes(
            Map<Class<? extends BaseElement>, Class<? extends BaseBpmnJsonConverter>> convertersToJsonMap) {
        convertersToJsonMap.put(UserTask.class, CustomizeUserTaskJsonConverter.class);
    }

    /**
     * 覆盖原 UserTaskJsonConverter 中的 fillJsonTypes 方法
     *
     * @param convertersToBpmnMap map
     * @see org.flowable.editor.language.json.converter.BpmnJsonConverter  中的静态代码块
     */
    public static void fillJsonTypes(Map<String, Class<? extends BaseBpmnJsonConverter>> convertersToBpmnMap) {
        convertersToBpmnMap.put(STENCIL_TASK_USER, CustomizeUserTaskJsonConverter.class);
    }

	// 主要用于 外部 xml 导入到 flowable画图页面中
    @Override
    public void convertToJson(BaseElement baseElement, ActivityProcessor processor, BpmnModel model, FlowElementsContainer container, ArrayNode shapesArrayNode, double subProcessX, double subProcessY) {
        super.convertToJson(baseElement, processor, model, container, shapesArrayNode, subProcessX, subProcessY);
        if (baseElement instanceof UserTask) {
            LOGGER.debug("userTaskId = {} 扩展属性 = {}", baseElement.getId(), baseElement.getAttributes());
            Map<String, List<ExtensionAttribute>> stringListMap = baseElement.getAttributes();

            String nodetype = stringListMap.get(NODE_TYPE_KEY).get(0).getValue();
            String revokeflag = stringListMap.get(REVOKE_FLAG_KEY).get(0).getValue();
            String endflag = stringListMap.get(END_FLAG_KEY).get(0).getValue();

            shapesArrayNode.forEach(node -> {
                if (baseElement.getId().equals(node.get("resourceId").textValue())) {
                    ObjectNode properties = (ObjectNode) node.get("properties");
                    properties.set(NODE_TYPE_KEY, new TextNode(nodetype));
                    properties.set(REVOKE_FLAG_KEY, BooleanNode.valueOf(Boolean.parseBoolean(revokeflag)));
                    properties.set(END_FLAG_KEY, BooleanNode.valueOf(Boolean.parseBoolean(endflag)));
                }
            });
        }
    }

    // 主要用于 页面属性加载和导出 xml使用
    @Override
    protected FlowElement convertJsonToElement(JsonNode elementNode, JsonNode modelNode,
                                               Map<String, JsonNode> shapeMap) {
        FlowElement flowElement = super.convertJsonToElement(elementNode, modelNode, shapeMap);

        if (flowElement instanceof UserTask) {
            LOGGER.debug("进入自定义属性解析CustomizeUserTaskJsonConverter...");
            Map<String, List<ExtensionAttribute>> attributes = flowElement.getAttributes();
            String nodetype = getPropertyValueAsString(NODE_TYPE_KEY, elementNode);
            if (StringUtils.isEmpty(nodetype)) {
                LOGGER.debug("nodetype 属性为空,设置为默认值");
                nodetype = "commit";
            }
            attributes.put(NODE_TYPE_KEY, Collections.singletonList(
                    ExtensionAttributeUtils.generate(NODE_TYPE_KEY, nodetype)));

            String revokeflag = getPropertyValueAsString(REVOKE_FLAG_KEY, elementNode);
            if (StringUtils.isEmpty(revokeflag)) {
                LOGGER.debug("revokeflag 属性为空,设置为默认值");
                revokeflag = "false";
            }
            attributes.put(REVOKE_FLAG_KEY, Collections.singletonList(
                    ExtensionAttributeUtils.generate(REVOKE_FLAG_KEY, revokeflag)));

            String endflag = getPropertyValueAsString(END_FLAG_KEY, elementNode);
            if (StringUtils.isEmpty(endflag)) {
                LOGGER.debug("endflag 属性为空,设置为默认值");
                endflag = "false";
            }
            attributes.put(END_FLAG_KEY, Collections.singletonList(
                    ExtensionAttributeUtils.generate(END_FLAG_KEY, endflag)));
            LOGGER.debug("自定义属性解析CustomizeUserTaskJsonConverter 完成");
            LOGGER.debug("当前 attributes 为 {}", attributes);
        }

        return flowElement;
    }


}

看到里面的 三个属性了吗,就是一开始我们在js文件中添加的3个自定义属性的 id值

    private static final String NODE_TYPE_KEY = "nodetype";
    private static final String REVOKE_FLAG_KEY = "revokeflag";
    private static final String END_FLAG_KEY = "endflag";

生效自定义属性转换器:
让 CustomizeUserTaskJsonConverter 在运行期间替换 UserTaskJsonConverter 还需要做如下事情:

创建一个 spring Bean 名称叫什么无所谓,暂且为 CustomBpmnJsonConverter ,必须 继承 BpmnJsonConverter,
为什么要继承 BpmnJsonConverter ? 因为没办法,convertersToBpmnMap convertersToJsonMap 是 protected 类型,而正是这两个Map 来存储当解析用户任务节点时用哪一个转换器。


package com.xxx.bbb.aaa.convert;

import org.flowable.editor.language.json.converter.BpmnJsonConverter;
import org.springframework.stereotype.Component;

/**
* 目的是将BpmnJsonConverter中的 UserTaskJsonConverter 设置替换成 CustomizeUserTaskJsonConverter。
* 为什么要继承 BpmnJsonConverter ? 因为没办法,convertersToBpmnMap  convertersToJsonMap 是 protected 类型
*
* @author guzt
* @see org.flowable.editor.language.json.converter.BpmnJsonConverter  中的静态代码块
*/
@Component
public class CustomBpmnJsonConverter extends BpmnJsonConverter {

  static {
      CustomizeUserTaskJsonConverter.fillTypes(convertersToBpmnMap, convertersToJsonMap);
  }
}

上面用到的工具类 ExtensionAttributeUtils 代码如下:

package com.xxx.bbb.aaa.convert;

import org.flowable.bpmn.model.ExtensionAttribute;
import org.flowable.editor.language.json.converter.BpmnJsonConverter;

/**
 * @author guzt
 */
public class ExtensionAttributeUtils {

    public static ExtensionAttribute generate(String key, String val) {
        ExtensionAttribute ea = new ExtensionAttribute();
        ea.setNamespace(BpmnJsonConverter.MODELER_NAMESPACE);
        ea.setName(key);
        ea.setNamespacePrefix("custom");
        ea.setValue(val);
        return ea;
    }

}

自此,准备工作全部结束,然后编译项目,重新启动看看,是否有3个自定义属性,而且可以导入,而且可以将外部的xml 文件导入进去也能解析出自定义属性值。

获取自定义属性值

我们调用 flowable 中查询 userTask 接口获得 用户任务列表时 或者 查询流程定义的属性信息时,总之是 org.flowable.bpmn.model.UserTask 类或其子类。

UserTask 可以 通过调用 getAttributes(); 方法获得自定义属性值。

    protected static final String NODE_TYPE_KEY = "nodetype";
    protected static final String REVOKE_FLAG_KEY = "revokeflag";
    protected static final String END_FLAG_KEY = "endflag";

  Map<String, List<ExtensionAttribute>> attributes = task.getAttributes();

        List<ExtensionAttribute> att1 = attributes.get(NODE_TYPE_KEY);
        if (CollectionUtil.isNotEmpty(att1)) {
           System.out.println("nodetype 的值为:" + att1.get(0).getValue()); 
        }

        List<ExtensionAttribute> att2 = attributes.get(REVOKE_FLAG_KEY);
        if (CollectionUtil.isNotEmpty(att1)) {
           System.out.println("revokeflag的值为:" + att1.get(0).getValue()); 
        }

        List<ExtensionAttribute> att3 = attributes.get(END_FLAG_KEY);
        if (CollectionUtil.isNotEmpty(att1)) {
           System.out.println("endflag的值为:" + att1.get(0).getValue()); 
        }

Over , Thanks 如有不对地方请指正

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐