整合 Springboot-Activiti-Vue 流程定义

最近在学习Activiti工作流引擎,为了在流程定义简便,Activit官方的流程定义插件需要集成到项目中,今天把这个的整合过程记录于此,以便大家参考。

一、准备工作

本次整合所使用的版本为springboot:2.2.2.RELEASEActiviti:6.0.0,下载Activiti-5.22.0只是为了使用其中的流程定义插件。

二、整合

1. 创建springboot工程

创建一个Springboot工程,并把相关的依赖包导入pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<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-viewereditor-appmodeler.html这三个复制到上一步创建好的springboot工程resource下的static中,如下:

接下来在springboot创建ModelControllerStencilsetController两个类:

2.1 ModelController

这个类里的方法也就是在解压后的activiti-5.22.0\modules\activiti-modeler\src\main\java\org\activiti\rest\editor\model中,经过我自己的改造成了如下的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
/**
* 模型管理类
*
* @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中,经过我自己的改造成了如下的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 流程定义插件所需要的描述----用于汉化
*
* @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
1
2
3
4
5
6
7
8
9
@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
1
2
3
4
5
6
7
'use strict';

var ACTIVITI = ACTIVITI || {};

ACTIVITI.CONFIG = {
'contextRoot' : '/activiti',
};

contextRoot修改为你自己的springboot工程的上下文。

3.2 创建application.yml

springboot工程resource目录下创建application.yml,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
1
2
3
4
5
6
7
8
@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
}
3.5 禁用spring-security

由于activiti强依赖spring-security,为了项目正常启动在启动类过滤SecurityAutoConfiguration.class

1
2
3
4
5
6
7
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class ActivitiApp {

public static void main(String[] args) {
SpringApplication.run(ActivitiApp.class, args);
}
}

4. 整合Vue

整合Vue相对简单一些。在views文件夹下创建workflow,在这个文件夹下面创建index.vue

4.1 index.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
<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>

最终整合结果预览如下:

5. 获取源码

请关注微信公众号:「特想学英语」并回复:activiti

原文作者: dgb8901,yinxing

原文链接: https://www.itwork.club/2019/12/12/springboot-activiti-vue/

版权声明: 转载请注明出处

为您推荐

体验小程序「简易记账」

关注公众号「特想学英语」

在 HTML 里写模块化的 JS 代码