一、概述
众所周知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部署成流程定义对象,流程定义对象启动后得到流程实例对象。关于流程处理这一块的内容我没给出,因为我相信流程启动和任务处理大家都很熟悉了。当然读者可以通过我上面给出的仓库地址查阅源码。谢谢!