camunda入门

Camunda

Camunda是一个工作流引擎,执行Bpmn2.0标准,因此依赖于基于bpmn的流程图(本质上是一个xml文件)

基本概念

1588901425191.png

ProcessDefinition

简单的认为就是画的流程图

ProcessInstance

流程图实例,就是根据某个流程图定义发起了一个新的流程,那么这个新的流程就是一个流程实例

StartEvent、EndEvent

流程的开始和结束节点

Task

任务,当流程流转到某个阶段,需要用户审核或者其他操作的时候,这个需要用户来完成的操作就是一个任务,除了用户任务之外,还有系统任务等其他任务

任务执行人

如果需要让某个用户执行某个任务,首先需要将任务分配给用户,一般有3种分配方式

  1. 直接指定,这里通过Assignee来直接指定某一个具体的用户(一般是用户ID或者唯一的用户名),支持表达式以支持动态指定
  2. 指定候选人,通过candidateUser来指定一系列候选人,如果是多个用户,通过,号分隔
  3. 指定候选组,通过candidateGroup来指定某一个组里面的所有用户(实际测试中,发现候选人和候选组是并集关系)

如果指定了候选人和候选组,那么并不意味着所有的候选人都需要执行任务,这些人首先需要进行一个认领的操作,一个任务只能由一个人认领,认领完成后才能执行任务,相对的,也可以取消认领

SequenceFlow

简而言之就是流程图的箭头,配合Gateway使用的时候,还可以在箭头上指定表达式,用于控制流向

流程变量

SequenceFlow的表达式依赖于流程变量,流程变量通过 流程启动、完成任务来传递

Gateway

网关,条件判断流程图流向,一般有Inclusive GatewayExclusive GatewayParallel Gateway

Exclusive Gateway

排他网关

1588899717890.png

排他网关可以有多个流出,但最终只能选择一条路径

Parallel Gateway

并行网关

1588899778691.png

并行网关可以有多个流出,并且每个流出都会被执行(忽略表达式),当每个流出都执行完毕之后,才进行下一步流转,并行网关的流入和流出的标识是相同的,例如

1588900246498.png

Inclusive Gateway

包含网关

1588899683807.png

包含网关等于并行网关和排他网关的组合,相对于并行网关,每个流出都会计算表达式,当每个流出都执行完毕之后,才会执行下一步流转,包含网关的流入和流出的标识是相同,例如:

1588900860271.png

流程图绘制工具

基本流程图

https://demo.bpmn.io/

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种形式

1588903181804.png

内置表单

内置表单缺点过于明显,实际情况基本不会使用

优点

  1. 方便

缺点

  1. 表单缺乏更深层次的校验
  2. 变更繁琐,维护困难,往往一个地方变更,其他地方也要变更
  3. 无法自定义样式

外联表单

外联表单通过formKey属性来关联,被关联的表单会储存于camunda自己的数据库表中,但实际操作中,我发现这个比较麻烦,所以可以用过自定义FormEngine来储存表单

优点

  1. 维护容易
  2. 自定义方便,可以通过拖拽等方式生成表单

缺点

  1. 当设计到和业务相关的表单控件(比如用户选择)需要一定量的开发工作,并且进行数据解析

业务表单

业务表单 通过 businessKey(基本就是业务表的ID)来关联,这样的话实际上表单和camunda是分开的,因此不存在通过camunda渲染表单的情况

优点

  1. 和业务无缝集成
  2. 无需针对表单开发

表单的渲染

StartEvent上的表单渲染

@Autowired
ProcessEngine engine ;

engine.getFormService().getRenderedStartForm(processDefinitionId, "testFormEngine")//返回一个String对象,一般情况下是一段html片段

Task上的表单渲染

@Autowired
ProcessEngine engine ;

engine.getFormService().getRenderedTaskForm(taskId, "testFormEngine")

基本使用

接口

ProcessEngine 核心接口,提供了工作流所有的操作的api

  1. RuntimeService 用于开启流程实例、删除流程实例、以及搜索流程实例等操作
  2. TaskService 用于用户任务的认领、完成、分发等操作
  3. IdentityService 用于提供身份认证以及管理用户和用户组
  4. HistoryService 用于查询历史流程实例、历史任务以及历史流程变量等
  5. 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

1588906568148.png

完成一个任务

如果使用了业务表单,那么应该通过 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); //查询审核人对任务处理的评论
}

demo

https://github.com/mhlx/camunda_demo

Jackson LocalDateTime的序列化
从0开始搭建一个ss服务