Camunda
Camunda是一个工作流引擎,执行Bpmn2.0标准,因此依赖于基于bpmn的流程图(本质上是一个xml文件)
基本概念
ProcessDefinition
简单的认为就是画的流程图
ProcessInstance
流程图实例,就是根据某个流程图定义发起了一个新的流程,那么这个新的流程就是一个流程实例
StartEvent、EndEvent
流程的开始和结束节点
Task
任务,当流程流转到某个阶段,需要用户审核或者其他操作的时候,这个需要用户来完成的操作就是一个任务,除了用户任务之外,还有系统任务等其他任务
任务执行人
如果需要让某个用户执行某个任务,首先需要将任务分配给用户,一般有3种分配方式
- 直接指定,这里通过Assignee来直接指定某一个具体的用户(一般是用户ID或者唯一的用户名),支持表达式以支持动态指定
- 指定候选人,通过candidateUser来指定一系列候选人,如果是多个用户,通过
,
号分隔 - 指定候选组,通过candidateGroup来指定某一个组里面的所有用户(实际测试中,发现候选人和候选组是并集关系)
如果指定了候选人和候选组,那么并不意味着所有的候选人都需要执行任务,这些人首先需要进行一个认领的操作,一个任务只能由一个人认领,认领完成后才能执行任务,相对的,也可以取消认领
SequenceFlow
简而言之就是流程图的箭头,配合Gateway
使用的时候,还可以在箭头上指定表达式,用于控制流向
流程变量
SequenceFlow
的表达式依赖于流程变量,流程变量通过 流程启动、完成任务来传递
Gateway
网关,条件判断流程图流向,一般有Inclusive Gateway
、Exclusive Gateway
和Parallel Gateway
Exclusive Gateway
排他网关
排他网关可以有多个流出,但最终只能选择一条路径
Parallel Gateway
并行网关
并行网关可以有多个流出,并且每个流出都会被执行(忽略表达式),当每个流出都执行完毕之后,才进行下一步流转,并行网关的流入和流出的标识是相同的,例如
Inclusive Gateway
包含网关
包含网关等于并行网关和排他网关的组合,相对于并行网关,每个流出都会计算表达式,当每个流出都执行完毕之后,才会执行下一步流转,包含网关的流入和流出的标识是相同,例如:
流程图绘制工具
基本流程图
camunda流程图
https://github.com/bpmn-io/bpmn-js-examples/tree/master/properties-panel
和Spring Boot集成
参考文档:https://docs.camunda.org/get-started/spring-boot/project-setup/
安装完毕后把绘制好的bpmn文件丢在resources
目录下,应用启动后会自动识别,camunda还支持在application.properties
对工作流进行配置,配置项如下:
https://docs.camunda.org/manual/latest/user-guide/spring-boot-integration/configuration/#camunda-engine-properties
和原系统权限的集成
@Bean
public FilterRegistrationBean<Filter> filter(ProcessEngine processEngine) {
FilterRegistrationBean<Filter> frBean = new FilterRegistrationBean<>();
frBean.setFilter((request, response, chain) -> {
User user;
if ((tbUser = getUserFromSpringSecurityOrElse()) != null) {
List<String> groups = getUserGroupOrRolesFromUser(user);
processEngine.getIdentityService().setAuthentication(String.valueOf(user.getId()), groups);
try {
chain.doFilter(request, response);
} finally {
processEngine.getIdentityService().clearAuthentication();
}
} else {
chain.doFilter(request, response);
}
});
frBean.addUrlPatterns("/*");
frBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD);
return frBean;
}
对表单的支持
一般发起一个流程或者用户进行审核时,依赖于一个表单,camunda和表单的关联,有3种形式
内置表单
内置表单缺点过于明显,实际情况基本不会使用
优点
- 方便
缺点
- 表单缺乏更深层次的校验
- 变更繁琐,维护困难,往往一个地方变更,其他地方也要变更
- 无法自定义样式
外联表单
外联表单通过formKey
属性来关联,被关联的表单会储存于camunda
自己的数据库表中,但实际操作中,我发现这个比较麻烦,所以可以用过自定义FormEngine
来储存表单
优点
- 维护容易
- 自定义方便,可以通过拖拽等方式生成表单
缺点
- 当设计到和业务相关的表单控件(比如用户选择)需要一定量的开发工作,并且进行数据解析
业务表单
业务表单 通过 businessKey
(基本就是业务表的ID)来关联,这样的话实际上表单和camunda
是分开的,因此不存在通过camunda
渲染表单的情况
优点
- 和业务无缝集成
- 无需针对表单开发
表单的渲染
StartEvent上的表单渲染
@Autowired
ProcessEngine engine ;
engine.getFormService().getRenderedStartForm(processDefinitionId, "testFormEngine")//返回一个String对象,一般情况下是一段html片段
Task上的表单渲染
@Autowired
ProcessEngine engine ;
engine.getFormService().getRenderedTaskForm(taskId, "testFormEngine")
基本使用
接口
ProcessEngine 核心接口,提供了工作流所有的操作的api
- RuntimeService 用于开启流程实例、删除流程实例、以及搜索流程实例等操作
- TaskService 用于用户任务的认领、完成、分发等操作
- IdentityService 用于提供身份认证以及管理用户和用户组
- HistoryService 用于查询历史流程实例、历史任务以及历史流程变量等
- FormService 用户内外联表单的渲染、通过提交表单开启流程实例、通过提交表单完成任务等
部署流程
新增一个流程定义和更新一个流程定义是同样的方法(更新的时候请保持processDefinitionKey不变)
@Autowired
ProcessEngine engine ;
engine.getRepositoryService().createDeployment()
.addString(resource.getName() + ".bpmn", resource.getXml())
.deploy();
但是更新一个流程并非我们认为的更新,而是一个重新部署,事实上ProcessDefinition
的id已经变了,因此如果我们通过如下方法查询流程定义:
processEngine.getRepositoryService().createProcessDefinitionQuery().active().list()
会查出来两个同样的流程定义,但事实上另一个重复的是我们更新的流程定义,如果只想显示最新的一个,查询语句上 加上 latestVersion() 这个即可,顺便说下通过定义id和定义key来启动流程的区别:
如果通过processDefinitionId来启动一个流程,那么代表启动流程定义的某一个版本,如果通过processDefinitionKey来启动流程,那么将会始终启动流程定义的最新版本
解析Bpmn模型
有时候我们需要在部署直接做一些校验,那么可以利用下面这个静态方法:
BpmnModelInstance modelInstance = Bpmn.readModelFromStream(is)
通过BpmnModelInstance
我们可以做一些简单或者深入的判断,比如判断流程有没有结束事件:
modelInstance.getModelElementsByType(EndEvent.class).isEmpty()
判断启动任务有没有设置表单
Collection<StartEvent> startEvents = modelInstance.getModelElementsByType(StartEvent.class);
for (StartEvent startEvent : startEvents) {
Collection<SequenceFlow> flows = startEvent.getIncoming();
if (flows.isEmpty()) {
if (StringUtils.isEmpty(startEvent.getCamundaFormKey())) {
//提醒用户设置启动表单
}
}
}
启动流程
启动流程一般有两种方式
通过表单启动
如果StartEvent
使用了内置或者外联表单,那么应该通过表单来启动一个流程,例如:
@Autowired
ProcessEngine engine ;
Map<String,Object> map;//这里的map是表单的键值对,在上述的表单中,这里应该是map.put("age","inputValue")
engine.getFormService().submitStartForm(processDefinitionId, map);//这里的map就是流程变量,后面可以通过${age}来获取传递的年龄
通过流程定义启动
如果跟业务表单相关联,那么应该通过流程定义来启动,例如:
@Autowired
ProcessEngine engine;
engine.getRuntimeService().startProcessInstanceByKey(processDefinitionKey, businessKey)//这里的business是业务表单的ID
流程变量
实际的使用过程中,流程变量使用很频繁,例如流程该怎么走,外联表单的数据传递等,都需要使用到流程变量,流程变量可以在启动流程的时候传入,也可以在完成任务的时候传入,除非特别指定变量为本地变量,否则传递的变量都是整个流程实例程共享,在上面的流程启动中,传递了一个map对象,那么这个map里面的值就会被视作为流程变量,下面流程中,如果填写的年龄小于16岁,便需要填写xxx
完成一个任务
如果使用了业务表单,那么应该通过 TaskService
来完成
TaskService ts = engine.getTaskService();
Task task = engine.getTaskService().createTaskQuery().taskId(taskId).singleResult();
ts.createComment(taskId, task.getProcessInstanceId(), comment);
ts.complete(vo.getTaskId(), Map.of(task.getId(), status));
如果使用了外联表单,那么应该通过 FormService
来完成
engine.getFormService().submitTaskForm(taskId, map);
查询需要认领的任务
List<String> groups = this.userGroupManager.getUserGroups(userId);
List<Task> tasks = engine.getTaskService().createTaskQuery().processDefinitionKey(definitionKey)
.taskCandidateGroupIn(groups).list();
tasks.addAll(engine.getTaskService().createTaskQuery()
.taskCandidateUser(userId).list());
认领任务
ProcessEngine engine;
engine.getTaskService().claim(taskId, userId);
取消认领任务
engine.getTaskService().claim(taskId, null);
判断用户是否认领过任务
engine.getHistoryService().createUserOperationLogQuery().taskId(taskId).userId(userId).operationType("Claim").list().isEmpty()
判断最后一个认领该任务的是否是当前用户
String userId = getCurrentUserIdFromContext();
Lis<UserOperationLogEntry> entries = engine.getHistoryService().createUserOperationLogQuery().taskId(taskId).operationType("Claim").orderByTimestamp().desc().listPage(0, 1);
return !entries.isEmpty() && userId.equals(entries.get(0).getUserId());
删除任务
engine.getTaskService().deleteTask(taskId, deleteReason);
当前需要处理的任务
List<HistoricTaskInstance> instances = engine.getHistoryService().createHistoricTaskInstanceQuery()
.taskAssignee('用户ID').unfinished().orderByHistoricActivityInstanceStartTime().asc().list();
查询处理过的历史任务
List<HistoricTaskInstance> instances = engine.getHistoryService().createHistoricTaskInstanceQuery()
.taskAssignee('用户ID').finished().orderByHistoricActivityInstanceStartTime().desc().list();
查询某个流程下任务处理记录
HistoryService hs = engine.getHistoryService();
List<HistoricTaskInstance> list = hs.createHistoricTaskInstanceQuery().processInstanceId(`流程实例ID`)
.orderByHistoricActivityInstanceStartTime().asc().list();
for (HistoricTaskInstance ins : list) {
if (ins.getAssignee() == null)//任务还没有被认领
continue;
System.out.println(ins.getStartTime());//任务开始时间
System.out.println(ins.getEndTime());//用户审核完毕时间
System.out.println(ins.getAssignee());//审核人
System.out.println(ins.getName());//任务名称
HistoricVariableInstance variable = engine.getHistoryService().createHistoricVariableInstanceQuery()
.variableName(ins.getId()).singleResult();
if (variable != null) {
System.out.println(variable.getValue().toString());//查询审核人对任务的审核结果
}
List<String> comments = engine.getTaskService().getTaskComments(ins.getId()).stream().map(Comment::getFullMessage)
.collect(Collectors.toList());
System.out.println(comments); //查询审核人对任务处理的评论
}