整合 Springboot-Activiti-Vue 流程定义
最近在学习Activiti工作流引擎,为了在流程定义简便,Activit官方的流程定义插件需要集成到项目中,今天把这个的整合过程记录于此,以便大家参考。
一、准备工作
本次整合所使用的版本为 springboot:2.2.2.RELEASE
, Activiti:6.0.0
,下载 Activiti-5.22.0
只是为了使用其中的流程定义插件。
-
创建一个 SpringBoot 工程
-
这里假设你已经搭建好了一个
Vue
项目
二、整合
1. 创建 springboot
工程
创建一个 Springboot
工程,并把相关的依赖包导入 pom.xml
中
org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-tomcat org.springframework.boot spring-boot-starter-undertow org.springframework.boot spring-boot-starter-cache org.activiti activiti-engine 6.0.0 org.activiti activiti-json-converter 6.0.0 org.activiti activiti-spring-boot-starter-basic 6.0.0 commons-io commons-io 1.4 org.apache.xmlgraphics batik-codec 1.7 org.apache.xmlgraphics batik-css 1.7 org.apache.xmlgraphics batik-svg-dom 1.7 org.apache.xmlgraphics batik-svggen 1.7
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> 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 models = modelQuery.listPage(pageNum - 1, pageSize); PageInfo pageInfo = PageUtil.toPageInfo(models); return RespData.ok(pageInfo); } /** * 保存模型 * * @param newModel * @return */ @PostMapping public RespData 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 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 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; }
3. 修改配置
找到刚才复制到 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
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() }) } } }搜索 添加{{ scope.row.id }} {{ scope.row.name }} {{ scope.row.key }} {{ scope.row.version }} {{ scope.row.deploymentId }} {{ scope.row.createTime }} 0" :total="workflow.total" :page.sync="query.pageNum" :limit.sync="query.pageSize" @pagination="queryList" />
最终整合结果预览如下: