最近在学习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>
最终整合结果预览如下: