activiti 面向业务人员最佳实践

1,123 阅读8分钟

一、概述

    众所周知Activiti是一个面向业务人员、开发人员和系统管理员的轻量级工作流和业务流程管理(BPM)平台。在我看来“面向业务人员”是Activiti最大的优势和亮点,无论如何系统最终是要交付给业务人员使用,而业务人员能轻易修改原有的流程才是系统的亮点。然而在有的系统中并未体现出Activiti“面向业务人员”的能力,当流程发生变动时仍需要开发人员参与,这并不是我们使用Activiti的初衷。本文将指导读者在前后端分离的项目中如何让Activiti面向业务人员,实现基本功能有在线编辑流程模型;流程模型发布与删除;流程定义的挂起、激活与删除;启动流程以及任务处理。笔者希望本文能起到一个抛砖引玉的作用,能够让读者在实现“Activiti面向业务人员”有一个新的思考方向。笔者能力有限,在书写中有不恰当的地方也请各位大佬留言指正,拜谢。

    部分实现效果展示:


(创建流程模型效果)


(流程启动受理效果)

    所有的代码均在我的代码仓库,点击即可访问所有源码

二、实践步骤

1、开发模型绘制时需要的接口

    开发流程模型绘制时需要的接口,我们可以参考activiti_model项目,把里面的模型创建、模型修改、模型数据获取等接口全部在自己的项目实现一遍。
    怎么获取activiti_model项目呢?首先前往Activiti github 仓库下载Activiti源码。(如果github下载速度很慢,笔者已经fork了一份到码云,可以去我的gitee 仓库下载)。下载完成后切换到activiti-5.22.0发行版本标签(因为从6.0版本开始,前端界面做了更多的功能,不便于单独抽离出流程绘制功能),命令:git checkout -b activiti-5.22.0 activiti-5.22.0。在activiti_model项目中找到模型创建接口、模型保存接口、获取模型编辑数据的接口、获取stencilset接口,并参考这些接口在自己的项目中实现一遍,实现代码以及关键截图如下:


(activiti_model项目截图)


(我的项目截图)

ProcessEditorController.java 模型绘时需要的接口代码:

package com.fbl.process.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.activiti.bpmn.converter.BpmnXMLConverter;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.editor.constants.ModelDataJsonConstants;
import org.activiti.editor.language.json.converter.BpmnJsonConverter;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.*;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.image.PNGTranscoder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author fbl
 * 流程编辑接口
 */
@Slf4j
@RestController
@RequestMapping("processEditor")
public class ProcessEditorController implements ModelDataJsonConstants {

    @Autowired
    private RepositoryService repositoryService;

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 创建基本模型
     * @return 新建模型id
     */
    @PutMapping("/create")
    public Integer createModel() {
        try {
            String modelName = "modelName";
            String modelKey = "modelKey";
            String description = "description";

            ObjectMapper objectMapper = new ObjectMapper();
            ObjectNode editorNode = objectMapper.createObjectNode();
            editorNode.put("id", "canvas");
            editorNode.put("resourceId", "canvas");
            ObjectNode stencilSetNode = objectMapper.createObjectNode();
            stencilSetNode.put("namespace", "http://b3mn.org/stencilset/bpmn2.0#");
            editorNode.set("stencilset", stencilSetNode);
            // 定义新模型
            Model modelData = repositoryService.newModel();

            ObjectNode modelObjectNode = objectMapper.createObjectNode();
            modelObjectNode.put(ModelDataJsonConstants.MODEL_NAME, modelName);
            modelObjectNode.put(ModelDataJsonConstants.MODEL_REVISION, 1);
            modelObjectNode.put(ModelDataJsonConstants.MODEL_DESCRIPTION, description);
            modelData.setMetaInfo(modelObjectNode.toString());
            modelData.setName(modelName);
            modelData.setKey(modelKey);

            //保存模型
            repositoryService.saveModel(modelData);
            repositoryService.addModelEditorSource(modelData.getId(), editorNode.toString().getBytes("utf-8"));
//            response.sendRedirect(request.getContextPath() + "/modeler.html?modelId=" + modelData.getId());
            return Integer.valueOf(modelData.getId());
        } catch (Exception e) {
            e.printStackTrace();
            return -1;
        }
    }

    /**
     * 保存流程模型
     * @param modelId 模型ID
     * @param name 模型名称
     * @param description 模型描述
     */
    @RequestMapping(value="/model/{modelId}/save", method = RequestMethod.PUT)
    @ResponseStatus(value = HttpStatus.OK)
    public void saveModel(@PathVariable String modelId, String name, String description, String json_xml, String svg_xml) {
        try {
            Model model = repositoryService.getModel(modelId);
            ObjectNode modelJson = (ObjectNode) objectMapper.readTree(model.getMetaInfo());
            modelJson.put(MODEL_NAME, name);
            modelJson.put(MODEL_DESCRIPTION, description);
            model.setMetaInfo(modelJson.toString());
            model.setName(name);
            repositoryService.saveModel(model);
            repositoryService.addModelEditorSource(model.getId(), json_xml.getBytes("utf-8"));
            InputStream svgStream = new ByteArrayInputStream(svg_xml.getBytes("utf-8"));
            TranscoderInput input = new TranscoderInput(svgStream);
            PNGTranscoder transcoder = new PNGTranscoder();
            // Setup output
            ByteArrayOutputStream outStream = new ByteArrayOutputStream();
            TranscoderOutput output = new TranscoderOutput(outStream);
            // Do the transformation
            transcoder.transcode(input, output);
            final byte[] result = outStream.toByteArray();
            repositoryService.addModelEditorSourceExtra(model.getId(), result);
            outStream.close();
        } catch (Exception e) {
            log.error("Error saving model", e);
            throw new ActivitiException("Error saving model", e);
        }
    }

    /**
     * 根据模型id获取流程编辑信息
     * @param modelId 模型id
     * @return 模型编辑信息
     */
    @RequestMapping(value="/model/{modelId}/json", method = RequestMethod.GET, produces = "application/json")
    public ObjectNode getEditorJson(@PathVariable String modelId) {
        ObjectNode modelNode = null;
        Model model = repositoryService.getModel(modelId);
        if (model != null) {
            try {
                if (StringUtils.isNotEmpty(model.getMetaInfo())) {
                    modelNode = (ObjectNode) objectMapper.readTree(model.getMetaInfo());
                } else {
                    modelNode = objectMapper.createObjectNode();
                    modelNode.put(MODEL_NAME, model.getName());
                }
                modelNode.put(MODEL_ID, model.getId());
                ObjectNode editorJsonNode = (ObjectNode) objectMapper.readTree(
                        new String(repositoryService.getModelEditorSource(model.getId()), "utf-8"));
                modelNode.set("model", editorJsonNode);

            } catch (Exception e) {
                log.error("Error creating model JSON", e);
                throw new ActivitiException("Error creating model JSON", e);
            }
        }
        return modelNode;
    }

    /**
     * 获取stencilset数据
     * @return stencilset json 数据
     */
    @RequestMapping(value="/editor/stencilset", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
    public @ResponseBody String getStencilset() {
        InputStream stencilsetStream = this.getClass().getClassLoader().getResourceAsStream("static/stencilset.json");
        try {
            InputStreamReader input = new InputStreamReader(stencilsetStream, "UTF-8");
            CharArrayWriter sw = new CharArrayWriter();
            char[] buffer = new char[1 << 12];
            long count = 0;
            for (int n = 0; (n = input.read(buffer)) >= 0; ) {
                sw.write(buffer, 0, n);
                count += n;
            }
            return sw.toString();
        } catch (Exception e) {
            throw new ActivitiException("Error while loading stencil set", e);
        }
    }

}

注意: 在Activiti 6.0版本的项目中实现这些接口时还需要导入下面依赖

	<!-- Activity6 集成 Modeler 需要 jar 包 -->
        <dependency>
            <groupId>org.apache.xmlgraphics</groupId>
            <artifactId>batik-transcoder</artifactId>
            <version>1.8</version>
        </dependency>
        <dependency>
            <groupId>org.apache.xmlgraphics</groupId>
            <artifactId>batik-codec</artifactId>
            <version>1.8</version>
        </dependency>
        <dependency>
            <groupId>org.activiti</groupId>
            <artifactId>activiti-json-converter</artifactId>
            <version>6.0.0</version>
        </dependency>

2、前端开发流程编辑器VUE组件

    流程编辑所需要的接口完成后,就是开发一个流程编辑器的组件ActivitiModelEditor。我的思路是使用iframe标签内嵌Activiti源码里的流程编辑界面(activiti-webapp-explorer2),然后通过window.ActivitiModelEditor = thisthis指的是ActivitiModelEditor,iframe中的内容可以通过window.parent.ActivitiModelEditor获取这个VUE组件,这样做就可以在iframe内嵌流程编辑器里调用VUE组件的数据以及方法(例如:token、接口地址及组件关闭函数)。
    具体的操作是这样,在下载的Activiti源码里找到activiti-webapp-explorer2模块,接着将webapp/下的前端资源copy到Vue项目的public/目录里(包括:diagram-viewer、editor-app、modeler.html)如图:


(activiti-webapp-explorer2里的modeler.html位置截图)


(Vue项目里的modeler.html位置截图)

    然后在Vue项目里创建一个名为ActivitiModelEditor的组件,组件里包含一个src指向 modeler.html?id=xx 的iframe标签。并在组件的mounted()钩子中把组件的this赋值给window.ActivitiModelEditor,这样就iframe里的界面就可以获取到组件或者项目配置文件里的接口ROOT地址了,以及组件里定义的关闭函数。实现关键代码如下:

ActivitiModelEditor组件代码:

<template>
  <div class="activiti-model-editor-container">
    <iframe
      class="activiti_editor"
      :src="path"
    />
  </div>
</template>

<script>

export default {
  name: 'ActivitiModelEditor',
  props: {
    id: {
      type: Number,
      required: true
    }
  },
  data() {
    return {
      name: '模型編輯器',
      path: `activiti_model/modeler.html?modelId=${this.id}`,
      contextRoot: process.env.VUE_APP_BASE_API + '/processEditor'
    }
  },
  computed: {
  },
  created() {
  },
  mounted() {
    // 让app-cfg.js获取到vue组件
    window.ActivitiModelEditor = this
  },
  methods: {
    saveAndClose() {
      this.$emit('saveAndClose')
    }
  }
}
</script>

<style lang="scss" scoped>
.activiti-model-editor {
  &-container {
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 1002;
    position: fixed;
    display: flex;
    justify-content: center;
    align-items: center;
    .activiti_editor{
      width: 95%;
      height: 95%;
    }
  }
}
</style>

app-cfg.js里配置contextRoot:

use strict';

var ACTIVITI = ACTIVITI || {};

ACTIVITI.CONFIG = {
  //獲取vue組件里的變量
	'contextRoot' : window.parent.ActivitiModelEditor.contextRoot,
};

toolbar-default-actions.js里改写保存关闭的函数:

$scope.saveAndClose = function () {
    $scope.save(function() {
    	// window.location.href = "./";
    	// 调用Vue组件的保存关闭函数,关闭组件
   		window.parent.ActivitiModelEditor.saveAndClose();
    });
};

3、使用流程编辑器VUE组件

    创建一个模型管理界面,进行模型的新增与修改。这个界面将引入我们ActivitiModelEditor组件,代码及效果如下:

ModelmMnagement界面代码:

<template>
  <div class="modelm-mnagement-container">
    <el-row v-if="editorId < 0" :gutter="10">
      <el-col v-for="(model, index) in modelList" :key="index" :span="6">
        <el-card class="card" :body-style="{ padding: '0', height: '100%' }" shadow="hover">
          <img :src="model.imgPath" alt="https://www.baidu.com/img/flexible/logo/pc/result.png" class="image">
          <div class="bottom">
            <span>{{ model.name }}</span><br>
            <time class="time">{{ model.creatDate }}</time>
            <div class="operation">
              <el-button type="text" class="button" @click="editModle(model.id)">编辑</el-button>
              <el-button type="text" class="button" @click="deleteModle(model.id)">删除</el-button>
              <el-button type="text" class="button" @click="deploymentProcessByModelId(model.id)">发布</el-button>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card :body-style="{ padding: '0', height: '100%' }" class="card" shadow="hover">
          <div class="add" @click="addModel">
            <i class="el-icon-circle-plus-outline" />
            <span>新增一个模型</span>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <activiti-model-editor v-if="editorId > 0" :id="editorId" @saveAndClose="saveAndClose" />
  </div>
</template>

<script>
import { Card, Row, Col, Button, MessageBox } from 'element-ui'
import { getModeList, addMode, deleteModelById, deploymentProcessByModelId } from '@/api/process_editor'
import ActivitiModelEditor from '@/components/ActivitiModelEditor'
export default {
  name: 'ModelmMnagement',
  components: {
    elCard: Card,
    elRow: Row,
    elCol: Col,
    elButton: Button,
    activitiModelEditor: ActivitiModelEditor
  },
  data() {
    return {
      name: '模型管理',
      modelList: [
      ],
      editorId: -1
    }
  },
  computed: {},
  created() {
    this.getModeList()
  },
  methods: {
    getModeList() {
      getModeList()
        .then(res => {
          console.log(res)
          this.modelList = res.map(item => {
            return {
              id: item.id,
              name: item.name,
              creatDate: item.createTime,
              imgPath: process.env.VUE_APP_API_HOST + process.env.VUE_APP_BASE_API + '/processEditor/getPng/model/' + item.id
            }
          })
        })
        .catch(e => {})
    },
    editModle(id) {
      this.editorId = id
    },
    saveAndClose() {
      this.editorId = -1
      this.getModeList()
    },
    addModel() {
      addMode().then(res => {
        this.editorId = res
      }).catch(e => {
        console.log(e)
      })
    },
    deleteModle(id) {
      deleteModelById(id).then(res => {
        this.getModeList()
      }).catch(e => {
        debugger
      })
    },
    deploymentProcessByModelId(id) {
      deploymentProcessByModelId(id).then(res => {
        console.log(res)
        MessageBox.alert('部署成功', '温馨提示', {
          confirmButtonText: '确定',
          type: 'info',
          callback: (action, instance) => {
            alert(action)
          }
        })
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.modelm-mnagement {
  &-container {
    height: 100%;
    width: 100%;
    box-sizing: border-box;
    padding: 10px;
    overflow-y: scroll;
    .card {
      position: relative;
      padding: 10px;
      height: 300px;
      .bottom {
        line-height: 12px;
        position: absolute;
        bottom: 10px;
        left: 10px;
        right: 10px;
        .operation {
          text-align: right;
          .button {
            padding: 0;
          }
        }
        .time {
          margin-top: 13px;
          display: block;
          font-size: 13px;
          color: #999;
        }
      }

      .image {
        width: 100%;
        height: 75%;
        display: block;
      }
      .add {
        width: 100%;
        height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
        flex-direction: column;
        i {
          display: block;
          font-size: 60px;
          margin-bottom: 10px;
        }
      }
    }
  }
}
</style>

(流程模型管理界面效果图1)


(流程模型管理界面效果图2)


(流程模型管理界面效果图3)



(流程模型的数据已经入库)

4、获取流程模型的图片及xml文件

    从上面的效果图可以看到模型列表可以展示出流程的图片,下面我将告诉大家这个图片怎么获取的。其实我们在保存流程模型时,已经将绘制的图片和XML保存到了表act_ge_bytearray中了,因此可以通过流程引擎提供的API将他们查询出来,实现代码如下:

ProcessEditorController.java 中加入获取图片和xml的代码:

    /**
     * 获取流程图片
     * @param id id
     * @param type 获取类型 model:获取model的图片;processDefinition:获取流程定义的图片
     * @return 返回流程png图片
     */
    @GetMapping(value = "/getPng/{type}/{id}", produces = MediaType.IMAGE_PNG_VALUE)
    public byte[] getPng(@PathVariable String type, @PathVariable String id, HttpServletResponse response) {
        if ("model".equals(type)) {
            Model model = repositoryService.getModel(id);
            return repositoryService.getModelEditorSourceExtra(model.getId());
        } else if ("processDefinition".equals(type)) {
            InputStream resourceAsStream = null;
            ByteArrayOutputStream byteArrayOutputStream = null;
            try {
                ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(id).singleResult();
                resourceAsStream = repositoryService.getResourceAsStream(processDefinition.getDeploymentId(), processDefinition.getDiagramResourceName());
                byteArrayOutputStream = new ByteArrayOutputStream();
                byte[] buff = new byte[1024];
                int len = 0;
                while ((len = resourceAsStream.read(buff, 0, buff.length)) > -1) {
                    byteArrayOutputStream.write(buff, 0, len);
                }
                return  byteArrayOutputStream.toByteArray();
            } catch (IOException e) {
                e.printStackTrace();
                response.setStatus(HttpStatus.NOT_FOUND.value());
                return null;
            } finally {
                try {
                    if (resourceAsStream != null) {
                        resourceAsStream.close();
                    }
                    if (byteArrayOutputStream != null) {
                        byteArrayOutputStream.close();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    log.error("流关闭异常");
                }
            }
        } else {
            response.setStatus(HttpStatus.NOT_FOUND.value());
            return null;
        }
    }
    /**
     * 获取流程定义xml
     * @param id model id
     * @return 流程定义xml
     */
    @GetMapping(value = "/getModeXml/{id}", produces = MediaType.APPLICATION_XML_VALUE)
    public String getModeXml(@PathVariable String id) {
        Model model = repositoryService.getModel(id);
        ObjectNode objectNode = null;
        try {
            objectNode = (ObjectNode) objectMapper.readTree(repositoryService.getModelEditorSource(model.getId()));
        } catch (IOException e) {
            e.printStackTrace();
        }
        BpmnModel bpmnModel = new BpmnJsonConverter().convertToBpmnModel(objectNode);
        byte[] bytes = new BpmnXMLConverter().convertToXML(bpmnModel);
        String res = null;
        try {
            res = new String(bytes, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            log.error("");
            e.printStackTrace();
        }
        return res;
    }

5、通过流程模型部署流程

    至此我们可以轻易的通过前端界面创建和修改一个流程模型了,现在就实现怎么通过模型id部署一个流程,其中主要代码如下:

ProcessEditorController.java 中加入通过模型id部署流程的代码:

    /**
     * 根据模型ID 部署流程
     * @param modelId 模型id
     * @return 流程Deployment(部署)Id
     */
    @PutMapping("deploymentProcessByModelId")
    public String deploymentProcessByModelId(String modelId) {
        Model model = repositoryService.getModel(modelId);
        if (StringUtils.isNotEmpty(model.getDeploymentId())) {
            ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(model.getDeploymentId()).singleResult();
            if (processDefinition != null) {
                return "已经部署过";
            }
        }
        JsonNode jsonNode = null;
        try {
            jsonNode= objectMapper.readTree(repositoryService.getModelEditorSource(model.getId()));
        } catch (IOException e) {
            e.printStackTrace();
        }
        BpmnModel bpmnModel = new BpmnJsonConverter().convertToBpmnModel(jsonNode);
        Deployment deploy = repositoryService.createDeployment().addBpmnModel(model.getName() + ".bpmn20.xml", bpmnModel)
                .name(model.getName())
                .deploy();
        model.setDeploymentId(deploy.getId());
        repositoryService.saveModel(model);
        return deploy.getId();
    }

6、解决模型部署后图片里中文为口的问题

    在获取部署后的流程图片时,发现图片里的中文都是口了。在创建ProcessEngineConfiguration时设置字体即可解决,代码如下:

....
        SpringProcessEngineConfiguration spec = new SpringProcessEngineConfiguration();
        spec.setDataSource(dataSource);
        spec.setTransactionManager(platformTransactionManager);
        spec.setDatabaseSchemaUpdate(databaseSchemaUpdate);
        spec.setDbIdentityUsed(dbIdentityUsed);
        spec.setActivityFontName("宋体");
        spec.setLabelFontName("宋体");
        spec.setAnnotationFontName("宋体");
....

7、其他需要实现的接口

    上面已经对一些核心的接口开发、组件的封装进行了说明,由于篇幅原因其他的接口我就不一一说明,感兴趣的读者可以去我的码云仓库查看完整代码。这里我就列举下还有哪些接口:
    流程管理的接口:删除一个模型、获取模型列表、获取流程定义列表、挂起一个流程定义、激活一个挂起一个流程定义、删除一个流程定义;
    流程启动及处理接口:获取流程启动表单、启动一个流程、通过用户id获取任务列表、获取任务表单、提交一个任务。

三、总结

    前面主要讲述了怎么将流程编辑器集成到Vue项目,并且在Spring Boot项目中编写支撑流程编辑的接口,借助Activiti里Model这个桥梁,让业务人员能够在线编辑维护流程。Model到流程实例大致是这样的,业务人员编辑Model,Model部署成流程定义对象,流程定义对象启动后得到流程实例对象。关于流程处理这一块的内容我没给出,因为我相信流程启动和任务处理大家都很熟悉了。当然读者可以通过我上面给出的仓库地址查阅源码。谢谢!