本Demo采用springBoot和camunda内嵌的方式。 在SpringBoot中加入camunda的依赖后在springBoot启动类上加上@EnableProcessApplication注解即可完成配置, 配置完毕后可以注入camunda引擎的RunTimeService、TaskService等工具类操作camunda工作流引擎。
demo源码: https://github.com/Himly1/camunda-demo
Note: 如果需要调用demo中的api可以在启动demo后访问localhost:8080/swagger-ui.html来方便快捷的完成调用.
如下图所示, 应用和引擎作为一个整体使用同一个db。
若后期存在service和web分离, 或者采用集群也很容易地完成拓展部署.
假设web和service分离, web和service均需要与camunda引擎交互。
那么最少会存在2个camonda引擎, 每个引擎都与同一个db做交互,这样一来只需要保证所有引擎访问的db一致即可。
假设后期流量非常大,配置了2个web集群, 那么会存在2个camunda引擎, 而这两个引擎都与同一个db做交互,目前来看不会存在问题.
demo流程图定义如下(对应源码中resource/project.dpmn文件)
学生报名:
startEvent名称为学生报名, id默认生成
其中有个表单,表单中有一个参数recordId。 recordId指向的是学生报名成功后存储在db报名信息表中的一条数据主键.
学生通过restApi发起项目报名申请, 处理完业务逻辑后如用户报名成功需要创建一个当前流程定义的实例。 如下代码所示
@ApiOperation(value = "项目报名")
@PostMapping(value = "/{projectId}/users{userId}")
public boolean ParticipatingProject(@PathVariable Long projectId, @PathVariable Long userId) {
//ignore argument verify
//save the record to db
Long savedRecordId = 3L;
//start a new instance of the process
Map variables = new HashMap();
variables.put(ProjectProcessConstant.VAR_NAME_SCHOOL, "上海交通大学");
variables.put(ProjectProcessConstant.VAR_NAME_STUDENT, String.valueOf(userId));
variables.put(ProjectProcessConstant.FORM_RECORD_ID, savedRecordId);
ProcessInstance instance = runtimeService.
startProcessInstanceByKey(ProjectProcessConstant.PROCESS_ID, variables);
if (instance == null) {
return false;
}else {
return true;
}
}
runtimeService是注入进来的, 通过startProcessInstaceByKey方法来完成开启一个流程定义的实例。 方法的第二个参数是流程定义中的变量值。 比如VAR_NAME_SCHOOL表明的是将“上海交通大学”赋值给流程定义中的school变量(如下图)
流程开启后我们可以通过访问项目首页,在本demo中就是localhost:8080输入camunda的用户名密码(本demo中账号密码均为demo)来查看具体情况, 如下图
进入实例中我们能够看到当前实例执行情况,如下图
UserTask是需要人员来执行的, 那么在此demo中就需要指定学校人员来先获取需要审批的项目报名申请, 其后就申请作出审批。
@ApiOperation(value = "获取需要审批的项目申请列表")
@GetMapping(value = "/project/approve/list")
public @ResponseBody List getAllProjectParticipateRequest(String schoolName, Integer reviewLevel) {
LOGGER.info("The school name is {}", schoolName);
//get the taskList
List tasks;
if (reviewLevel.equals(1)) {
tasks = taskService.createTaskQuery().
taskName(ProjectProcessConstant.TASK_NAME_FIRST_LEVEL_REVIEW).
taskCandidateGroup(schoolName).
list();
}else {
tasks = taskService.createTaskQuery().
taskName(ProjectProcessConstant.TASK_NAME_SECOND_LEVEL_REVIEW).
taskCandidateGroup(schoolName).
list();
}
以上代码中通过taskService来查询task. 首先通过taskName来查, 我们需要查询的是一级审核任务, 在流程定义中一级审核的name就是”一级审核”, 其次我们还需要通过学校名称作为UserTask的group值来过滤只获取当前学校需要审批的学生报名申请.
在流程定义中一级审核任务的group是一个变量${school}(请翻上面的图), 在学生报名成功后开启一个实例时,我们将“上海交通大学”赋值给了此变量。 那么如果上图代码中请求参数schoolName不是“上海交通大学”则无法查询到任何task, 因为目前只有上海交通大学才有一名学生提交了报名申请并开启了一个流程定义实例。
因为一个流程定义会有n个实例, 那么自然会有n条一级审批的user task. 为了审批时能够知道当前审批的是哪个实例的一级审批user task就需要在响应参数中返回taskId,具体作用下面再细说
如下图, 输入上海交通大学我们看到有一条待审批的报名申请
输入其他名称时, 没有任何待审批的项目申请
当作出审批时将审批结果和审批taskId作为参数发送到后端, 后端通过taskId获取对应实例的一级审批user task并提交给camunda引擎, 引擎根据审批结果及gateWay来决定走向。
@ApiOperation(value = "审批项目申请")
@PutMapping(value = "/project/participateRequests/{taskId}")
public boolean approveProjectParticipateRequest(@PathVariable String taskId, boolean needExtraInfo, boolean passed, String schoolName) {
Task task = taskService.createTaskQuery().
taskCandidateGroup(schoolName).taskId(taskId).singleResult();
if (task == null) {
LOGGER.error("The task not found, task id is {}", taskId);
return false;
}else {
//business logic here
//Into next step
LOGGER.info("The taskId is {}", taskId);
Map variables = new HashMap<>();
variables.put(ProjectProcessConstant.FORM_EXTRA_INFO_1, needExtraInfo);
variables.put(ProjectProcessConstant.FORM_APPROVED_1, passed);
taskService.complete(task.getId(), variables);
return true;
}
}
上述代码中首先通过获取当前用户就职的学校(相当于鉴权), 通过task的group(${school}变量)和taskId来获取指定的task.
之所以不直接使用taskId来调用taskService.complete()方法将结果提交给camunda引擎是因为当前用户只能对他们学校的报名申请作出审批, 若传递的taskId指向的task的group属性并非“上海交通大学”会存在严重的问题。
其中FORM_EXTRA_INFO_1和FORM_APPROVED_1常量对应的一级审核task的表单, 如下图
假设我们将extra_info_1设置为true, approved_info 任意, 我们可以在后台看到当前流程定义实例的执行情况
网关"是否需要额外材料"是根据extra_info_1字段来决定流程走向的, 上个步骤中我们将extra_info_1设置为了true, 那么上图中可以看到当前实例的流程走到了"上传额外材料"的user task.
接下来需要有个api来获取等待指定用户上传额外材料的记录列表,如下代码
@ApiOperation(value = "获取学生需要上传额外材料的记录")
@GetMapping(value = "/users/{userId}/extraInfo/list")
public List getUploadExtraTask(Long userId) {
List uploadExtraInfoTask =
taskService.createTaskQuery().
taskAssignee(String.valueOf(userId)).
taskName(ProjectProcessConstant.TASK_NAME_UPLOAD_EXTRA_INFO).
list();
List records = new ArrayList<>(uploadExtraInfoTask.size());
uploadExtraInfoTask.forEach( task -> {
UploadExtraInfoRecord record = new UploadExtraInfoRecord();
record.setTaskId(task.getId());
//the upload url of extra info is up to the variable
record.setTheUploadUrlOfExtraInfo("www.google.com");
records.add(record);
});
return records;
}
首先同样在上传额外材料的user task中有个所属用户的属性, 属性使用的是变量如下图
在开启实例时我们将userId变量赋值给了${student}变量, 如下图
@ApiOperation(value = "项目报名")
@PostMapping(value = "/{projectId}/users{userId}")
public boolean ParticipatingProject(@PathVariable Long projectId, @PathVariable Long userId) {
//ignore argument verify
//save the record to db
Long savedRecordId = 3L;
//start a new instance of the process
Map variables = new HashMap();
variables.put(ProjectProcessConstant.VAR_NAME_SCHOOL, "上海交通大学");
variables.put(ProjectProcessConstant.VAR_NAME_STUDENT, String.valueOf(userId));
variables.put(ProjectProcessConstant.FORM_RECORD_ID, savedRecordId);
ProcessInstance instance = runtimeService.
startProcessInstanceByKey(ProjectProcessConstant.PROCESS_ID, variables);
if (instance == null) {
return false;
}else {
return true;
}
}
那么获取用户需要上传额外材料的记录可以通过任务所属用户+任务名来获取。
如下代码
List uploadExtraInfoTask =
taskService.createTaskQuery().
taskAssignee(String.valueOf(userId)).
taskName(ProjectProcessConstant.TASK_NAME_UPLOAD_EXTRA_INFO).
list();
调用获取指定用户待上传额外材料列表的api来查看如下所示, 可以看到有一条记录
同样的, 也需要一个用户上传额外材料的api, 如下图
@ApiOperation(value = "上传指定项目所需的额外资料")
@PostMapping(value = "/{projectId}/users/{userId}/extraInfo")
public boolean uploadExtraInfo(@PathVariable Long projectId, @PathVariable Long userId, String extraInfo, String taskId) {
//must verify the task of the taskId pointing is belong the current user.
Task task = taskService.createTaskQuery().
taskAssignee(String.valueOf(userId)).
taskName(ProjectProcessConstant.TASK_NAME_UPLOAD_EXTRA_INFO).
taskId(taskId).
singleResult();
if (task == null) {
LOGGER.error("The task not found.");
LOGGER.error("the assignee is {}, taskName is {}, taskId is {}.", userId, ProjectProcessConstant.TASK_NAME_UPLOAD_EXTRA_INFO, taskId);
return false;
}else {
//upload extra info to db.
//business logic here
//into next step
taskService.complete(task.getId());
return true;
}
}
同样通过assIgnee及taskName及taskId来拿到task对象, 并提交该task使得camunda引擎继续工作。
上传额外材料完毕后的流程如下:
又返回到了一级审核, 这次一级审核中我们将extra_info_1设置为false, approved_1设置为true, 如下图
可以看到流程已经走到了二级审核, 二级审核的group是教务处,并没有使用变量。
我们通过api来查询下教务处需要审批的报名申请, 如下图
二级审批表单如下
同样有两个参数, 意义与一级审核中的一致。 在一级审核中提交的表单数据只会被保留, 若在二级审核中没有更改则会继续向下传递, 若更改了数据则传递更改后的数据。
这次我们将extra_info_1设置为false, approved_1设置为true来看下流程的走向.
此时查看后台发现没有实例在运行, 这表明之前存在的流程定义实例已经执行完毕。
我们看到二级审批通过后需要执行一个serviceTask
下面看看serviceTask. 其中service的实现方式是Delegate expression, 值为${smsServiceTask}, 这表明的是由spring的smsServiceTask这个 Bean来执行当前的service
smsServiceTask如下
package org.camunda.bpm.getstarted.loanapproval.camunda.tasks.service;
import org.camunda.bpm.engine.TaskService;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class SmsServiceTask implements JavaDelegate {
private static final Logger log = LoggerFactory.getLogger(SmsServiceTask.class);
private final TaskService taskService;
public SmsServiceTask(TaskService taskService) {
this.taskService = taskService;
}
public void execute(DelegateExecution delegateExecution) throws Exception {
Map variables = delegateExecution.getVariables();
log.info("variables is {}", variables);
String studentId = (String)variables.get("student");
log.info("success send sms message to the student {}", studentId);
}
}
需要实现JavaDelegate类并重写excute方法, 具体请查阅此官放doc https://docs.camunda.org/manual/7.7/user-guide/process-engine/delegation-code/
当之前流程实例实例的二级审批执行完毕且表单中值extra_info_1 = false, approved_1 = true时控制台打印如下
这表明执行到短信通知学生这个serviceTask时对应的实现被自动调用了, 调用成功后继续执行, 最后执行到了endEvent, 此时结束该实例表明流程执行完毕.
1.应用部署后,怎么在不重启应用的情况下更新已经部署的流程定义?
简单来说,可以通过restApi来同正在工作中的引擎交互, 具体的api可到官网查询。
比如当前demo中流程定义的key为project, 那么在同样key的情况下我可以发布一个新的流程定义, camunda引擎会根据key的不同自动新增或更新流程定义。 当Java应用通过key启动一个更新后的流程定义实例时,创建的是更新后的流程定义实例。
具体文档如下:https://docs.camunda.org/manual/7.5/user-guide/process-engine/process-versioning/