Camunda工作流引擎二

本篇将在上一篇应用的基础上开发一个最基本的 ”流程Demo“ !

提要

简单总结一下《Camunda工作流引擎一》中关于表的知识,大体上分为 4 类:

  1. act_RE_* :RE 表示 repository,这个前缀的表包含了流程定义和流程静态资源(图片,规则,等等)。
  2. act_RU_*:RU 表示 runtime,这些运行时的表,包含流程实例、任务、变量、异步任务等运行中的数据。只在流程实例执行过程中保存这些数据,在流程结束时就会删除这些记录,这样来尽量使运行时表可以一直很小速度很快。
  3. act_HI_* :HI 表示 history,这些表包含历史数据,比如历史流程实例、遍历、任务等等。
  4. act_GE_* :GE 表示 general,通用数据,用于不同场景下。

这些表存储了流程相关的各种信息,那么我们如何去对其进行CRUD呢?按照传统的思路,那自然是写 domain、dao、service 去进行对上述表的操作。

但是,实际上 Camunda 已经将上述表的操作给我们封装好了:
Camunda工作流引擎二_第1张图片

可以通过流程引擎去获取到各种 Service,从而进行一些流程操作:RepositoryService,资源管理类;RuntimeService,流程运行管理类;TaskService,任务管理类;HistoryService,历史管理类;ManagerService,引擎管理类等等。。。(在 Spring 项目中更多的是将这些 Service 也交由 Spring 容器进行管理控制)

本篇将按照下述的开发第一个 Camunda 流程Demo 的基本步骤来进行:

  1. 整合 Camunda(添加依赖)。

  2. 完善必要的配置。

  3. 实现业务流程建模(绘制 BPMN )。

    工具下载地址: https://camunda.com/download/modeler/

  4. 部署流程定义。

  5. 启动流程实例。

  6. 查询代办任务。

  7. 处理代办任务。

  8. 结束流程。

1、2两个步骤,在上一篇中已经完成了,这两部也是启动应用、创建相关表的先行条件。以公司请假审批流程为例,直接从第 3 步开始!由简到繁!

绘制业务流程

先看一下绘制好的样子:
Camunda工作流引擎二_第2张图片

首先创建一个开始事件U5SZVA.png和一个结束事件在这里插入图片描述
,可以修改两个事件的 id 和 name,或者不作任何修改。

接下来就是创建 Task,然后点击右侧的"小扳手"设置为用户任务,可以根据需要修改用户任务的 Id、Name 和 Version Tag,或者不做修改使用默认,重点是 assignee 这个就是任务的处理人,可以有三种方式进行设置:

  1. 直接字符串写死

  2. UEL-value,是 Java EE6 规范的一部分,写法:${变量名} 或者 ${对象.属性名},本篇也将采用这种形式进行设置,以"员工输入"任务(input)为例:
    Camunda工作流引擎二_第3张图片

    如何传这个变量呢?后面运行流程实例的时候会再次提到!

  3. UEL-Method,写法:${bean.getId()}、${xxxService.findIdByName(empName)},bean 是 Spring 容器中的一个 bean,调用这个 bean 的 getId() 方法,xxxService 也是 Spring 容器中的一个 bean,其中 empName 是流程变量,empName 作为参数传到 findIdByName() 方法中。

用同样的方法创建另外两个用户任务:approve1(部门审批) 和 approve2(人事审批)。

最后使用最后将事件和任务按照顺序连接起来(顺序流)。

将文件另存为 holiday.bpmn。

PS:bpmn 文件本质上是 xml 配置文件,只是我们的工具将其解析为了这样的图形样式,去方便使用者去进行业务流程模型的创建。

部署流程定义

定义好了 bpmn 文件,就可以将其进行部署了!首先在《Camunda工作流引擎一》中那个 SpringBoot 项目的资源路径下建一个 BPMN 文件夹,将我们的 bpmn 文件 copy 进去:

UbJpl9.png

然后就是使用我们开头说的各种工作流已经帮我们定义好的 Service 去进行流程部署操作:

@Autowired
RepositoryService repositoryService;

/**
 * 流程定义部署
 */
@Test
void deploy() {
    Deployment deploy = repositoryService.createDeployment()
        .addClasspathResource("BPMN/holiday.bpmn")
        .deploy();
    //部署Id
    System.out.println(deploy.getName());//84c0a64c-cc1c-11ea-85a7-98fa9b4e8fcb
}

除了这种最简单的部署方式之外,还可以同时部署 .bpmn 文件对应的 .png 图片、压缩包的方式进行部署等等。。。可以根据业务的需求选择合适的方式。

部署完成后可以去观察数据库表的变化:act_re_deployment 中会新增本次的部署信息、act_re_procdef流程定义的一些信息、act_ge_bytearray 部署的资源文件。。

PS:流程定义和流程实例,很像是 Java 中类定义和 对象(实例) 的关系。

启动流程实例

有了流程定义,我们就可以去启动对应的流程实例!同时会演示“绘制业务流程”时留下的问题:如何给 assignee 中的 UEL-value 表达式进行传参!

/**
 * 开启一条流程,并给用户任务的 assignee 赋值
 */
@Test
public void startProcessInstanceWithAssignee(){
    Map<String,Object> map = new HashMap<>();
    map.put("employee","zhangsan");
    map.put("deptment","lisi");
    map.put("personal","wangwu");
    ProcessInstance holiday = runtimeService.startProcessInstanceByKey("holiday", map);
    //流程实例Id
    System.out.println(holiday.getProcessInstanceId());//c3156a0d-cc28-11ea-adb8-98fa9b4e8fcb
}

一个流程实例启动后,可以去观察 act_hi_actinst 表中记录了每个任务的相关信息(审批人、开始时间、结束时间等等)、act_hi_identitylink 表中记录了流程实例的参与者、act_hi_procinst 表中是流程实例的信息、act_hi_tastinst 记录了流程实例中每个任务实例的信息、act_ru_identitylink 表中记录了运行时的流程实例当前的参与者、act_ru_tast 表记录了当前运行中的任务、act_ru_variable 表中记录了设置的参数、等等。。。

流程变量

在业务流程推进的过程中,可能会涉及到很多流程变量。

Camunda 支持的变量类型不限于:String、int、short、long、double、date、binary、serializable(Java 对象需要去实现序列化接口,并生成 serialVersionUID) 等等。。

流程变量的作用域默认是一个流程实例(从流程启动到流程结束),一个流程实例是一个流程变量最大范围的作用域,所以被称为 global 变量;作用域也可以是一个任务(Task)或者一个执行实例(execution),所以被称为 local 变量。

PS:global 变量中变量名不允许重复,设置相同的变量名,后设置的值会覆盖先设置的值。local 变量和 global 变量可以变量名相同,没有影响。

设置流程变量的方法也有多种:

  1. 启动流程时设置变量:

    //键值对形式的变量,值也可以是实现了 serializable 接口的对象
    Map<String,Object> params = new HashMap<>();
    params.put("xxx","xxx");
    params.put("xxx","xxx");
    ...
    ProcessInstance holiday = runtimeService.startProcessInstanceByKey("holiday",params);
    
  2. 完成任务时设置变量:

    //依然是键值对形式的 params
    taskService.complete(task.getId(),params);
    
  3. 设置global/local变量:

    Map params = new HashMap();
    params.put("days",3);
    params.put("type","休假");
    ...
    //设置global变量
    runtimeService.setVariable("excutionId","key","value");
    runtimeService.setVariables("excutionId",params);
    //设置local变量
    runtimeService.setVariableLocal("excutionId","key","value");
    runtimeService.setVariablesLocal("excutionId",params);
    

任务监听器

除此之外 Camunda 还可以给每个任务节点设置监听器
Camunda工作流引擎二_第4张图片

就以 ”用户输入“ 这个用户任务为例,为其设置一个任务监听器:
Camunda工作流引擎二_第5张图片

既然设置了 Java Class 类型的监听器,那么就需要实现一个监听器:

public class UserListener implements TaskListener {
    @Override
    public void notify(DelegateTask delegateTask) {
        System.out.println("给用户发一个消息!");
    }
}

监听器需要去实现 TaskListener 接口,并重写其 notify() 方法,之后当这个任务节点被创建的时候,就会去执行这个任务监听器中的 notify() 方法。

PS:执行监听器使用方法也类似,需要实现 org.camunda.bpm.engine.delegate.ExecutionListener 接口。

一个示例

Camunda工作流引擎二_第6张图片

依然是请假流程,除了开始事件和结束事件,还有 input(用户任务—员工输入)、approve1(用户任务—部门审批)、approve2(用户任务—人事审批),以及一个系统任务—数据同步方法(模拟保存请假信息)。

给两个审批人都添加了监听器,当其任务创建时,会调用监听器的 notify() 方法给予 assignee 一个模拟的通知:
Camunda工作流引擎二_第7张图片

public class ApproverNotice implements TaskListener {
    @Override
    public void notify(DelegateTask delegateTask) {
        System.out.println("给"+ delegateTask.getAssignee() +"发一个消息!");
    }
}

如果员工请假天数少于等于 3 天,则只需要部门审批(approve1)之后即可审批结束保存数据,如果请假天数大于 3 天,则还需要人事审批(approve2)之后才能完成审批保存数据:
Camunda工作流引擎二_第8张图片

上图是其中的一条连线(序列流)。

当审批结束,则进入到系统任务(autoSync)保存请假信息:
Camunda工作流引擎二_第9张图片

public class AutoSync implements JavaDelegate {
    @Override
    public void execute(DelegateExecution delegateExecution) throws Exception {
        System.out.println("存储请假信息!!!!");
    }
}

这只是一个及其简化的示例,可以在每个用户任务后面添加排他网关,审批通过则继续,驳回时则直接到结束事件,只有当用户任务都审批通过才到 autoSync 任务。

部署上述流程后,就可以开始启动一条流程,观察表中的变化:

public void runProcinst(){
    Map<String,Object> params = new HashMap<>();
    params.put("employee","zhangsan");
    params.put("leave",new Leave("NO00001","休假",new Date()));
    params.put("days",2);
    ProcessInstance holiday = runtimeService.startProcessInstanceByKey("holiday",params);
    System.out.println(holiday.getProcessDefinitionId());
    System.out.println(holiday.getId());
    System.out.println(holiday.getProcessInstanceId());
}

启动流程后,可以在 act_hi_procinst 表中看到开启的流程实例, act_ru_task 和 act_hi_taskinst 表中看到正在运行中的用户任务实例,act_ru_variable、act_ru_varinst 和 act_hi_detail 表中找到设置的参数(其中 Leave 对象还有其对应的 bytearray_id_ 可以在 act_ge_bytearray 中找到其更具体的参数内容)

完成当前任务,向前推进:

public void taskComplete(){
    //目前zhangsan只有一个任务,业务中根据场景选择其他合适的方式
    Task task = taskService.createTaskQuery()
        .taskAssignee("zhangsan")
        .singleResult();
    Map<String,Object> params = new HashMap<>();
    params.put("deptment","lisi");
    //zhangsan完成任务,交由部门经理lisi审批
    taskService.complete(task.getId(),params);
}

执行方法,控制台上打印 给lisi发一个消息! 说明监听器在部门审批(approve1)用户任务创建时被调用了。

继续观察启动流程时介绍的表,act_ru_task 中之前 zhangsan 的用户任务已经被删除,在 act_hi_taskinst 中已经变为 completed 状态;但是由于我们设置的是 global 变量,在 act_ru_variable 中还可以找到开启流程时设置的参数;其他的表中也会有记录,细细观察会有更多收获,碍于篇幅,就不再赘述。

继续执行推进 lisi 的当前任务:

public void taskComplete(){
    //目前lisi只有一个任务,业务中根据场景选择其他合适的方式
    Task task = taskService.createTaskQuery()
        .taskAssignee("lisi")
        .singleResult();
    Map<String,Object> params = new HashMap<>();
    params.put("personal","wangwu");
    taskService.complete(task.getId(),params);
}

控制台打印 存储请假信息!!!! ,并没有去执行人事审批(approve2)用户任务,而是直接去执行数据同步(autoSync)系统任务,说明连线(顺序流)设置的条件表达式起效了(开启流程时,设置的请假天数是 2)。

此时, act_ru_task 表中的已经没有当前流程实例的任务了,流程结束。act_hi_taskinsk 表中此流程实例的用户任务也都变成了 completed 状态。

总结

初学,仅作为学习记录,备忘。

本篇仅作为从0到1的入门过程,不过是 Camunda 的冰山一角。

下一篇继续学习记录 Camunda 的其他组件功能!

文末放出笔者在学习本篇内容时的测试手稿:

@SpringBootTest
class CamundaDemoApplicationTests {

    @Autowired
    RuntimeService runtimeService;

    @Autowired
    TaskService taskService;

    @Autowired
    HistoryService historyService;

    @Autowired
    RepositoryService repositoryService;

    /**
     * 流程定义部署
     */
    @SneakyThrows
    @Test
    void deploy() {
        Deployment deploy = repositoryService.createDeployment()
            .addClasspathResource("bpmn/holiday.bpmn")
            .deploy();
        System.out.println(deploy.getId());
    }

    /**
     * 开启一个流程实例
     */
    @Test
    public void runProcinst(){
        Map<String,Object> params = new HashMap<>();
        params.put("employee","zhangsan");
        params.put("leave",new Leave("NO00001","休假",new Date()));
        params.put("days",2);
        ProcessInstance holiday = runtimeService.startProcessInstanceByKey("holiday",params);
        System.out.println(holiday.getProcessDefinitionId());
        System.out.println(holiday.getId());
        System.out.println(holiday.getProcessInstanceId());
    }

    /**
     * 流程任务查询
     */
    @Test
    public void taskQuery() {
        List<Task> tasks = taskService.createTaskQuery()
            .processDefinitionKey("holiday")
            .list();
        for (Task task : tasks) {
            System.out.println(task.getAssignee());
            System.out.println(task.getId());
            System.out.println(task.getName());
            System.out.println(task.getTenantId());
        }
    }

    /**
     * 流程任务执行
     */
    @Test
    public void taskComplete(){
        //目前lisi只有一个任务,业务中根据场景选择其他合适的方式
        Task task = taskService.createTaskQuery()
            .taskAssignee("lisi")
            .singleResult();
        Map<String,Object> params = new HashMap<>();
        params.put("personal","wangwu");
        taskService.complete(task.getId(),params);
    }

    /**
     * 流程定义查询
     */
    @Test
    public void queryDefine(){
        ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery();
        List<ProcessDefinition> definitions = query.processDefinitionKey("holiday")
            .orderByProcessDefinitionVersion()
            .desc()
            .list();
        for (ProcessDefinition definition : definitions) {
            System.out.println(definition.getDeploymentId());
            System.out.println(definition.getName());
            System.out.println(definition.getVersion());
            System.out.println(definition.getId());
            System.out.println(definition.getKey());
        }
    }

    /**
     * 删除流程定义
     */
    @Test
    public void deleteDefine(){
        ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
        List<ProcessDefinition> definitions = processDefinitionQuery.processDefinitionKey("holiday")
            .orderByProcessDefinitionVersion()
            .asc()
            .list();
        ProcessDefinition processDefinition = definitions.get(0);
        if (processDefinition != null){
            //删除流程定义,如果存在该定义的流程实例在运行中则删除报错
            //repositoryService.deleteDeployment(processDefinition.getDeploymentId());
            //true 级联删除流程定义,即使该流程有流程实例启动也可以删除(没有审批完的流程也会被先删除)
            repositoryService.deleteDeployment(processDefinition.getDeploymentId(),true);
        }
    }

    /**
     * 获取流程定义的资源文件
     */
    @SneakyThrows
    @Test
    public void outputBPMNFile(){
        List<ProcessDefinition> res = repositoryService.createProcessDefinitionQuery()
            .processDefinitionKey("holiday")
            .list();
        ProcessDefinition holiday = res.get(0);
        //获取BPMN文件的输入流                                                                   获取BPMN文件名
        InputStream BPMNIs = repositoryService.getResourceAsStream(holiday.getDeploymentId(), holiday.getResourceName());
        //如果部署了对应的图片,也可以获取到该图片的输入流                                             获取对应图片的资源名
        //InputStream PNGIs = repositoryService.getResourceAsStream(holiday.getDeploymentId(), holiday.getDiagramResourceName());
        //输入流转换为输出流
        FileOutputStream outputStream = new FileOutputStream("E:\\流程\\" + holiday.getResourceName(),true);
        IOUtils.copy(BPMNIs,outputStream);
    }

    /**
     * 查询历史信息
     */
    @Test
    public void queryHistory(){
        List<HistoricActivityInstance> list = historyService.createHistoricActivityInstanceQuery()
            .finished()
            .orderByHistoricActivityInstanceEndTime()
            .asc()
            .list();
        for (HistoricActivityInstance instance : list) {
            System.out.println(instance.getActivityId());
            System.out.println(instance.getProcessDefinitionKey());
            System.out.println(instance.getAssignee());
            System.out.println(instance.getStartTime());
            System.out.println(instance.getEndTime());
            System.out.println("=============================");
        }
    }

    /**
     * 启动一个流程实例,并且添加一个业务key
     * 业务key 可以在 act_ru_execution 中看到
     */
    @Test
    public void startProcInstAddBusinessKey(){
        ProcessInstance holiday = runtimeService.startProcessInstanceByKey("holiday", "0001");
        System.out.println(holiday.getBusinessKey());
    }

    /**
     * 挂起、激活流程定义的所有流程实例
     */
    @Test
    public void activateOrSuspendProcInsts(){
        //获取流程定义
        List<ProcessDefinition> holidays = repositoryService.createProcessDefinitionQuery()
            .processDefinitionKey("holiday")
            .list();
        ProcessDefinition holiday = holidays.get(0);
        //得到流程定义的所有流程实例会否都是暂停状态
        boolean suspended = holiday.isSuspended();
        if (suspended) {
            //激活
            repositoryService.activateProcessDefinitionById(holiday.getId(),true,null);
            System.out.println("定义"+holiday.getId()+"激活");
        }else {
            //挂起
            repositoryService.suspendProcessDefinitionById(holiday.getId(),true,null);
            System.out.println("定义"+holiday.getId()+"挂起");
        }
    }

    /**
     * 挂起、激活单个流程实例
     * 执行挂起的流程会抛出异常
     */
    @Test
    public void activateOrSuspendProcInst(){
        ProcessInstance holiday = runtimeService.createProcessInstanceQuery()
            .processInstanceBusinessKey("0001")
            .singleResult();
        boolean suspended = holiday.isSuspended();
        if (suspended) {
            runtimeService.activateProcessInstanceById(holiday.getId());
            System.out.println("实例"+holiday.getId()+"激活");
        }else {
            runtimeService.suspendProcessInstanceById(holiday.getId());
            System.out.println("实例"+holiday.getId()+"挂起");
        }
    }

    /**
     * 开启一条流程,并给用户任务的 assignee 赋值
     */
    @Test
    public void startProcessInstanceWithAssignee(){
        Map<String,Object> map = new HashMap<>();
        map.put("employee","zhangsan");
        map.put("deptment","lisi");
        map.put("personal","wangwu");
        ProcessInstance holiday = runtimeService.startProcessInstanceByKey("holiday", map);
        System.out.println(holiday.getProcessInstanceId());
    }

    /**
     * 设置global/loacal变量
     */
    @Test
    public void setVariables(){
        Map params = new HashMap();
        params.put("days",3);
        params.put("type","休假");
        //设置global变量
        runtimeService.setVariable("excutionId","key","value");
        runtimeService.setVariables("excutionId",params);
        //设置local变量
        runtimeService.setVariableLocal("excutionId","key","value");
        runtimeService.setVariablesLocal("excutionId",params);
    }
}

菜鸟本菜,不吝赐教,感激不尽!

更多题解源码和学习笔记:github 、CSDN 、M1ng

你可能感兴趣的:(工作流引擎,Camunda,工作流引擎,Activiti,教程)