一、简介
1.1 Activiti
Activiti是一个开源的、易扩展的业务流程管理框架,采用BPMN2.0标准定义流程。
1.2 版本选取
版本 | 特点 |
---|---|
5.x | 中文资料最多 |
6.x | 对5.x进行了代码重构,没有什么重大革新和bug修复 |
7.x | 支持spring cloud |
二、BPMN
2.1 概述
业务流程建模与标注(business process modeling notation),使用BPMN可以定义一个业务流程图。
Activiti支持BPMN2.0标准并加入了一些自定义的属性。
2.2 流程定义
下面是一个最简单的流程定义。
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:activiti="http://activiti.org/bpmn"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"
typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath"
targetNamespace="http://www.activiti.org/processdef">
<process id="leave" name="请假流程" isExecutable="true">
<startEvent id="start1" name="开始"></startEvent>
<userTask id="task1" name="请假" activiti:candidateGroups="employee" activiti:formKey="create"></userTask>
<endEvent id="end1" name="结束"></endEvent>
<sequenceFlow id="flow1" sourceRef="start1" targetRef="task1"></sequenceFlow>
<sequenceFlow id="flow2" sourceRef="task1" targetRef="end1"></sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_planOrder">
<bpmndi:BPMNPlane bpmnElement="leave" id="BPMNPlane_planOrder">
<bpmndi:BPMNShape bpmnElement="start1" id="BPMNShape_start1">
<omgdc:Bounds height="30.0" width="30.0" x="90.0" y="75.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="task1" id="BPMNShape_task1">
<omgdc:Bounds height="80.0" width="100.0" x="225.0" y="50.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="end1" id="BPMNShape_end1">
<omgdc:Bounds height="28.0" width="28.0" x="435.0" y="76.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
<omgdi:waypoint x="120.0" y="90.0"></omgdi:waypoint>
<omgdi:waypoint x="225.0" y="90.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
<omgdi:waypoint x="325.0" y="90.0"></omgdi:waypoint>
<omgdi:waypoint x="435.0" y="90.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
定义分成流程和流程图两个部分,根据定义中的流程图定义可以生成流程图如下。
2.3 基本元素
- task,节点任务,包含:用户任务、脚本任务、邮件任务、业务规则任务等
- sequence flow,流,也就是连接线
- gateway,网关,如排他网关、并行网关、包容网关、事件网关
- event,事件,如启动、结束
- listener,监听器
2.4 绘制工具
- eclipse插件
- idea插件
- modeler,activiti内置的web端绘制流程定义的方式
- 手写xml
三、数据库
3.1 表结构概述
表类别 | 描述 |
---|---|
act_hi_* | history,完整的流程信息 |
act_id_* | identity,内置的用户及角色组 |
act_ge_* | general,基本信息 |
act_ru_* | runtime,运行时流程数据 |
act_re_* | repository,流程定义数据 |
3.2 流程与表的关系
- deploy
流程部署成功后,打包上传的 bpmn
和 png
文件属于静态资源文件,保存在 act_ge_bytearray
中;
除了资源文件外,生成的部署信息和流程定义分别保存在 act_re_deployment
和 act_re_procdef
中。
- model
流程定义的一种形式,以 model
形式保存流程,信息保存在表 act_re_model
,act_ge_bytearray
中;
model
流程的部署同 bpmn
流程, 生成 bpmn
和 png
文件保存在 act_ge_bytearray
中。
- processInstance
流程每次启动都会生成一个流程实例,保存在 act_hi_procint
表中, proc_inst_id_
是流程实例的 id
。
- execution
execution
指一个流程实例要执行的过程对象,保存在 act_ru_execution
表中;
execution
表的 id_
字段通常与 proc_inst_id_
字段一致,不一致时代表该 execution
是子流程。
- task
流程图中的每个节点都是一个任务,上一个任务完成后,下一个任务就会被添加到 act_hi_taskinst
和 act_ru_task
表中。
- variable
执行任务过程中产生的变量信息(也包含动态表单的信息)存储在 act_ru_variable
和 act_hi_varinst
表中;
任务完成后,历史变量信息会被存储在 act_hi_detail
表中,该表存储哪些信息由配置中的 HistoryLevel
决定。
- user
任务的执行者信息存储在 act_ru_identitylink
和 act_hi_identitylink
表中
四、API文档
4.1 引入项目
以spring boot工程为例,在pom文件中添加如下依赖即可。
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-engine</artifactId>
<version>5.22.0</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter-basic</artifactId>
<version>5.22.0</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-model</artifactId>
<version>5.22.0</version>
</dependency>
4.2 API
模块 | 描述 |
---|---|
HistoryService | 历史数据操作接口 |
IdentityService | 组织架构操作接口 |
FormService | 任务表单管理 |
RepositoryService | 管理流程定义 |
RuntimeService | 运行时操作 |
TaskService | 任务管理 |
ManagementService | 流程的管理和维护,控制台操作 |
4.3 常用方法
1.查询待办任务
List<Task> taskUser = taskService.createTaskQuery()
.processDefinitionKey(procInstId)
.taskCandidateOrAssigned(userId)
.list();
2.查询历史任务
HistoricTaskInstance task = historyService.createHistoricTaskInstanceQuery()
.taskDefinitionKey(taskDefKey)
.processInstanceId(procInstId)
.singleResult();
3.查询流程实例
HistoricProcessInstance processInstance = historyService
.createHistoricProcessInstanceQuery()
.processInstanceId(procInstId)
.singleResult();
4.部署流程定义
a.zip
repositoryService.createDeployment()
.name(deployFile.getOriginalFilename())
.addZipInputStream(new ZipInputStream(deployFile.getInputStream()))
.deploy();
b.classpath
repositoryService.createDeployment()
.name(deployName)
.addClasspathResource("processes/" + fileName + ".bpmn20.xml")
.addClasspathResource("processes/" + fileName + ".png")
.deploy();
5.启动流程实例
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(procKey);
6.删除流程实例
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId(pi.getId()).singleResult();
if (processInstance == null) {
historyService.deleteHistoricProcessInstance(pi.getId());
} else {
runtimeService.deleteProcessInstance(pi.getId(), "");
historyService.deleteHistoricProcessInstance(pi.getId());
}
五、常用扩展
5.1 自定义表单
在Activiti中,每个节点都可以定义不一样的表单内容。
动态表单是Activiti自带的一种自定义表单的方式,以key-value的形式定义表单,表单以每行一条属性的方式显示。
<userTask activiti:candidateGroups="emp" activiti:exclusive="true" id="usertask6" name="员工发起请假申请">
<extensionElements>
<activiti:formProperty datePattern="yyyy-MM-dd" id="startTime" name="开始时间" type="date"/>
<activiti:formProperty datePattern="yyyy-MM-dd" id="endTime" name="结束时间" type="date"/>
<activiti:formProperty id="restReason" name="请假原因" type="string"/>
</extensionElements>
</userTask>
由于该方式不能满足大部分web应用的需求,Activiti还提供了外置表单。
所谓外置表单,就是通过Task中定义的formKey加载不同的表单模板,从而实现复杂的布局、交互、结构需求。
这里的模板一般指jsp或者thymeleaf这类模板引擎。
<userTask activiti:candidateGroups="manger" activiti:exclusive="true" activiti:formKey="audit.jsp" id="usertask1" name="审批"/>
通过formService获取formKey进行处理,formKey一般来说是路径+名称+后缀。
5.2 动态指派
动态指派的简单实现有两种方式,一种是定义任务监听器,另一种是在流程实例中注入变量。
1.TaskListener
首先实现TaskListenerImpl接口:
public class TaskListenerImpl implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
delegateTask.deleteCandidateGroup("manager");
delegateTask.addCandidateGroup("president");
}
}
然后在流程定义中引用该实现:
<userTask activiti:candidateGroups="leader" activiti:exclusive="true" activiti:formKey="audit.jsp" id="usertask1" name="项目组长审批">
<extensionElements>
<activiti:taskListener event="create" class="com.xxx.TaskListenerImpl"/>
</extensionElements>
</userTask>
除了节点监听器,还有全局监听器(ExecutionListener)。
2.注入变量
<userTask id="hrAudit" name="人事审批" activiti:assignee="${hrUserId}" />
在执行该任务前,需要在流程中设置该变量。
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("hrUserId", hrUserId);
taskService.complete(taskId, variables);
其实监听器也可以结合spring的注解进行注入。
5.3 组织架构
Activiti内置了一套组织架构管理,结构较为简单,不符合应用场景。
表 | 介绍 |
---|---|
act_id_group | 角色 |
act_id_info | 用户扩展信息 |
act_id_membership | 用户与角色的关联表 |
act_id_user | 用户基本信息 |
自定义组织架构的实现参考了博客:重构用户组数据的多种方案比较
原理
自定义 sessionFactory
替换原生的数据源模块。
实现
1.配置 activiti
使用自定义 sessionFactory
。
@Configuration
@AutoConfigureBefore(org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class)
public class ActivitiConfiguration implements ProcessEngineConfigurationConfigurer {
@Resource(name = "customGroupEntityFactory")
private SessionFactory groupManagerFactory;
@Resource(name = "customUserEntityFactory")
private SessionFactory userManagerFactory;
@Override
public void configure(SpringProcessEngineConfiguration pec) {
pec.setDbIdentityUsed(false);
List<SessionFactory> customSessionFactories = new ArrayList<>();
customSessionFactories.add(userManagerFactory);
customSessionFactories.add(groupManagerFactory);
if (pec.getCustomSessionFactories() == null) {
pec.setCustomSessionFactories(customSessionFactories);
} else {
pec.getCustomSessionFactories().addAll(customSessionFactories);
}
}
}
2.自定义 UserManagerFactory
和 GroupManagerFactory
实现 SessionFactory
接口。
@Service
public class CustomUserEntityFactory implements SessionFactory{
private CustomUserEntityManager customUserEntityManager;
@Autowired
public void setUserEntityManager(CustomUserEntityManager customUserEntityManager) {
this.customUserEntityManager = customUserEntityManager;
}
@Override
public Class<?> getSessionType() {
return UserIdentityManager.class;
}
@Override
public Session openSession() {
return customUserEntityManager;
}
}
3.继承 activiti
定义的 UserEntityManager
和 GroupEntityManager
, 重写主要方法。
替换后可删除表 act_id_*
,该方案在使用的时候需要避免调用未重写的方法。
@Service
public class CustomUserEntityManager extends UserEntityManager {
@Resource
private OemUserRepo oemUserRepo;
@Resource
private CategoryRepo categoryRepo;
@Override
public User createNewUser(String userId) {
return new UserEntity(userId);
}
@Override
public void updateUser(User updatedUser) {
throw new CustomEntityManagerException(CANNOT_USE_FUNC);
}
@Override
public UserEntity findUserById(String userId) {
return ActivitiUtil.toActivitiUser(oemUserRepo.getOne(Integer.parseInt(userId)));
}
@Override
public void deleteUser(String userId) {
throw new CustomEntityManagerException(CANNOT_USE_FUNC);
}
@Override
public List<Group> findGroupsByUser(String userId) {
List<Group> list = new ArrayList<>();
// list by customize service
return list;
}
// need override all function
}
5.4 流程图及标识
通过BPMN绘制工具获取的流程定义,除了xml文件,还可以生成图片。
部署后可通过流程实例查询到流程图。
InputStream inputStream = repositoryService.getResourceAsStream(deploymentId, diagramResourceName);
配合流程定义中的位置信息,可以实现当前节点定位。
ProcessInstance pi = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult();
ActivityImpl activityImpl = processDefinitionEntity.findActivity(pi.getActivityId());
modelView.addObject("x", activityImpl.getX());
modelView.addObject("y", activityImpl.getY());
modelView.addObject("width", activityImpl.getWidth());
modelView.addObject("height", activityImpl.getHeight());
5.5 回退的实现
1. 增加中转节点
实际上不增加中转节点直接指向需要回退的节点也没有什么问题。
2. 自定义扩展
实现思路:
获取当前活动实例,清空当前节点的下一步,获取当前节点的上一步,设置当前节点的下一步为上一步。
自定义扩展可以实现回退到指定节点。
5.6 指派与委托
当执行人不能执行当前任务时,可委托给他人执行。task实例有两个字段ASSIGNEE_
和OWNER_
来保存这两个值。
可以定义在流程定义中。
<userTask id="usertask2" name="审批" activiti:assignee="manager" activiti:formKey="hello.jsp"></userTask>
也可以在运行时设置。
Task task=taskService.createTaskQuery().singleResult();
// set assignee
taskService.claim(task.getId(), "billy");
// change assignee and set owner
taskService.delegateTask(task.getId(), "cc");
5.7 流程挂起
需要暂停流程而不是删除流程时,可以对流程实例或定义进行挂起,挂起的流程可以重新激活。
挂起的流程实例不可以继续执行,挂起的流程定义不可以新增流程实例。
// suspend process instance
ProcessInstance pi = runtimeService.createProcessInstanceQuery()
.processInstanceId(procInstId).singleResult();
if (!pi.isSuspended()) {
runtimeService.suspendProcessInstanceById(procInstId);
}
// active process instance
if (pi.isSuspended()) {
runtimeService.activateProcessInstanceById(procInstId);
}
// suspend process definition
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionId(procDefId)
.singleResult();
if (!processDefinition.isSuspended()) {
repositoryService.suspendProcessDefinitionById(procDefId);
}
// activie process definition
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionId(procDefId)
.singleResult();
if (processDefinition.isSuspended()) {
repositoryService.activateProcessDefinitionById(procDefId, true, null);
}
挂起与激活实际上是变更了字段SUSPENSION_STATE_
,在源码中可以看到该值的定义。
SuspensionState ACTIVE = new SuspensionState.SuspensionStateImpl(1, "active");
SuspensionState SUSPENDED = new SuspensionState.SuspensionStateImpl(2, "suspended");
5.8 其他
通过代码动态定义流程,全局事件处理,邮件服务,定时任务,规则任务,租户
六、modeler
Activiti官方demo中提供了web端绘制流程定义的方式,只要将该部分代码剥离拷贝至自己的项目中稍作修改即可,绘制页面有汉化包。
参考博客:https://blog.csdn.net/h1059141989/article/details/79870043
最终页面展示:
绘制的model可以先进行校验。
Model modelData = repositoryService.getModel(modelId);
ObjectNode modelNode = (ObjectNode) new ObjectMapper().readTree(repositoryService.getModelEditorSource(modelData.getId()));
BpmnModel model = new BpmnJsonConverter().convertToBpmnModel(modelNode);
ProcessValidatorFactory processValidatorFactory = new ProcessValidatorFactory();
ProcessValidator defaultProcessValidator = processValidatorFactory.createDefaultProcessValidator();
List<ValidationError> validate = defaultProcessValidator.validate(model);
model可以直接部署,也可以导出xml或png部署静态资源文件。
// deploy
Model modelData = repositoryService.getModel(modelId);
ObjectNode modelNode = (ObjectNode) new ObjectMapper().readTree(repositoryService.getModelEditorSource(modelData.getId()));
BpmnModel model = new BpmnJsonConverter().convertToBpmnModel(modelNode);
String processName = modelData.getName() + ".bpmn20.xml";
repositoryService.createDeployment().name(modelData.getName()).addBpmnModel(processName, model).deploy();
// export xml
Model modelData = repositoryService.getModel(modelId);
BpmnJsonConverter jsonConverter = new BpmnJsonConverter();
JsonNode editorNode = new ObjectMapper().readTree(repositoryService.getModelEditorSource(modelData.getId()));
BpmnModel bpmnModel = jsonConverter.convertToBpmnModel(editorNode);
BpmnXMLConverter xmlConverter = new BpmnXMLConverter();
byte[] bpmnBytes = xmlConverter.convertToXML(bpmnModel);
ByteArrayInputStream in = new ByteArrayInputStream(bpmnBytes);
IOUtils.copy(in, response.getOutputStream());
String filename = bpmnModel.getMainProcess().getId() + ".bpmn20.xml";
response.setHeader("Content-Disposition", "attachment; filename=" + filename);
response.flushBuffer();
// export png
Model modelData = repositoryService.getModel(modelId);
ObjectNode modelNode;
try {
modelNode = (ObjectNode) new ObjectMapper()
.readTree(repositoryService.getModelEditorSource(modelData.getId()));
BpmnModel model = new BpmnJsonConverter().convertToBpmnModel(modelNode);
ProcessDiagramGenerator processDiagramGenerator = new DefaultProcessDiagramGenerator();
ProcessEngineConfiguration processEngineConfiguration = ProcessEngines.getDefaultProcessEngine().getProcessEngineConfiguration();
InputStream inputStream = processDiagramGenerator.generateDiagram(model, "png",
processEngineConfiguration.getActivityFontName(),
processEngineConfiguration.getLabelFontName(),
processEngineConfiguration.getAnnotationFontName(),
processEngineConfiguration.getClassLoader(), 1.0);
OutputStream out = response.getOutputStream();
for (int b = -1; (b = inputStream.read()) != -1; ) {
out.write(b);
}
out.close();
inputStream.close();
} catch (IOException e) {
// logger
}