SpringBoot集成Activiti和UI
# SpringBoot集成Activiti和UI
版本配置信息:
SpringBoot:2.6.3
Activiti:6.0.0
JDK:8
# 1、SpringBoot集成Activiti
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.6.3</version>
<relativePath/>
</parent>
<properties>
<java.version>8</java.version>
<apache.xmlgraphics.version>1.7</apache.xmlgraphics.version>
<activiti.version>6.0.0</activiti.version>
<hibernate.version>5.4.33.Final</hibernate.version>
<jackson.version>2.11.0</jackson.version>
<mysql-connector.version>5.1.49</mysql-connector.version>
</properties>
<dependencies>
<!-- spring boot依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- junit依赖包 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- MySQL的jdbc驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
<!-- Activiti依赖包 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter-basic</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-json-converter</artifactId>
<version>${activiti.version}</version>
<exclusions>
<exclusion>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-model</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>editor-image-generator</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-layout</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- hibernate依赖包 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.0.2.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.0-api</artifactId>
<version>1.0.1.Final</version>
</dependency>
<!-- commons-io工具包 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.10.0</version>
</dependency>
<!-- java持久化API -->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0</version>
</dependency>
<!-- httpclient工具包 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.10</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
<!-- jackson依赖包 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
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
# 2、配置数据库和Activiti
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/bpm?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.properties.hibernate.cache.use_second_level_cache=false
spring.jpa.properties.hibernate.cache.use_query_cache=false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
# 将ddl-auto方式设置为更新数据库
spring.jpa.hibernate.ddl-auto=update
# 配置命名策略
spring.jpa.hibernate.naming.physical-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
# 注意这里,开启true会自动创建activiti表
spring.activiti.database-schema-update=true
# 启动时候不检查流程配置文件
spring.activiti.check-process-definitions=false
# 使用spring jpa
spring.activiti.jpa-enabled=true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 3、集成Activiti Modeler(在线绘制流程图)
# 3.1、复制Activiti Modeler前端资源文件
预先下载Activiti6官方包: https://www.activiti.org/get-started
下载完成之后解压,找到activiti-6.0.0\wars包下的activiti-app.war文件,并将其解压,解压后进入editor路径下,如图所示
1)、将editor-app整个文件夹复制到项目的src/main/resources/static路径下
2)、将activiti-app/editor路径下的index.html文件复制到项目的src/main/resources/static路径下
3)、将activiti-app/editor路径下的views文件夹复制到项目的src/main/resources/static路径下
4)、复制libs目录及文件:将activiti-app路径下的libs文件夹复制到项目的src/main/resources/static/editor-app路径下
5)、将activiti-app/styles路径下的文件夹及文件复制到项目的src/main/resources/static/editor-app/css路径下
6)、将activiti-app/editor/styles路径下的文件复制到项目的src/main/resources/static/editor-app/css路径下
7)、将activiti-app/scripts路径下的文件夹及文件复制到项目的src/main/resources/static/editor-app路径下
8)、将activiti-app/editor/scripts路径下的controllers文件夹、services文件夹、.js文件复制到src/main/resources/static/editor-app/common路径下
9)、将activiti-app/editor/scripts/configuration路径下的文件复制到项目的src/main/resources/static/editor-app/configuration路径下
10)、将activiti-app/WEB-INF/lib路径下的activiti-app-logic-6.0.0.jar用解压缩工具解压缩,复制其中的stencilset_bpmn.json文件到项目的src/main/resources/static/editor-app/stencilsets路径下
11)、将activiti-app/editor路径下的i18n文件夹复制到项目的src/main/resources/static/路径下
12)、将activiti-app路径下的fonts文件夹复制到src/main/resources/static/editor-app路径下
13)、将activiti-app路径下的images文件夹复制到src/main/resources/static/editor-app路径下
# 3.2、创建Activiti Modeler依赖的后端接口
1)Entity类
package com.bpm.example.modeler.domain;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
@Data
@Entity
@Table(name="ACT_DE_MODEL")
public class Model {
public static final int MODEL_TYPE_BPMN = 0;
public static final int MODEL_TYPE_FORM = 2;
public static final int MODEL_TYPE_APP = 3;
public static final int MODEL_TYPE_DECISION_TABLE = 4;
@Id
@GeneratedValue(generator="modelIdGenerator")
@GenericGenerator(name="modelIdGenerator", strategy="uuid2")
@Column(name="id", unique=true, length = 50)
protected String id;
@Column(name="name")
protected String name;
@Column(name="model_key")
protected String key;
@Column(name="description")
protected String description;
@Temporal(TemporalType.TIMESTAMP)
@Column(name="created")
protected Date created;
@Temporal(TemporalType.TIMESTAMP)
@Column(name="last_updated")
protected Date lastUpdated;
@Column(name="created_by")
private String createdBy;
@Column(name="last_updated_by")
private String lastUpdatedBy;
@Column(name="version")
protected int version;
@Column(name="model_editor_json", columnDefinition = "longtext")
protected String modelEditorJson;
@Column(name="model_comment")
protected String comment;
@Column(name="model_type")
protected Integer modelType;
@Column(name="thumbnail", columnDefinition = "longblob")
private byte[] thumbnail;
public Model()
{
this.created = new Date();
}
}
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
2)、Repository类
package com.bpm.example.modeler.repository;
import java.util.List;
import com.bpm.example.modeler.domain.Model;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public abstract interface ModelRepository extends JpaRepository<Model, String> {
@Query("from Model as model where (lower(model.name) like :filter or lower(model.description) like :filter) and model.modelType = :modelType")
public abstract List<Model> findModelsByModelType(@Param("modelType") Integer paramInteger, @Param("filter") String paramString);
@Query("from Model as model where model.modelType = :modelType")
public abstract List<Model> findModelsByModelType(@Param("modelType") Integer paramInteger);
@Query("from Model as model where model.id = :parentModelId")
public abstract List<Model> findModelsByParentModelId(@Param("parentModelId") String paramString);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
3)、Controller类
package com.bpm.example.modeler.controller;
import com.bpm.example.modeler.domain.Model;
import com.bpm.example.modeler.repository.ModelRepository;
import com.bpm.example.modeler.service.ModelImageService;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.activiti.bpmn.converter.BpmnXMLConverter;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.bpmn.model.Process;
import org.activiti.editor.language.json.converter.BpmnJsonConverter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.http.HttpStatus;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@RestController
public class ModelBpmnController {
private static final Logger logger = LoggerFactory.getLogger(ModelBpmnController.class);
@Autowired
protected ModelRepository modelRepository;
@Autowired
protected ObjectMapper objectMapper;
/**
* GET /rest/models/{modelId}/bpmn -> Get BPMN 2.0 xml
*/
@RequestMapping(value = "/rest/models/{processModelId}/bpmn20", method = RequestMethod.GET)
public void getProcessModelBpmn20Xml(HttpServletResponse response, @PathVariable String processModelId) throws IOException {
if (processModelId == null) {
throw new RuntimeException("No process model id provided");
}
Optional<Model> optional = this.modelRepository.findById(processModelId);
Model model = optional.get();
generateBpmn20Xml(response, model);
}
protected void generateBpmn20Xml(HttpServletResponse response, Model model) {
String name = model.getName().replaceAll(" ", "_");
response.setHeader("Content-Disposition", "attachment; filename=" + name + ".bpmn20.xml");
if (model.getModelEditorJson() != null) {
try {
ServletOutputStream servletOutputStream = response.getOutputStream();
response.setContentType("application/xml");
BpmnModel bpmnModel = getBpmnModel(model);
byte[] xmlBytes = getBpmnXML(bpmnModel);
BufferedInputStream in = new BufferedInputStream(new ByteArrayInputStream(xmlBytes));
byte[] buffer = new byte[8096];
while (true) {
int count = in.read(buffer);
if (count == -1) {
break;
}
servletOutputStream.write(buffer, 0, count);
}
// Flush and close stream
servletOutputStream.flush();
servletOutputStream.close();
} catch (Exception e) {
throw new RuntimeException("Could not generate BPMN 2.0 xml");
}
}
}
private BpmnModel getBpmnModel(Model model) {
BpmnModel bpmnModel = null;
try {
Map<String, Model> formMap = new HashMap<String, Model>();
Map<String, Model> decisionTableMap = new HashMap<String, Model>();
List<Model> referencedModels = modelRepository.findModelsByParentModelId(model.getId());
for (Model childModel : referencedModels) {
if (Model.MODEL_TYPE_FORM == childModel.getModelType()) {
formMap.put(childModel.getId(), childModel);
} else if (Model.MODEL_TYPE_DECISION_TABLE == childModel.getModelType()) {
decisionTableMap.put(childModel.getId(), childModel);
}
}
bpmnModel = getBpmnModel(model, formMap, decisionTableMap);
} catch (Exception e) {
throw new RuntimeException("Could not generate BPMN 2.0 model");
}
return bpmnModel;
}
private BpmnModel getBpmnModel(Model model, Map<String, Model> formMap, Map<String, Model> decisionTableMap) {
try {
ObjectNode editorJsonNode = (ObjectNode) objectMapper.readTree(model.getModelEditorJson());
Map<String, String> formKeyMap = new HashMap<String, String>();
for (Model formModel : formMap.values()) {
formKeyMap.put(formModel.getId(), formModel.getKey());
}
Map<String, String> decisionTableKeyMap = new HashMap<String, String>();
for (Model decisionTableModel : decisionTableMap.values()) {
decisionTableKeyMap.put(decisionTableModel.getId(), decisionTableModel.getKey());
}
BpmnJsonConverter bpmnJsonConverter = new BpmnJsonConverter();
return bpmnJsonConverter.convertToBpmnModel(editorJsonNode, formKeyMap, decisionTableKeyMap);
} catch (Exception e) {
throw new RuntimeException("Could not generate BPMN 2.0 model");
}
}
public byte[] getBpmnXML(Model model) {
BpmnModel bpmnModel = getBpmnModel(model);
return getBpmnXML(bpmnModel);
}
public byte[] getBpmnXML(BpmnModel bpmnModel) {
for (Process process : bpmnModel.getProcesses()) {
if (StringUtils.isNotEmpty(process.getId())) {
char firstCharacter = process.getId().charAt(0);
// no digit is allowed as first character
if (Character.isDigit(firstCharacter)) {
process.setId("a" + process.getId());
}
}
}
BpmnXMLConverter bpmnXMLConverter = new BpmnXMLConverter();
byte[] xmlBytes = bpmnXMLConverter.convertToXML(bpmnModel);
return xmlBytes;
}
}
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
# 3.3、修改Activiti Modeler文件
由于前面复制前端资源时调整了原有的文件路径、新建了依赖的后端接口,所以这里需要修改Activiti Modeler,重新引用新路径下的资源文件,以及将其适配到新建的后端接口上
1)、修改项目中流程设计器主页src/main/resources/static/index.html引用资源的路径
2)、修改项目路径src/main/resources/static/editor-app/common下的app.js文件
3)、修改项目路径src/main/resources/static/editor-app/configuration/下的url-config.js文件,使其调用创建的后端接口
4)、修改项目路径src/main/resources/static/editor-app下的app-cfg.js文件,设置项目根路径
5)、修改项目路径src/main/resources/static/editor-app/common/controllers下的processes.js文件,适配前面创建的后端接口
6)、修改项目路径src/main/resources/static/editor-app下的editor-controller.js文件
到此为止,启动Springboot项目就可以看到流程界面和设计流程了http://localhost:8080/index.html

# 3.4、汉化Activiti Modeler
语言配置文件为src/main/resources/static/editor-app/stencilsets路径下的stencilset_bpmn.json文件
将stencilset_bpmn_cn.json文件复制到src/main/resources/static/editor-app/stencilsets路径下,然后在Controller中读取cn配置文件,完成汉化
# 4、自定义流程属性
Activiti每种流程元素都具备自己的属性,但这些默认的属性往往并不能完全满足实际业务的需要。为了满足各种业务流程需求,实际应用时经常需要为Activiti的流程元素扩展出一系列自定义属性,如流程是否允许撤销、任务允许催办等。为了实现这个目标,需要在流程设计器中增加自定义属性的配置功能,在流程部署时将自定义属性写入流程定义,并在流程流转时工作流引擎获取自定义扩展属性,从而进行相应的后续处理动作
比如需要在业务节点增加属性【允许直送】,业务场景是第五个节点的审核岗打回单据之后,提交人可以直接提交到第五节点,而不需要再次重复经历2、3、4节点的审批
那么可以如下操作:
1、找到stencilset.json文件
2、在propertyPackages属性中增加是否直送的属性
3、这个属性是加入到【用户任务】中的,也就是只有点击用户任务才会多出来这个属性,所以找到stencils元素中的用户任务,在propertyPackages中加入刚才的扩展属性的name值,然后重启服务,在UI设计器中点击用户任务就可以看到下面的属性多出来【允许直送】的属性
# 5、自定义属性解析
由Activiti Modeler设计的流程被转换为JSON格式文本存储到数据库中,而Activiti引擎只能识别符合BPMN 2.0规范的XML格式,所以在部署流程时会先将JSON格式文本转换为BpmnModel,再将BpmnModel转换成XML格式文本保存进数据库。23.2.1小节中为用户任务节点增加了一个自定义扩展属性,但如果此时直接部署,生成的流程定义文件中不存在这个属性,因为这个属性不能被Activiti识别。这时就需要在JSON格式文本转换为BpmnModel的过程中加以干预,对自定义属性进行解析
刚才加入的属性是属于【用户任务】,也就是默认解析器是org.activiti.editor.language.json.converter. UserTaskJsonConverter,那么就需要自定义解析器并继承UserTaskJsonConverter,重写其中的方法
package com.bpm.example.modeler.converter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.activiti.bpmn.model.BaseElement;
import org.activiti.bpmn.model.ExtensionElement;
import org.activiti.bpmn.model.FlowElement;
import org.activiti.bpmn.model.UserTask;
import org.activiti.editor.language.json.converter.BaseBpmnJsonConverter;
import org.activiti.editor.language.json.converter.UserTaskJsonConverter;
import java.util.List;
import java.util.Map;
/**
* 自定义用户任务json转换器
* @author zhaoyubo
* @date 2024/11/26 15:00
*/
public class CustomUserTaskJsomConverter extends UserTaskJsonConverter {
@Override
protected void convertElementToJson(ObjectNode propertiesNode, BaseElement baseElement) {
super.convertElementToJson(propertiesNode, baseElement);
UserTask userTask = (UserTask) baseElement;
//解析
Map<String, List<ExtensionElement>> extensionElements = userTask.getExtensionElements();
if(extensionElements != null && extensionElements.containsKey("process_direct_feedpackage")){
ExtensionElement e = extensionElements.get("process_direct_feedpackage").get(0);
setPropertyValue("process_direct_feedpackage", e.getElementText(), propertiesNode);
}
}
@Override
protected FlowElement convertJsonToElement(JsonNode elementNode, JsonNode modelNode, Map<String, JsonNode> shapeMap) {
FlowElement flowElement = super.convertJsonToElement(elementNode,
modelNode, shapeMap);
//解析自定义扩展属性
ExtensionElement extensionElement = new ExtensionElement();
extensionElement.setNamespace(NAMESPACE);
extensionElement.setNamespacePrefix("modeler");
extensionElement.setName("process_direct_feedpackage");
String direct = getPropertyValueAsString("process_direct_feedpackage", elementNode);
extensionElement.setElementText(direct);
UserTask userTask = (UserTask) flowElement;
userTask.addExtensionElement(extensionElement);
return userTask;
}
public static void fillTypes(Map<String, Class<? extends BaseBpmnJsonConverter>> convertersToBpmnMap,
Map<Class<? extends BaseElement>, Class<? extends BaseBpmnJsonConverter>> convertersToJsonMap) {
fillJsonTypes(convertersToBpmnMap);
fillBpmnTypes(convertersToJsonMap);
}
public static void fillJsonTypes(Map<String, Class<? extends BaseBpmnJsonConverter>> convertersToBpmnMap) {
convertersToBpmnMap.put(STENCIL_TASK_USER, CustomUserTaskJsonConverter.class);
}
public static void fillBpmnTypes(Map<Class<? extends BaseElement>, Class<? extends BaseBpmnJsonConverter>> convertersToJsonMap) {
convertersToJsonMap.put(UserTask.class, CustomUserTaskJsonConverter.class);
}
}
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