前言:
系统的核心根本上是业务流程,工作流只是协助进行业务流程管理。
在没有使用工作流引擎时,可以采用状态字段来跟踪流程的变化情况,这样不同角色的用户,通过状态字段的取值来决定记录是否显示。针对有权限可以查看的记录,当前用户根据自己的角色来决定审批是否通过的操作。如果通过将状态字段设置一个值,否则设置另一个值。
通过状态字段虽然能做到流程控制,但是当流程发生变更时,所编写的代码也要进行调整。
Activiti是一个工作流引擎, activiti可以将业务系统中复杂的业务流程抽取出来,使用专门的建模语言BPMN2.0进行定义,业务流程按照预先定义的流程进行执行,实现了系统的流程由activiti进行管理,减少业务系统由于流程变更进行系统升级改造的工作量,从而提高系统的健壮性,同时也减少了系统开发维护成本。
Activiti工作机制:
Activiti解析流程图文件,将每个流程节点的数据读取保存到数据库中,对于流程图流程的增删,只是在表中多或少一条记录,不需要改变源代码。
Activiti 的表都以 ACT_ 开头,第二部分是表示表的用途的两个字母标识。用途也和服务的 API 对应。
① ACT_RE :RE表示 repository。这个前缀的表包含了流程定义和流程静态资源。
② ACT_RU:RU表示 runtime。这些运行时的表,包含流程实例,任务,变量,异步任务,等运行中的数据。Activiti 只在流程实例执行过程中保存这些数据, 在流程结束时就会删除这些记录。这样运行时表可以一直很小速度很快。
③ ACT_HI:HI表示 history。这些表包含历史数据,比如历史流程实例, 变量,任务。
④ ACT_GE :GE 表示 general。通用数据, 用于不同场景下。
表分类 | 表名 | 解释 |
---|---|---|
一般数据 | ||
ACT_GE_BYTEARRAY | 通用的流程定义和流程资源 | |
ACT_GE_PROPERTY | 系统相关属性 | |
流程历史记录 | ||
ACT_HI_ACTINST | 历史流程实例 | |
ACT_HI_ATTACHMENT | 历史流程附件 | |
ACT_HI_COMMENT | 历史说明性信息 | |
ACT_HI_DETAIL | 历史流程运行中的细节信息 | |
ACT_HI_IDENTITYLINK | 历史流程运行过程中用户信息 | |
ACT_HI_PROCINST | 历史流程实例 | |
ACT_HI_TASKINST | 历史任务实例 | |
ACT_HI_VARINST | 历史的流程运行中的变量信息 | |
流程定义表 | ||
ACT_RE_DEPLOYMENT | 部署单元信息 | |
ACT_RE_MODEL | 模型信息 | |
ACT_RE_PROCDEF | 已部署的流程定义 | |
运行实例表 | ||
ACT_RU_EVENT_SUBSCR | 运行时事件 | |
ACT_RU_EXECUTION | 运行时流程执行实例 | |
ACT_RU_IDENTITYLINK | 运行时用户关系信息,存储任务节点与参与者的相关信息 | |
ACT_RU_JOB | 运行时作业 | |
ACT_RU_TASK | 运行时任务 | |
ACT_RU_VARIABLE | 运行时变量表 |
Service是工作流引擎提供用于进行工作流部署、执行、管理的服务接口,我们使用这些接口可以就是操作服务对应的数据表。
//通过ProcessEngineConfiguration创建ProcessEngine
ProcessEngine processEngine = processEngineConfiguration.buildProcessEngine();
工作流引擎(ProcessEngine),相当于一个门面接口,通过ProcessEngineConfiguration创建processEngine,通过ProcessEngine创建各个service接口。
Service总览
RepositoryService | activiti的资源管理类 |
RuntimeService | activiti的流程运行管理类 |
TaskService | activiti的任务管理类 |
HistoryService | activiti的历史管理类 |
ManagerService | activiti的引擎管理类 |
使用Activiti提供的api把流程定义内容(.bpmn文件)存储在数据库中,在Activiti执行过程中可以查询定义的内容。
@SpringBootTest
public class Part1_Deployment {
@Autowired
private RepositoryService repositoryService;
//通过bpmn部署流程
@Test
public void initDeploymentBPMN(){
String filename="BPMN/Part4_Task_claim.bpmn";
// String pngname="BPMN/Part1_Deployment.png";
Deployment deployment=repositoryService.createDeployment()
.addClasspathResource(filename)
//.addClasspathResource(pngname)//图片
.name("流程部署测试候选人task")
.deploy();
System.out.println(deployment.getName());
}
//通过ZIP部署流程
@Test
public void initDeploymentZIP() {
InputStream fileInputStream = this.getClass()
.getClassLoader()
.getResourceAsStream("BPMN/Part1_DeploymentV2.zip");
ZipInputStream zip=new ZipInputStream(fileInputStream);
Deployment deployment=repositoryService.createDeployment()
.addZipInputStream(zip)
.name("流程部署测试zip")
.deploy();
System.out.println(deployment.getName());
}
//查询流程部署
@Test
public void getDeployments() {
List list = repositoryService.createDeploymentQuery().list();
for(Deployment dep : list){
System.out.println("Id:"+dep.getId());
System.out.println("Name:"+dep.getName());
System.out.println("DeploymentTime:"+dep.getDeploymentTime());
System.out.println("Key:"+dep.getKey());
}
}
}
影响的表:
act_ge_bytearray
act_re_deployment
act_re_procdef
使用流程建模工具定义的.bpmn文件,通过xml定义业务流程。
@SpringBootTest
public class Part2_ProcessDefinition {
@Autowired
private RepositoryService repositoryService;
//查询流程定义
@Test
public void getDefinitions(){
List list = repositoryService.createProcessDefinitionQuery()
.list();
for(ProcessDefinition pd : list){
System.out.println("------流程定义--------");
System.out.println("Name:"+pd.getName());
System.out.println("Key:"+pd.getKey());
System.out.println("ResourceName:"+pd.getResourceName());
System.out.println("DeploymentId:"+pd.getDeploymentId());
System.out.println("Version:"+pd.getVersion());
}
}
//删除流程定义
@Test
public void delDefinition(){
String pdID="44b15cfe-ce3e-11ea-92a3-dcfb4875e032";
repositoryService.deleteDeployment(pdID,true);
System.out.println("删除流程定义成功");
}
}
启动一个流程实例表示开始一次业务流程的运行。
@SpringBootTest
public class Part3_ProcessInstance {
@Autowired
private RuntimeService runtimeService;
//初始化流程实例
@Test
public void initProcessInstance(){
//1、获取页面表单填报的内容,请假时间,请假事由,String fromData
//2、fromData 写入业务表,返回业务表主键ID==businessKey
//3、把业务数据与Activiti7流程数据关联
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("myProcess_claim","bKey002");
System.out.println("流程实例ID:"+processInstance.getProcessDefinitionId());
}
//获取流程实例列表
@Test
public void getProcessInstances(){
List list = runtimeService.createProcessInstanceQuery().list();
for(ProcessInstance pi : list){
System.out.println("--------流程实例------");
System.out.println("ProcessInstanceId:"+pi.getProcessInstanceId());
System.out.println("ProcessDefinitionId:"+pi.getProcessDefinitionId());
System.out.println("isEnded"+pi.isEnded());
System.out.println("isSuspended:"+pi.isSuspended());
}
}
//暂停与激活流程实例
@Test
public void activitieProcessInstance(){
// runtimeService.suspendProcessInstanceById("73f0fb9a-ce5b-11ea-bf67-dcfb4875e032");
//System.out.println("挂起流程实例");
runtimeService.activateProcessInstanceById("73f0fb9a-ce5b-11ea-bf67-dcfb4875e032");
System.out.println("激活流程实例");
}
//删除流程实例
@Test
public void delProcessInstance(){
runtimeService.deleteProcessInstance("73f0fb9a-ce5b-11ea-bf67-dcfb4875e032","删着玩");
System.out.println("删除流程实例");
}
}
影响的表:
act_hi_actinst 已完成的活动信息
act_hi_identitylink 参与者信息
act_hi_procinst 流程实例
act_hi_taskinst 任务实例
act_ru_execution 执行表
act_ru_identitylink 参与者信息
act_ru_task 任务
@SpringBootTest
public class Part4_Task {
@Autowired
private TaskService taskService;
//任务查询
@Test
public void getTasks(){
List list = taskService.createTaskQuery().list();
for(Task tk : list){
System.out.println("Id:"+tk.getId());
System.out.println("Name:"+tk.getName());
System.out.println("Assignee:"+tk.getAssignee());
}
}
//查询我的代办任务
@Test
public void getTasksByAssignee(){
List list = taskService.createTaskQuery()
.taskAssignee("bajie")
.list();
for(Task tk : list){
System.out.println("Id:"+tk.getId());
System.out.println("Name:"+tk.getName());
System.out.println("Assignee:"+tk.getAssignee());
}
}
//办理任务
@Test
public void completeTask(){
taskService.complete("d07d6026-cef8-11ea-a5f7-dcfb4875e032");
System.out.println("完成任务");
}
//拾取任务
@Test
public void claimTask(){
Task task = taskService.createTaskQuery().taskId("1f2a8edf-cefa-11ea-84aa-dcfb4875e032").singleResult();
taskService.claim("1f2a8edf-cefa-11ea-84aa-dcfb4875e032","bajie");
}
//归还与交办任务
@Test
public void setTaskAssignee(){
Task task = taskService.createTaskQuery().taskId("1f2a8edf-cefa-11ea-84aa-dcfb4875e032").singleResult();
taskService.setAssignee("1f2a8edf-cefa-11ea-84aa-dcfb4875e032","null");//归还候选任务
taskService.setAssignee("1f2a8edf-cefa-11ea-84aa-dcfb4875e032","wukong");//交办
}
// 组任务拾取
@Test
public void setTaskAssignee(){
设置一些参数,流程定义的key,候选用户
String key = "myProcess_1";
String candidate_users="zhangsan";
Task task = taskService.createTaskQuery()
.processDefinitionKey(key )
.taskCandidateUser(candidate_users)//设置候选用户
.singleResult();
if(task!=null){
taskService.claim(task.getId(),candidate_users);//第一个参数任务ID,第二个参数为具体的候选用户名
System.out.println("任务拾取完毕!");
}
}
影响的表:
act_hi_actinst
act_hi_identitylink
act_hi_taskinst
act_ru_identitylink
act_ru_task
@SpringBootTest
public class Part5_HistoricTaskInstance {
@Autowired
private HistoryService historyService;
//根据用户名查询历史记录
@Test
public void HistoricTaskInstanceByUser(){
List list = historyService
.createHistoricTaskInstanceQuery()
.orderByHistoricTaskInstanceEndTime().asc()
.taskAssignee("bajie")
.list();
for(HistoricTaskInstance hi : list){
System.out.println("Id:"+ hi.getId());
System.out.println("ProcessInstanceId:"+ hi.getProcessInstanceId());
System.out.println("Name:"+ hi.getName());
}
}
//根据流程实例ID查询历史
@Test
public void HistoricTaskInstanceByPiID(){
List list = historyService
.createHistoricTaskInstanceQuery()
.orderByHistoricTaskInstanceEndTime().asc()
.processInstanceId("1f2314cb-cefa-11ea-84aa-dcfb4875e032")
.list();
for(HistoricTaskInstance hi : list){
System.out.println("Id:"+ hi.getId());
System.out.println("ProcessInstanceId:"+ hi.getProcessInstanceId());
System.out.println("Name:"+ hi.getName());
}
}
}
流程变量就是 Activiti 在管理工作流时根据管理需要而设置的变量。
流程变量的作用域可以是一个流程实例(processInstance),或一个任务(task),或一个执行实例(execution),默认是流程实例。
在设置流程变量时,可以在启动流程时设置,也可以在任务办理时设置。
public class Part6_UEL {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
//启动流程实例带参数,执行执行人
@Test
public void initProcessInstanceWithArgs() {
//流程变量
Map variables = new HashMap();
variables.put("ZhiXingRen", "wukong");
//variables.put("ZhiXingRen2", "aaa");
//variables.put("ZhiXingRen3", "wukbbbong");
ProcessInstance processInstance = runtimeService
.startProcessInstanceByKey(
"myProcess_UEL_V1"
, "bKey002"
, variables);
System.out.println("流程实例ID:" + processInstance.getProcessDefinitionId());
}
//完成任务带参数,指定流程变量测试
@Test
public void completeTaskWithArgs() {
Map variables = new HashMap();
variables.put("pay", "101");
taskService.complete("a616ea19-d3a7-11ea-9e14-dcfb4875e032",variables);
System.out.println("完成任务");
}
//启动流程实例带参数,使用实体类
@Test
public void initProcessInstanceWithClassArgs() {
UEL_POJO uel_pojo = new UEL_POJO();
uel_pojo.setZhixingren("bajie");
//流程变量
Map variables = new HashMap();
variables.put("uelpojo", uel_pojo);
ProcessInstance processInstance = runtimeService
.startProcessInstanceByKey(
"myProcess_uelv3"
, "bKey002"
, variables);
System.out.println("流程实例ID:" + processInstance.getProcessDefinitionId());
}
//任务完成环节带参数,指定多个候选人
@Test
public void initProcessInstanceWithCandiDateArgs() {
Map variables = new HashMap();
variables.put("houxuanren", "wukong,tangseng");
taskService.complete("4f6c9e23-d3ae-11ea-82ba-dcfb4875e032",variables);
System.out.println("完成任务");
}
//直接指定流程变量
@Test
public void otherArgs() {
runtimeService.setVariable("4f6c9e23-d3ae-11ea-82ba-dcfb4875e032","pay","101");
// runtimeService.setVariables();
// taskService.setVariable();
// taskService.setVariables();
}
//局部变量
@Test
public void otherLocalArgs() {
runtimeService.setVariableLocal("4f6c9e23-d3ae-11ea-82ba-dcfb4875e032","pay","101");
// runtimeService.setVariablesLocal();
// taskService.setVariableLocal();
// taskService.setVariablesLocal();
}
}
用户任务
用户任务用来设置必须由人员完成的工作。 当流程执行到用户任务,会创建一个新任务, 并把这个新任务加入到分配人或群组的任务列表中。
Java服务任务
用来调用外部java类。
手工任务
用来表示工作需要某人完成,而引擎不需要知道,手工任务是直接通过的活动, 流程到达它之后会自动向下执行。
执行监听器
执行监听器可以执行外部Java代码或执行表达式,当流程定义中发生了某个事件。
可以捕获的事件有:流程实例的启动和结束、选中一条连线、节点的开始和结束、网关的开始和结束、中间事件的开始和结束、开始时间结束或结束事件开始。
任务监听器:
任务监听器可以在发生对应的任务相关事件时执行自定义Java逻辑或表达式。
任务监听器支持以下属性:
event(必选):任务监听器会被调用的任务类型。 可能的类型为:
class:必须调用的代理类。 这个类必须实现org.activiti.engine.delegate.TaskListener接口。
public class MyTaskCreateListener implements TaskListener {
public void notify(DelegateTask delegateTask) {
// Custom logic goes here
}
}
可以使用属性注入把流程变量或执行传递给代理类。 注意代理类的实例是在部署时创建的 (和activiti中其他类代理的情况一样),这意味着所有流程实例都会共享同一个实例。
一个包含其他节点,网关,事件等等的节点。 它自己就是一个流程,同时是更大流程的一部分。
事件子流程: 事件子流程可以添加到流程级别或任意子流程级别。用于触发事件子流程的事件是使用开始事件配置的。
调用活动(子流程): 这个流程定义需要被很多其他流程定义调用的时候。
排它网关: 内部是一个“X”图标,用来在流程中实现决策。 当流程执行到这个网关,所有分支都会判断条件是否为true,如果为true则执行该分支。
注意:排他网关只会选择一个为true的分支执行。如果有两个分支条件都为true,排他网关会选择id值较小的一条分支去执行。
不用排他网关也可以实现分支,如:在连线的condition条件上设置分支条件。
在连线设置condition条件的缺点:如果条件都不满足,流程就结束了(是异常结束)。
并行网关: 内部是一个“加号”图标。并行网关允许将流程分成多条分支,也可以把多条分支汇聚到一起,并行网关的功能是基于进入和外出顺序流的:
l 分支:
并行后的所有外出顺序流,为每个顺序流都创建一个并发分支。
l 汇聚:
所有到达并行网关,在此等待的进入分支, 直到所有进入顺序流的分支都到达以后, 流程就会通过汇聚网关。
注意:并行网关不会解析条件。 即使顺序流中定义了条件,也会被忽略。
总结:所有分支到达汇聚结点,并行网关执行完成。
包含网关: 内部包含一个圆圈图标,包含网关可以看做是排他网关和并行网关的结合体。
和排他网关一样,你可以在外出顺序流上定义条件,包含网关会解析它们。
包含网关的功能是基于进入和外出顺序流的:
l 分支:
包含网关可以在外出顺序流上定义条件,结果为true的顺序流会以并行方式继续执行, 为每个顺序流创建一个分支。
l 汇聚:
所有并行分支到达包含网关,会进入等待状态, 直到每个包含流程token的进入顺序流的分支都到达。换句话说,包含网关只会等待被选中执行了的进入顺序流。 在汇聚之后,流程会穿过包含网关继续执行。
总结:在分支时,需要判断条件,符合条件的分支,将会执行,符合条件的分支最终才进行汇聚。
基于事件网关: 网关的每个外出顺序流都要连接到一个中间捕获事件。 当流程到达一个基于事件网关,网关会进入等待状态:会暂停执行。 与此同时,会为每个外出顺序流创建相对的事件订阅。
@SpringBootTest
public class Part8_ProcessRuntime {
@Autowired
private ProcessRuntime processRuntime;
@Autowired
private SecurityUtil securityUtil;
//获取流程实例
@Test
public void getProcessInstance() {
securityUtil.logInAs("bajie");
Page processInstancePage = processRuntime
.processInstances(Pageable.of(0,100));
System.out.println("流程实例数量:"+processInstancePage.getTotalItems());
List list = processInstancePage.getContent();
for(ProcessInstance pi : list){
System.out.println("-----------------------");
System.out.println("getId:" + pi.getId());
System.out.println("getName:" + pi.getName());
System.out.println("getStartDate:" + pi.getStartDate());
System.out.println("getStatus:" + pi.getStatus());
System.out.println("getProcessDefinitionId:" + pi.getProcessDefinitionId());
System.out.println("getProcessDefinitionKey:" + pi.getProcessDefinitionKey());
}
}
//启动流程实例
@Test
public void startProcessInstance() {
securityUtil.logInAs("bajie");
ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder
.start()
.withProcessDefinitionKey("myProcess_ProcessRuntime")
.withName("第一个流程实例名称")
//.withVariable("","")
.withBusinessKey("自定义bKey")
.build()
);
}
//删除流程实例
@Test
public void delProcessInstance() {
securityUtil.logInAs("bajie");
ProcessInstance processInstance = processRuntime.delete(ProcessPayloadBuilder
.delete()
.withProcessInstanceId("6fcecbdb-d3e0-11ea-a6c9-dcfb4875e032")
.build()
);
}
//挂起流程实例
@Test
public void suspendProcessInstance() {
securityUtil.logInAs("bajie");
ProcessInstance processInstance = processRuntime.suspend(ProcessPayloadBuilder
.suspend()
.withProcessInstanceId("1f2314cb-cefa-11ea-84aa-dcfb4875e032")
.build()
);
}
//激活流程实例
@Test
public void resumeProcessInstance() {
securityUtil.logInAs("bajie");
ProcessInstance processInstance = processRuntime.resume(ProcessPayloadBuilder
.resume()
.withProcessInstanceId("1f2314cb-cefa-11ea-84aa-dcfb4875e032")
.build()
);
}
//流程实例参数
@Test
public void getVariables() {
securityUtil.logInAs("bajie");
List list = processRuntime.variables(ProcessPayloadBuilder
.variables()
.withProcessInstanceId("2b2d3990-d3ca-11ea-ae96-dcfb4875e032")
.build()
);
for(VariableInstance vi : list){
System.out.println("-------------------");
System.out.println("getName:" + vi.getName());
System.out.println("getValue:" + vi.getValue());
System.out.println("getTaskId:" + vi.getTaskId());
System.out.println("getProcessInstanceId:" + vi.getProcessInstanceId());
}
}
}
@SpringBootTest
public class Part9_TaskRuntime {
@Autowired
private SecurityUtil securityUtil;
@Autowired
private TaskRuntime taskRuntime;
//获取当前登录用户任务
@Test
public void getTasks() {
securityUtil.logInAs("wukong");
Page tasks = taskRuntime.tasks(Pageable.of(0,100));
List list=tasks.getContent();
for(Task tk : list){
System.out.println("-------------------");
System.out.println("getId:"+ tk.getId());
System.out.println("getName:"+ tk.getName());
System.out.println("getStatus:"+ tk.getStatus());
System.out.println("getCreatedDate:"+ tk.getCreatedDate());
if(tk.getAssignee() == null){
//候选人为当前登录用户,null的时候需要前端拾取
System.out.println("Assignee:待拾取任务");
}else{
System.out.println("Assignee:"+ tk.getAssignee());
}
}
}
//完成任务
@Test
public void completeTask() {
securityUtil.logInAs("wukong");
Task task = taskRuntime.task("db9c5f80-d3ae-11ea-99e8-dcfb4875e032");
if(task.getAssignee() == null){
taskRuntime.claim(TaskPayloadBuilder.claim()
.withTaskId(task.getId())
.build());
}
taskRuntime.complete(TaskPayloadBuilder
.complete()
.withTaskId(task.getId())
.build());
System.out.println("任务执行完成");
}
}
任务监听器拿到的数据和任务相关
场景:项目启动后,流转到任意环节都需要给相应的执行人发送短信提醒。一般用来通知或动态修改任务的执行人/候选组使用。
public class TkListener1 implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
System.out.println("执行人:"+delegateTask.getAssignee());
//根据用户名查询用户电话并调用发送短信接口
delegateTask.setVariable("delegateAssignee",delegateTask.getAssignee());
}
}
public class TkListener2 implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
System.out.println("执行人2:"+delegateTask.getVariable("delegateAssignee"));
//根据执行人username获取组织机构代码,加工后得到领导是wukong
delegateTask.setAssignee("wukong");
//delegateTask.addCandidateGroup("wukong");
}
}
执行监听器常用于统计执行时长。
执行监听器拿到的数据和流程相关,
执行监听器作用:
存储读取变量
处理业务信息
public class PiListener implements ExecutionListener {
@Autowired
private Expression sendType;
@Override
public void notify(DelegateExecution execution) {
System.out.println(execution.getEventName());
System.out.println(execution.getProcessDefinitionId());
if("start".equals(execution.getEventName())){
//记录节点开始时间
}else if("end".equals(execution.getEventName())){
//记录节点结束时间
}
System.out.println("sendType:"+sendType.getValue(execution).toString());
}
}
常用场景:
指定日期开启流程实例
24小时任务未办理短信提醒
3天未审核则主管领导介入
定时事件类型:
Time date:日期,什么时间触发
Time duration:持续延时多长时间后触发
Time cycle:循环,循环规则(监控报警,循环推送)
持续例子:
P1DT1M - 一天一分钟执行一次
P1W - 一周执行一次
PT1H - 一小时执行一次
PT10S - 十秒执行一次
说明:
P - 开始标记
1Y - 一年
2M -两个月
10D - 十天
T - 时间和日期分的割标记
2H - 两个小时
30M - 三十分钟
15S - 十五秒钟
循环例子:
循环3次/开始循环时间/每次间隔:R3/2021-07-30T19:12:00/PT1M(13【开始时间+间隔】分后开始)
执行2次,1分钟执行一次:R2/PT1M
无限循环/时间间隔/结束时间:R/RT1M/2021-01-01
变量无限循环:R/PT1H/${EndTime}
事件中间事件:
第三个任务:3天未审核,任务不会向下执行,自动执行到他主管领导这,可以使用边界事件。
第二个任务:非中断边界事件,与边界事件的区别是本身任务不会流转,八戒继续办理,他的主管领导也会接到一个任务,还会保存在当前的任务,典型场景24小时未处理,短信提醒,任务还在八戒那里,下面的是如果三天未审核,任务由主管来办理,八戒的任务被取消掉了,而非中断是八戒和主管领导都有任务。
第一种:
定时任务创建后会在act_ru_timer_job表创建一条数据,运行后删除
第三种(中间事件):
十秒后只有八戒2的任务
第二种:中间事件非中断
十秒后有八戒1和八戒2的任务
参考文献:
Activiti7精讲&Java通用型工作流开发实战
工作流引擎 Activiti 万字详细进阶
activiti7笔记
史上最全的工作流引擎 Activiti 学习教程(值得收藏)
activiti BPMN—顺序流、网关、任务、子流程-多极客编程