最近在学习Activiti工作流引擎,为了在流程定义简便,Activit官方的流程定义插件需要集成到项目中,今天把这个的整合过程记录于此,以便大家参考。
本次整合所使用的版本为 springboot:2.2.2.RELEASE
, Activiti:6.0.0
,下载 Activiti-5.22.0
只是为了使用其中的流程定义插件。
创建一个 SpringBoot 工程
下载 Activiti-5.22.0
这里假设你已经搭建好了一个 Vue
项目
1. 创建 springboot
工程
创建一个 Springboot
工程,并把相关的依赖包导入 pom.xml
中
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-engine</artifactId> <version>6.0.0</version> </dependency> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-json-converter</artifactId> <version>6.0.0</version> </dependency> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring-boot-starter-basic</artifactId> <version>6.0.0</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>org.apache.xmlgraphics</groupId> <artifactId>batik-codec</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>org.apache.xmlgraphics</groupId> <artifactId>batik-css</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>org.apache.xmlgraphics</groupId> <artifactId>batik-svg-dom</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>org.apache.xmlgraphics</groupId> <artifactId>batik-svggen</artifactId> <version>1.7</version> </dependency>
2. 解压 Activiti-5.22.0
解压完成如下图所示:
这时将 modules>activiti-webapp-explorer2>src>main>webapp
中的 diagram-viewer
、 editor-app
、 modeler.html
这三个复制到上一步创建好的 springboot
工程 resource
下的 static
中,如下:
接下来在 springboot
创建 ModelController
、 StencilsetController
两个类:
2.1 ModelController
这个类里的方法也就是在解压后的 activiti-5.22.0/modules/activiti-modeler/src/main/java/org/activiti/rest/editor/model
中,经过我自己的改造成了如下的内容。
/** * 模型管理类 * * @author dgb */ @Slf4j @RestController @RequestMapping("model") public class ModelController { private final String MODEL_ID = "modelId"; private final String MODEL_NAME = "name"; private final String MODEL_DESCRIPTION = "description"; private final String MODEL_REVISION = "revision"; @Autowired private RepositoryService repositoryService; @Autowired private ObjectMapper objectMapper; /** * 获取所有模型 * * @return */ @PostMapping("/s") public RespData<PageInfo<Model>> modelList(@RequestBody ModelEntityImpl model, Integer pageNum, Integer pageSize) { ModelQuery modelQuery = repositoryService.createModelQuery(); if (StringUtils.isNotBlank(model.getName())) { modelQuery.modelNameLike("%" + model.getName() + "%"); } modelQuery.orderByCreateTime().desc(); List<Model> models = modelQuery.listPage(pageNum - 1, pageSize); PageInfo<Model> pageInfo = PageUtil.toPageInfo(models); return RespData.ok(pageInfo); } /** * 保存模型 * * @param newModel * @return */ @PostMapping public RespData<String> create(@RequestBody NewModel newModel) { ObjectNode modelNode = objectMapper.createObjectNode(); modelNode.put(MODEL_NAME, newModel.getName()); modelNode.put(MODEL_DESCRIPTION, newModel.getDesc()); modelNode.put(MODEL_REVISION, "1"); Model model = repositoryService.newModel(); model.setName(newModel.getName()); model.setKey(newModel.getKey()); model.setMetaInfo(modelNode.toString()); repositoryService.saveModel(model); String id = model.getId(); //完善ModelEditorSource 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.putPOJO("stencilset", stencilSetNode); repositoryService.addModelEditorSource(id, editorNode.toString().getBytes(StandardCharsets.UTF_8)); return RespData.ok(id); } /** * 更新模型 * * @param model * @return */ @PutMapping public RespData<String> update(@RequestBody ModelEntityImpl model) { String id = newModel.getId(); ObjectNode modelNode = objectMapper.createObjectNode(); modelNode.put(MODEL_NAME, newModel.getName()); modelNode.put(MODEL_DESCRIPTION, newModel.getDesc()); modelNode.put(MODEL_REVISION, "1"); //完善ModelEditorSource 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.putPOJO("stencilset", stencilSetNode); repositoryService.addModelEditorSource(id, editorNode.toString().getBytes(StandardCharsets.UTF_8)); ModelEntityImpl model = new ModelEntityImpl(); model.setId(id); model.setName(newModel.getName()); model.setKey(newModel.getKey()); model.setMetaInfo(modelNode.toString()); repositoryService.saveModel(model); return RespData.ok(model.getId()); } /** * 根据Id查询模型 * * @param id * @return */ @GetMapping("/{id}") public RespData<Model> getById(@PathVariable("id") String id) { Model model = repositoryService.createModelQuery().modelId(id).singleResult(); return RespData.ok(model); } /** * 删除模型 * * @param id * @return */ @DeleteMapping("/{id}") public RespData<?> delete(@PathVariable("id") String id) { repositoryService.deleteModel(id); return RespData.sucess().build(); } /** * 获取流程定义json数据 * * @param modelId * @return */ @GetMapping(value = "/{modelId}/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()); byte[] modelEditorSource = repositoryService.getModelEditorSource(model.getId()); ObjectNode editorJsonNode = (ObjectNode) objectMapper.readTree(new String(modelEditorSource, StandardCharsets.UTF_8)); modelNode.putPOJO("model", editorJsonNode); } catch (Exception e) { log.error("Error creating model JSON", e); throw new ActivitiException("Error creating model JSON", e); } } return modelNode; } /** * 保存流程定义数据 */ @PutMapping(value = "/{modelId}/save") public void saveModel(@PathVariable String modelId, @RequestParam("name") String name, @RequestParam("json_xml") String json_xml, @RequestParam("svg_xml") String svg_xml, @RequestParam("description") String description) { 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(), Objects.requireNonNull(json_xml.getBytes(StandardCharsets.UTF_8))); InputStream svgStream = new ByteArrayInputStream(Objects.requireNonNull(svg_xml.getBytes(StandardCharsets.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); } } /** * 部署模型 * * @param modelId * @return */ @GetMapping("/{modelId}/deployment") public RespData<?> deploy(@PathVariable("modelId") String modelId) { // 获取模型 Model modelData = repositoryService.getModel(modelId); if (modelData == null) { return RespData.invalid().appendMsg("模型不存在").build(); } byte[] bytes = repositoryService.getModelEditorSource(modelData.getId()); if (bytes == null) { return RespData.invalid().appendMsg("请先设计流程定义并成功保存,再进行部署").build(); } JsonNode modelNode = null; try { modelNode = new ObjectMapper().readTree(bytes); BpmnModel model = new BpmnJsonConverter().convertToBpmnModel(modelNode); if (model.getProcesses().size() == 0) { return RespData.invalid().appendMsg("流程定义不符要求,请至少设计一条主线流程").build(); } byte[] bpmnBytes = new BpmnXMLConverter().convertToXML(model); //发布流程 String processName = modelData.getName() + ".bpmn20.xml"; Deployment deployment = repositoryService.createDeployment() .name(modelData.getName()) .key(modelData.getKey()) .category(modelData.getCategory()) .addString(processName, new String(bpmnBytes, StandardCharsets.UTF_8)) .deploy(); modelData.setDeploymentId(deployment.getId()); repositoryService.saveModel(modelData); } catch (IOException e) { e.printStackTrace(); } return RespData.sucess().build(); } }
2.2 StencilsetController
这个类里的方法也就是在解压后的 activiti-5.22.0/modules/activiti-modeler/src/main/java/org/activiti/rest/editor/main
中,经过我自己的改造成了如下的内容。
/** * 流程定义插件所需要的描述----用于汉化 * * @author dgb */ @RestController @RequestMapping("editor") public class StencilsetController { @GetMapping(value = "/stencilset") public String getStencilset() { InputStream stencilsetStream = this.getClass().getClassLoader().getResourceAsStream("stencilset.json"); try { return IOUtils.toString(Objects.requireNonNull(stencilsetStream), "utf-8"); } catch (Exception e) { throw new ActivitiException("Error while loading stencil set", e); } } }
这个方法用到了 stencilset.json
文件,它的位置是在刚解压好的 activiti-5.22.0/modules/activiti-webapp-explorer2/src/main/resources
文件夹下,将其复制到 resources
文件夹下。
2.3 自定义的 NewModel
@Data public class NewModel { private String id; private String name; private String key; private String desc; private String category; }
找到刚才复制到 static
中的文件 app-cfg.js
3.1 修改 app-cfg.js
'use strict'; var ACTIVITI = ACTIVITI || {}; ACTIVITI.CONFIG = { 'contextRoot' : '/activiti', };
将 contextRoot
修改为你自己的 springboot
工程的上下文。
3.2 创建 application.yml
在 springboot
工程 resource
目录下创建 application.yml
,内容如下:
server: servlet: context-path: /activiti port: 8083 spring: profiles: active: dev activiti: database-schema-update: true #自动更新数据库结构 check-process-definitions: false #自动检查、部署流程定义文件 process-definition-location-prefix: classpath:/processes/ #流程定义文件存放目录 datasource: # 数据库连接池 driver-class-name: com.mysql.cj.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource url: jdbc:mysql://localhost:3306/activiti?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true username: xxxx password: xxxx hikari: maximum-pool-size: 500 minimum-idle: 1 idle-timeout: 60000 mvc: static-path-pattern: /static/** resources: static-locations: classpath:/static/
3.3 创建 processes
在 resource
目录下创建 processes
文件夹,这个文件夹是 activiti
默认加载流程定义文件的位置。
3.4 创建 WebAppConfigurer
@Configuration public class WebAppConfigurer implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); } }
4. 整合 Vue
整合 Vue
相对简单一些。在 views
文件夹下创建 workflow
,在这个文件夹下面创建 index.vue
4.1 index.vue
<template> <div class="app-container"> <el-card> <div class="filter-container"> <el-input v-model="query.name" placeholder="模型名称" style="width: 200px;" class="filter-item" /> <el-button class="filter-item" type="primary" icon="el-icon-search" @click="queryList">搜索</el-button> <el-button class="filter-item" style="margin-left: 10px;" type="success" icon="el-icon-plus" @click="handleCreate" >添加</el-button> </div> <el-table :key="tableKey" v-loading="loading" :data="workflow.list" fit stripe highlight-current-row style="width: 100%;" :header-cell-style="{background:'#eef1f6',color:'#606266'}" > <el-table-column label="ID" prop="id" align="center"> <template slot-scope="scope"> <span>{{ scope.row.id }}</span> </template> </el-table-column> <el-table-column label="模型名称" prop="name" align="center"> <template slot-scope="scope"> <span>{{ scope.row.name }}</span> </template> </el-table-column> <el-table-column label="KEY" prop="key" align="center"> <template slot-scope="scope"> <span>{{ scope.row.key }}</span> </template> </el-table-column> <el-table-column label="版本" prop="version" align="center"> <template slot-scope="scope"> <span>{{ scope.row.version }}</span> </template> </el-table-column> <el-table-column label="部署ID" prop="deploymentId" align="center"> <template slot-scope="scope"> <span>{{ scope.row.deploymentId }}</span> </template> </el-table-column> <el-table-column label="创建时间" prop="createTime" align="center" width="150"> <template slot-scope="scope"> <span>{{ scope.row.createTime }}</span> </template> </el-table-column> <el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width" > <template slot-scope="{row}"> <el-tooltip content="流程定义" placement="top"> <i class="el-icon-s-marketing operate-edit" @click="handleDraw(row)" /> </el-tooltip> <el-tooltip content="部署" placement="top"> <i class="el-icon-s-promotion operate-edit" @click="handleDeploy(row)" /> </el-tooltip> <el-tooltip content="编辑" placement="top"> <i class="el-icon-edit-outline operate-edit" @click="handleUpdate(row)" /> </el-tooltip> <el-tooltip content="删除" placement="top"> <i class="el-icon-delete-solid operate-delete" @click="handleDelete(row)" /> </el-tooltip> </template> </el-table-column> </el-table> <pagination v-show="workflow.total>0" :total="workflow.total" :page.sync="query.pageNum" :limit.sync="query.pageSize" @pagination="queryList" /> <el-dialog :title="title" width="35%" top="5vh" :visible.sync="showDialog" :close-on-click-modal="false" @close="cancel" > <model-edit v-if="showDialog" ref="modelForm" :model-id="modelId" /> <div slot="footer" class="dialog-footer"> <el-button @click="cancel">取消</el-button> <el-button type="primary" @click="submit">确定</el-button> </div> </el-dialog> </el-card> </div> </template> <script> import Pagination from '@/components/Pagination' import ModelEdit from './edit' import { mapState, mapActions } from 'vuex' import { MessageBox } from 'element-ui' export default { name: 'WorkFlow', components: { Pagination, ModelEdit }, data() { return { title: '创建模型', loading: false, showDialog: false, tableKey: 1, query: { name: '', pageNum: 1, pageSize: 10 }, modelId: '', actUrl: 'http://127.0.0.1/activiti/static/modeler.html?' } }, computed: { ...mapState({ workflow: state => state.workflow }) }, mounted() { this.queryList() }, methods: { ...mapActions('workflow', ['getList', 'deleteModel', 'deploy']), queryList() { const self = this this.loading = true this.getList({ ...self.query, success: () => { self.loading = false } }) }, handleDraw(row) { window.open(this.actUrl + `modelId=${row.id}`) }, handleDeploy(row) { const self = this MessageBox.confirm('您确定要部署该模型吗?', '确认部署', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { self.deploy({ modelId: row.id, success: () => { self.queryList() } }) }) }, handleCreate() { this.title = '创建模型' this.showDialog = true }, handleUpdate(row) { this.modelId = row.id this.title = '修改模型' this.showDialog = true }, handleDelete(row) { const self = this MessageBox.confirm('您确定要删除该模型吗?', '确认删除', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { self.deleteModel({ modelId: row.id, success: () => { self.queryList() } }) }) }, cancel() { this.modelId = '' this.showDialog = false this.$refs.modelForm.resetForm() }, submit() { const self = this this.$refs.modelForm.submitForm(() => { self.showDialog = false self.queryList() }) } } } </script>
最终整合结果预览如下: