目录
- 一、以一个处理流程开始
- 1.1 后端
- 1.2 前端
- 1.3 执行
- 二、Camunda的补充
- 2.1 使用方式
- 2.2 可视化平台的Cockpit
- 2.3 流程相关数据
- 2.4 表介绍
- 2.5 前端集成Modeler
- 三、用Java集成Camunda
- 3.1 集成配置
- 3.2 自动部署
- 3.2.1 修改process.xml位置
- 3.2.2 多进程引擎配置与多租户
- 3.3 历史事件配置
- 3.3.1 查询
- 3.3.2 任务报告
- 3.4 Service
- 3.5 用户业务
- 3.6 流程启动Controller
- 3.7 业务任务-内部任务
- 3.7.1 Java Class实现
- 3.7.2 Delegate Expression实现
- 3.7.3 Express实现
- 3.7.4 流程的回退与重启与暂停
- 回退
- 重启
- 暂停
- 3.8 业务任务-外部任务
- 3.8.1 异步响应:长轮询(Long Polling)
- 3.8.2 注解方式
- 配置与依赖
- 写一个外部任务
- 重试方式
- 优先级
- 3.9 任务监听器(引擎端)
- 3.10 鉴权
- 3.11 多实例任务
- 3.11.1 顺序执行
- 3.11.2 多实例内置变量
- 3.11.3 并行执行
- 3.12 脚本任务
- 附录
集成与简单的使用请参看前文。camunda流程引擎基本使用(笔记)
流程活动可参考-流程引擎实施参考
例子学习于blibli-camunda工作流实战课程,该视频简介者有一些地方是错的,谨慎观看。
一、以一个处理流程开始
我们建立如下流程,安排:
- 后端:加入购物车与物流发货。使用SpringBoot配合Camunda的包
- 前端:付款(只有逻辑)。使用nodejs代码配合Camuanda的包(也可以用VUE等方式)
1.1 后端
后端配置如下内容:
server:
port: 8081
camunda:
connect:
url: "http://localhost:8080/engine-rest"
timeout: 10000
注意,笔者现在使用的是7.2版本,和上篇文章版本不同(可不替换)。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>CamudaDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.6</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>org.camunda.bpm</groupId>
<artifactId>camunda-external-task-client</artifactId>
<version>7.20.0</version>
</dependency>
</dependencies>
</project>
创建一个config,配置Camunda连接
package com.camunda.demo.config;
import lombok.Data;
import org.camunda.bpm.client.ExternalTaskClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Data
public class CamundaConfig {
/**
* platform地址
* */
@Value("${camunda.connect.url}")
private String CAMUNDA_URL;
/**
* 超时时间
* */
@Value("${camunda.connect.timeout}")
private Integer TIME_OUT;
@Bean
public ExternalTaskClient getExternalTaskClient(){
return ExternalTaskClient.create()
.baseUrl(CAMUNDA_URL)
.asyncResponseTimeout(TIME_OUT)
.build();
}
}
package com.camunda.demo.shopping;
import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.client.ExternalTaskClient;
import org.camunda.bpm.engine.variable.Variables;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Map;
@Slf4j
@Component(value = "shoppingTask")
public class ShoppingTask {
@Resource
private ExternalTaskClient externalTaskClient;
/**
* 订阅一个Service(external类型)
* */
public void handleShoppingCart(){
/**
* 订阅服务
* */
externalTaskClient.subscribe("add_card")
/**
* 流程定义,即所属流程不
* */
.processDefinitionKey("Process_11089zy")
/**
* 最长加锁时间
* */
.lockDuration(2000)
/**
* 订阅配置
* ===========
* 处理区域
*
* externalTask 外部传参相关
*
* externalTaskService 流程执行相关
* */
.handler((externalTask,externalTaskService)->{
/**
* 处理逻辑
* */
log.info("开始加入到购物车");
Map<String,Object> goodVariable = Variables.createVariables()
.putValue("size","xl")
.putValue("count",2);
log.info("已加入到购物车:\n{}",goodVariable);
/**
* 执行完成
* */
externalTaskService.complete(externalTask,goodVariable);
}).open();
}
public void handleDelivery(){
externalTaskClient.subscribe("delivery")
.processDefinitionKey("Process_11089zy")
.lockDuration(2000)
.handler(((externalTask, externalTaskService) -> {
Object toWhere = externalTask.getVariable("toWhere");
log.info("即将发往目的地:{}",toWhere);
externalTaskService.complete(externalTask);
})).open();
}
}
可以临时写一个启动类
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
ConfigurableApplicationContext configurableApplicationContext = SpringApplication.run(MainApplication.class,args);
ShoppingTask shoppingTask = (ShoppingTask)configurableApplicationContext.getBean("shoppingTask");
shoppingTask.handleShoppingCart();
shoppingTask.handleDelivery();
}
}
注解方式也类似,见后文 外部任务。
1.2 前端
使用如下命令,初始化前端
npm init -y
使用如下命令,安装依赖(如果有版本问题,可以加–force,要注意找一个低版本的,高版本不支持直接启动(笔者使用2.2.0)
可以参考-camunda-external-task-client-js
npm install camunda-external-task-client-js
npm install -D open
直接写一个subscribe.js
const { Client, logger ,Variables} = require("camunda-external-task-client-js");
/**
* 流程包
* ==========
* 配置项
* */
const config = {baseUrl: "http://localhost:8080/engine-rest",use:logger,asyncResponseTimeout: 10000}
const client = new Client(config);
/**
* 配置项
* ========
* 流程处理
*
* 订阅服务-同后端代码
* */
client.subscribe('pay',{processDefinitionKey:"Process_11089zy"},
async function({task,taskService}){
/**
* 读取我们加入购物车流程中的变量
* */
const size = task.variables.get('size');
const count = task.variables.get('count');
console.log(`顾客下单尺寸:${size},数量${count}`);
/**
* 添加新的变量
* 订单送往哪里
* */
const processVariables = new Variables().set("toWhere","shanghai China")
try{
/**
* 处理完成
* 查看源码可知:compelete 返回的是Promise,因此需要await来等待
* */
await taskService.complete(task,processVariables);
console.log("完成");
}catch (e){
console.error(`异常${e}`);
}
})
通过如下代码直接启动
node .\subscribe.js
1.3 执行
完成后,我们部署这个流程
登录可以看到我们的流程已经部署上去了,启动它。(基本使用见上一篇)
二、Camunda的补充
2.1 使用方式
- 嵌入式:及整合进项目,如上篇中的Java整合Camunda。代码与流程解耦。
- 组件式:被所有程序共有,类似一个消息队列。它将任务指派给不同程序。(一个调度组件)。代码与流程耦合。
- 中间件:做成一个平台,单独部署,第一部分就是这种方式(SAAS平台)。代码与流程解耦。
- 集群:camunda8,中间件集群。
- 云原生:camunda8,部署于k8s中,elasticsearch存储方式,以支持大型微服务。
2.2 可视化平台的Cockpit
可参考官方文档-Web Applications
我们启动platform平台时,可以进入到该页面。
- Cockpit:监控中心(驾驶舱)
- Tasklist:有权限可看到的任务
- Admin:权限信息
点击进入Cockpit
- Running Process Instance:流程实例
- Open Incidents:异常
- Open Human Task:人工任务
- Deployed:部署的流程(流程定义)
- Process Definitions:BPMN相关(流程)。 Cockpit 允许监控 BPMN 流程。仪表板是 BPMN 监视功能的入口点。选择已部署的流程定义或搜索流程实例。这将带您进入流程定义视图或流程实例视图。
- Decision Definitions:DMN相关(决策)。Cockpit 允许监控 DMN 决策。仪表板是 DMN 监控功能的入口点。选择已部署的决策定义以访问决策定义视图。显示的有关决策定义和决策实例的所有数据都基于历史数据。与流程定义和流程实例不同,没有运行时数据,因为决策是即时执行的,没有中间等待状态或保存点。
- Case Definitions: CMMN相关(案例,此功能仅包含在 Camunda 平台的企业版中,在社区版中不可用)。Cockpit允许监控CMMN案例。仪表板是 CMMN 监视功能的入口点。选择已部署的案例定义,或搜索案例实例。这将分别将您带到案例定义视图或案例实例视图。
- Deployments:总部署(多版本,前面的多版本统一模型算一个)。显示所有部署、其资源以及这些资源的内容的概述。它允许删除现有部署,以及重新部署旧资源和创建新部署。可以显示部署中的资源内容。也可以从此视图下载单个资源
2.3 流程相关数据
进入某一个流程定义,我们是可以看到下列数据:
- Deinition Version: 版本,默认每次修改再部署时,都会提高一个版本
- Version Tag:版本标签,用户自定义版本
- Definition Key:流程图的唯一ID。
- Definition ID = Definition Key:Deinition Version:随机乱码。流程图下各个版本的唯一ID。
Definition ID 与 Definition Key 为 多 对 1
乱码的作用:同一流程图的同一版本可以交给不同租户(理解为公司),乱码就是为了区分不同租户,避免区分不开。
- History Time to LIve:历史数据保存时间
- Tenant ID:租户ID
- Deployment ID:该Definition ID对应在数据库中的主键ID。
- Definition Name:流程名称
流程实例
ID为流程实例的instance ID ,我们点进去
- Instance ID:实例ID
- Business Key:业务Key(流水号之类的)
其他部分
- Variable:流程变量,以Map的形式,我们之前也操作过的
- Incidents:异常
- Called Process Instance:跨流程调用,调用其他实例
- User Tasks:用户任务
- Jobs:定时任务,可重做
- External Task:外部任务
2.4 表介绍
关于Camunda的表可以参考camunda数据库表结构介绍
以及官方文档Database Schema
2.5 前端集成Modeler
参考官网-基于 Web 的 BPMN、DMN 和表单工具
三、用Java集成Camunda
集成可以参考
Spring Boot 集成
流程引擎配置
3.1 集成配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>CamundaEngine</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.1.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter</artifactId>
<version>7.20.0</version>
<exclusions>
<exclusion>
<groupId>org.camunda.bpm.model</groupId>
<artifactId>camunda-cmmn-model</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.camunda.bpm.model</groupId>
<artifactId>camunda-cmmn-model</artifactId>
<version>7.18.0</version>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId>
<version>7.20.0</version>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-rest</artifactId>
<version>7.20.0</version>
</dependency>
</dependencies>
</project>
注意:
- 7.20.0版本中camunda-cmmn-model这个包笔者导不进来,因此另外导入了一个版本
- 导入了webapp前端方便操作,页面默认位置如下:
http://localhost:8080/camunda/app/welcome/default/#!/login
- 引入了RestAPI。相关文档可参考REST API Reference
yml文件配置可以参考官方给出的
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/camunda?useUnicode=true&NamePatternMatchesAll=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
application:
name: work-engine
camunda:
bpm:
admin-user:
id: demo
password: demo
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3.2 自动部署
启动时自动扫描文件
参考-官方文档-The processes.xml Deployment Descriptor
自定义部署(运行中添加)可以参考:【Camunda 三】Camunda模型文件部署
resources下,创建如下文件 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/c189109a043f4314b797049c9f671bc8.png#pic_center)
process.xml默认配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<process-application
xmlns="http://www.camunda.org/schema/1.0/ProcessApplication"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
>
<process-archive name="loan-approval">
<process-engine>default</process-engine>
<properties>
<property name="isDeleteUponUndeploy">false</property>
<property name="isScanForProcessDefinitions">true</property>
</properties>
</process-archive>
</process-application>
- process-archive name:这个是归档名称,可以为空
- process-engine:部署到的流程引擎名称 ,可不填写默认(多引擎时需要)
- isDeleteUponUndeploy:此属性控制取消部署流程应用程序是否应导致从数据库中删除流程引擎部署。默认设置为 false。如果此属性设置为 true,那么取消部署流程应用程序将导致从数据库中删除部署(包括流程实例)。
- isScanForProcessDefinitions:如果此属性设置为 true,那么将自动扫描流程应用程序的类路径以查找可部署资源。
可以参考Camunda动态生成工作流流程定义并部署更新流程(新手上路版)
3.2.1 修改process.xml位置
如果需要修改process.xml位置,可以使用官方给出代码,如下:
@ProcessApplication(
name="my-app",
deploymentDescriptors={"path/to/my/processes.xml"}
)
public class MyProcessApp extends ServletProcessApplication {
}
3.2.2 多进程引擎配置与多租户
如何创建多进程引擎可以参考The Process Application class,有多种配置方式
<process-application
xmlns="http://www.camunda.org/schema/1.0/ProcessApplication"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<process-engine name="my-engine">
<configuration>org.camunda.bpm.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration</configuration>
</process-engine>
<process-archive name="loan-approval" tenantId="tenant1>
<process-engine>my-engine</process-engine>
<properties>
<property name="isDeleteUponUndeploy">false</property>
<property name="isScanForProcessDefinitions">true</property>
</properties>
</process-archive>
</process-application>
3.3 历史事件配置
可参考-历史记录和审核事件日志
通过配置该项,决定记录的日志颗粒度。
- NONE:不触发任何历史记录事件
- ACTIVITY:触发以下事件:
- 流程实例 START、UPDATE、END、MIGRATE:在流程实例启动、更新、结束和迁移时触发
- 案例实例 CREATE、UPDATE、CLOSE:在创建、更新和关闭案例实例时触发
- 活动实例 START、UPDATE、END、MIGRATE:在活动实例启动、更新、结束和迁移时触发
- 案例活动实例 CREATE、UPDATE、END:在创建、更新和结束案例活动实例时触发
- 任务实例 CREATE、UPDATE、COMPLETE、DELETE、MIGRATE:在创建、更新(即重新分配、委派等)、完成、删除和迁移任务实例时触发。
- AUDIT:除了 history level 提供的事件外,还会触发以下事件:ACTIVITY
- 变量实例 CREATE、UPDATE、DELETE、MIGRATE:在创建、更新、删除和迁移流程变量时触发。默认历史记录后端 (DbHistoryEventHandler) 将变量实例事件写入历史变量实例数据库表。此表中的行会随着变量实例的更新而更新,这意味着只有流程变量的最后一个值可用。
- FULL:除了 history level 提供的事件外,还会触发以下事件:AUDIT
- 表单属性 UPDATE:在创建和/或更新表单属性时触发。
默认历史记录后端 (DbHistoryEventHandler) 将历史变量更新写入数据库。这样就可以使用历史记录服务检查过程变量的中间值。
用户操作日志更新:当用户执行声明用户任务、委派用户任务等操作时触发。 - 事件创建、删除、解决、迁移:在创建、删除、解决和迁移事件时触发
- 历史作业日志 CREATE、FAILED、SUCCESSFUL、DELETED:在创建作业、作业执行失败或成功或作业已删除时触发
- 决策实例 EVALUATE:当 DMN 引擎评估决策时触发。
- Batch START、END:在批处理开始和结束时触发
- 标识链接 ADD、DELETE:在添加、删除标识链接时,或者在设置或更改用户任务的受托人时,以及设置或更改用户任务的所有者时触发。
- 历史外部任务日志 CREATED, DELETED, FAILED, SUCCESSFUL:在已创建、删除外部任务或外部任务执行已报告失败或成功时触发。
- 表单属性 UPDATE:在创建和/或更新表单属性时触发。
- AUTO:如果您计划在同一数据库上运行多个引擎,则该级别非常有用。在这种情况下,所有引擎都必须使用相同的历史记录级别。与其手动保持配置同步,不如使用级别,引擎会自动确定数据库中已配置的级别。如果未找到,则使用默认值。
- 请记住:如果您计划使用自定义历史记录级别,则必须为每个配置注册自定义级别,否则会引发异常。
请注意,使用默认历史记录后端时,历史记录级别存储在数据库中,以后无法更改。
设置方式:
ProcessEngine processEngine = ProcessEngineConfiguration
.createProcessEngineConfigurationFromResourceDefault()
.setHistory(ProcessEngineConfiguration.HISTORY_FULL)
.buildProcessEngine();
-
使用Spring XML或部署描述符(bpm-platform.xml,processes.xml)进行设置。
-
使用 Camunda Wildfly 子系统时,可以通过 Wildfly 配置(独立 .xml、domain.xml)设置属性。
总之,只需要在xm中添加如下成员变量即可
<property name="history">audit</property>
3.3.1 查询
使用HistoryService可创建如下查询:
- HistoricProcessInstanceQuery:历史进程实例查询
- HistoricCaseInstanceQuery:历史案例实例查询
- HistoricActivityInstanceQuery:历史活动实例查询
- HistoricCaseActivityInstanceQuery:历史案例活动查询
- HistoricVariableInstanceQuery:历史变量查询
- HistoricDetailQuery:历史细节查询,记录变量更新等信息
- HistoricTaskInstanceQuery:任务实例查询
- HistoricIncidentQuery:历史事件(异常)查询
- UserOperationLogQuery:用户操作日志查询
- HistoricJobLogQuery:历史作业日志查询
- HistoricDecisionInstanceQuery:历史决策实例查询
- HistoricBatchQuery:历史批量处理查询
- HistoricIdentityLinkLogQuery:与用户关联的日志查询
- HistoricExternalTaskLogQuery:外部日志查询
3.3.2 任务报告
检索已完成任务的报告。
对于任务报告,有两种可能的报告类型:
- 计数
- 持续时间。
3.4 Service
Camunda提供各类Service以便使用。
-
ProcessEngine:第一次调用流程引擎时初始化并构建流程引擎,之后总是返回相同的流程引擎
- 可以使用ProcessEngines.init() 和 ProcessEngines.destroy()来正确地创建和关闭所有的流程引擎
- 通过ProcessEngine.XXService 来获取Service
- SpringBoot中不需要如此
-
RepositoryService:用于管理和操作部署和流程定义的操作
-
RuntimeService:流程实例相关操作
-
TaskService:Activity,也就是对各个节点的相关操作(含挂起,激活等)
-
IdentityService:用户管理等相关操作
-
FormService:表单相关操作
- 启动表单是在流程实例启动之前向用户显示的表单
- 任务表单是在用户希望完成表单时显示的表单
-
HistoryService:历史记录相关
-
ManagementService:它允许检索关于数据库表和表元数据的信息。此外,它还公开了作业的查询功能和管理操作。作业在引擎中用于各种用途,如计时器、异步延续、延迟挂起/激活等
-
FilterService:允许创建和管理过滤器。过滤器是像任务查询一样存储的查询。Tasklist(自带的页面)使用过滤器来过滤用户任务。
- Filter相关说明
-
ExternalTaskService:提供对外部任务实例的访问。(见第一部分)
-
CaseService:同RunService,不过用于案例
-
DecisionService:决策相关服务
3.5 用户业务
Modeler可参考
前置工作:使用平台自行创建一个员工
我们接下来创建一个请假流程:员工请假,上级审批。
创建如下审批流程:
初始化变量 starter,流程引擎将在流程发起时,自动将发起人信息填入。
指派给starter,即发起者,可以指派给人、组、用户组、以及设置有效期
在员工请假处添加一个基础表单
在领导审批处添加另一个用户(此处直接指派给我们的管理员也可以)
完成后部署即可。
package com.engine.controller;
import jakarta.annotation.Resource;
import org.camunda.bpm.engine.IdentityService;
import org.camunda.bpm.engine.RuntimeService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/leave")
public class LeaveController {
/**
* 用户、组、租户等相关
* */
@Resource
private IdentityService identityService;
/**
* 流程实例操作
* */
@Resource
private RuntimeService runtimeService;
@GetMapping("/start/{user}")
public void startProcess(@PathVariable("user")String user){
/**
* 表:act_id_user
*
* 在此处我们设置发起人信息,对象存储在了如下类型中
* ThreadLocal<Authentication> currentAuthentication = new ThreadLocal();
* 因此线程之间是隔离的,不会造成混用情况
* */
identityService.setAuthenticatedUserId(user);
/**
* 表:
* act_re_procdef BPMN定义
* act_re_deployment流程部署信息
* act_ge_bytearray 部署的实际内容
* 开启一个流程实例
*
* 开启后的实例存放于
* 表:act_run_execution 以及 act_run_task
* act_run_execution BPMN运行时记录,包含到那个节点了
* act_run_task 流程总的记录,记录了执行到哪个节点了,指派给了谁等信息
* 多个版本,默认使用最新版本
* 启动加入流程变量时,需要加入startProcessInstanceByKey中
* */
runtimeService.startProcessInstanceByKey("Process_0pzlxi8");
}
}
启动后调用接口,可以看到确实有一个流程,并且参数已经有了
我们使用页面去处理后,完成该流程。
3.6 流程启动Controller
可以做一个简单的启动类,帮助我们启动
package com.engine.controller;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.engine.IdentityService;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
@RequestMapping("/process")
public class ProcessController {
@Resource
private IdentityService identityService;
@Resource
private RuntimeService runtimeService;
@GetMapping(value = {"" +
"/start/{processKey}/user/{user}",
"/start/{processKey}",
"/start/{processKey}/businessKey/{businessKey}",
"/start/{processKey}/{businessKey}/{user}"
})
public void startProcess(@PathVariable("processKey") String processKey,
@PathVariable(value = "user",required = false)String user,
@PathVariable(value = "businessKey",required = false)String businessKey
){
if(user != null)
identityService.setAuthenticatedUserId(user);
ProcessInstance processInstance = null;
if(businessKey != null)
processInstance = runtimeService.startProcessInstanceByKey(processKey,businessKey);
else processInstance =
runtimeService.startProcessInstanceByKey(processKey);
log.info("启动成功,\nInstanceId:{}\nDefinitionId:{}\nInstanceId:{}\nstarter:{}",
processInstance.getRootProcessInstanceId(),
processInstance.getProcessDefinitionId(),
processInstance.getProcessInstanceId(),
identityService.getCurrentAuthentication() == null?null:identityService.getCurrentAuthentication().getUserId()
);
}
}
3.7 业务任务-内部任务
创建如下结构
3.7.1 Java Class实现
在预约修理家电实现处选择java class,表示以流程引擎自己的Java服务实现。随后我们在集成的服务里面新建一个ReserveRepairService 服务,并把路径添加进去。
package com.engine.serviceTask;
import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
@Slf4j
public class ReserveRepairService implements JavaDelegate {
@Override
public void execute(DelegateExecution execution) throws Exception {
log.info("\n当前流程实例-{}\n执行-{}\n事件名称-{}",
execution.getProcessInstanceId(),
execution.getCurrentActivityName(),
execution.getEventName()
);
}
}
3.7.2 Delegate Expression实现
使用Delegate表达式,可以让流程引擎直接调用自己的Bean,我们需要让这个bean实现Camunda提供的包,默认调用execute。
package com.engine.serviceTask;
import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.springframework.stereotype.Service;
@Service("doRepair")
@Slf4j
public class DoRepairService implements JavaDelegate {
@Override
public void execute(DelegateExecution execution) throws Exception {
log.info("\n当前流程实例-{}\n执行-{}\n事件名称-{}",
execution.getProcessInstanceId(),
execution.getCurrentActivityName(),
execution.getEventName()
);
execution.setVariable("repairManName","打工人A");
}
}
3.7.3 Express实现
该方式让流程引擎调用指定Bean的方法。需要指定接收返回的变量
package com.engine.serviceTask;
import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.springframework.stereotype.Service;
@Service("telCall")
@Slf4j
public class TelCallService {
public Integer doCall(DelegateExecution execution){
log.info("\n当前流程实例-{}\n执行-{}\n事件名称-{}",
execution.getProcessInstanceId(),
execution.getCurrentActivityName(),
execution.getEventName()
);
/**
* VariablesLocal 是该活动的变量,生命周期与活动一致。
* Variables 是流程变量,生命周期与流程一致。当设置同名变量时,会进行覆盖。
* 所有变量会:
* 保存在表 act_ru_variable中,当有Task_ID时,代表是Local
* act_hi_varinst 为历史变量表
* */
String repairManName = String.valueOf(execution.getVariable("repairManName"));
log.info("请对-{}的服务打分",repairManName);
return 10;
}
}
我们也可以用#号表达式
public void getScore(DelegateExecution execution){
log.info("\n当前流程实例-{}\n执行-{}\n事件名称-{}",
execution.getProcessInstanceId(),
execution.getCurrentActivityName(),
execution.getEventName()
);
log.info("员工-{}的服务得分是:{}",
execution.getVariable("repairManName"),
execution.getVariable("score"));
}
执行结果:
可以发现,这个我们启动的log在最后才打出来,是同步执行的。
3.7.4 流程的回退与重启与暂停
回退
可以参考Process Instance Modification
继续上述逻辑,我们想要在回访后,重新设置评分。
@Resource
RuntimeService runtimeService;
@Resource
RepositoryService repositoryService;
public Integer doCall(DelegateExecution execution){
log.info("\n当前流程实例-{}\n执行-{}\n事件名称-{}",
execution.getProcessInstanceId(),
execution.getCurrentActivityName(),
execution.getEventName()
);
if(execution.getVariable("redo") != null && (Boolean) execution.getVariable("redo") ){
log.info("重新打分:6分");
execution.setVariable("rescore",6);
return (Integer) execution.getVariable("score");
}
String repairManName = String.valueOf(execution.getVariable("repairManName"));
log.info("请对-{}的服务打分",repairManName);
return 10;
}
public void getScore(DelegateExecution execution){
log.info("\n当前流程实例-{}\n执行-{}\n事件名称-{}",
execution.getProcessInstanceId(),
execution.getCurrentActivityName(),
execution.getEventName()
);
if(execution.getVariable("rescore") == null){
ProcessDefinitionEntity processDefinitionEntity = (ProcessDefinitionEntity)repositoryService.getProcessDefinition(execution.getProcessDefinitionId());
List<ActivityImpl> activityList = processDefinitionEntity.getActivities();
log.info("该流程的所有活动\n{}\n",activityList);
int target = -1;
int i = 0;
for(;i < activityList.size(); i++,target++){
if(activityList.get(i).getActivityId() == execution.getCurrentActivityId()){
break;
}
}
if(target < 0){
target = 0;
}
log.info("\n当前节点位置:{},id:{},name:{}\n回退节点位置:{},id:{},name:{}"
,i,activityList.get(i).getActivityId(),activityList.get(i).getName()
,target,activityList.get(target).getActivityId(),activityList.get(target).getName());
log.info("想要重新打分");
// 修改当前流程至上一流程前
runtimeService
.createProcessInstanceModification(execution.getProcessInstanceId())
.startBeforeActivity(activityList.get(target).getActivityId(),execution.getActivityInstanceId())
.setVariable("redo",true)
.cancelActivityInstance(execution.getActivityInstanceId())
.execute();
return;
}
log.info("员工-{}的服务得分是:{},修改得分是:{}",
execution.getVariable("repairManName"),
execution.getVariable("score"),
execution.getVariable("rescore")
);
}
重启
依据官方例子流程重启动
直接删除实例
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().singleResult();
runtimeService.deleteProcessInstance(processInstance.getId(), "any reason");
然后重新启动即可
runtimeService.restartProcessInstance(processInstance.getProcessDefinitionId())
.startBeforeActivity("receivePayment")
.startBeforeActivity("shipOrder")
.processInstanceIds(processInstance.getId())
.execute();
重启后,全局变量将会带入重启流程。需手动设置局部变量,例如通过调用 。RuntimeService.setVariableLocal(…)
从技术上讲,已创建一个新的流程实例。历史进程实例和重新启动的进程实例的 ID 不同。
也可以使用-RESTAPI
暂停
使用如下方法即可
runtimeService.suspendProcessInstanceById();
runtimeService.startProcessInstanceById();
3.8 业务任务-外部任务
可参考-外部任务
外部任务的执行流程:
- 外部任务注册至External Task Client
- 有实例任务时,由External Task Client抓取任务并锁定(避免重复消费)
- 分发至对应外部任务
- 外部任务完成后告知External Task Client
- 失败时上报异常,流程会卡在该节点:可重试。
3.8.1 异步响应:长轮询(Long Polling)
流程引擎对于客户端设计为长轮询模式拉取任务。可参考长轮询的实现方式
- 当没有外部任务可以用的时候,请求会被服务器挂起并加锁,防止重复消费
- 一旦有新的外部任务可以执行时,就会重新激活请求并执行响应
- 设置超时时间,可以在超时后释放该锁并不在挂起该任务
3.8.2 注解方式
非注解方式见第一部分。
官方文档 spring-boot-starter外部任务配置项 7.20
配置与依赖
此处可以另起一个项目。
引入如下同版本包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>CamudaDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.1.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-external-task-client</artifactId>
<version>7.20.0</version>
</dependency>
</dependencies>
</project>
配置
server:
port: 8081
camunda:
bpm:
client:
# 流程引擎地址
base-url: http://localhost:8080/engine-rest
# 长轮询持续时间(异步响应超时时间),默认为null
# 设置后开启长轮询
async-response-timeout: 20000
# 一次最多拉取任务数量,默认10
max-tasks: 1
# 订阅topic的上锁时间,超时后,其他外部任务才能获取
# 优先级小余直接在接口上配置,默认20,000
lock-duration: 10000
# 当前工作节点的ID
worker-id: camunda-demo
spring:
application:
name: camunda demo
写一个外部任务
沿用上述流程,添加如下配置:
package com.camunda.demo.camundaSubscribe;
import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.client.spring.annotation.ExternalTaskSubscription;
import org.camunda.bpm.client.task.ExternalTaskHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Slf4j
public class RepairSubscribe {
//全局配置优先级小余私有配置,因此超时会以此处为准
@Bean
@ExternalTaskSubscription(topicName = "try_self_repair",processDefinitionKeyIn = {"Process_10qrmih"},lockDuration = 2000)
public ExternalTaskHandler TrySelfRepair(){
return ((externalTask, externalTaskService) -> {
/**
* 是否免费修
* */
Boolean isFree = true;
if(isFree){
log.info("免费维修");
externalTaskService.complete(externalTask);
}
else{
log.info("自己修");
/**
* message stacktrace 重试次数 重试超时时间
* 重试次数为0时,会创建一个异常事件
* */
externalTaskService.handleFailure(externalTask,"message-自己修不好","stacktrace",0,5000);
}
});
}
}
为了方便,我们手动调整true,false来看一下效果。
true时:
可以看到转为外部服务时,就不再是同步等待了。
false时:
可以看到创建了一个异常
重试方式
需要设置重试时可以用如下格式:
Integer retries = 3;
if (externaltask.getRetries() != null) {
retries = externaltask.getRetries() - 1;
}
externaltaskService.failure("Reason", retries);
优先级
在一个流程中,各个节点可以设置优先级,通过优先级,来让哪个流程实先执行。
优先级越高越先执行,默认都为0
3.9 任务监听器(引擎端)
在引擎端使用监听器,
添加如下配置,在结束时判断用户是否添写上门修理的地址,没有则使用默认地址
同理,于上门修理处添加一个监听器,在开始时监测
package com.engine.listener;
import io.micrometer.common.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.ExecutionListener;
import org.springframework.stereotype.Component;
@Component("checkHomeAddress")
@Slf4j
public class CheckHomeAddressListener implements ExecutionListener {
@Override
public void notify(DelegateExecution execution) throws Exception {
log.info("校验用户");
String homeAddress = (String) execution.getVariable("homeAddress");
if(StringUtils.isBlank(homeAddress)){
log.info("未获取到地址");
execution.setVariable("homeAddress","默认地址");
}
}
}
package com.engine.listener;
import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.ExecutionListener;
import org.springframework.stereotype.Component;
@Component("noticeCustomer")
@Slf4j
public class NoticeCustomerListener implements ExecutionListener {
@Override
public void notify(DelegateExecution execution) throws Exception {
log.info("开始通知客户");
String homeAddress = (String) execution.getVariable("homeAddress");
log.info("尊敬的客户您好,维修师傅正在前往:{},请耐心等候",homeAddress);
}
}
3.10 鉴权
官方可以用web.xml配置,建议使用另外实现。
比如整合SpringSecurity等。
3.11 多实例任务
当需要执行一个循环的流程时,可以让其作为多实例任务。
多实例任务分为:
- 串行多实例任务
- 并行多实例任务
修改前面的请假流程,让它变为多实例任务。
下图中,可以看到:
- 三根竖线:异步执行
- 三根横线:顺序执行
可以看到上图可以添加多实例
- Loop cardinality:执行循环次数
- Completion condition:循环跳出条件,当满足时,可以提前结束循环
- collection:不指定循环次数,直接循环对象(list)
- Element:List 中的E
- Asynchronous before/after:异步前/异步后操作
3.11.1 顺序执行
我们先做顺序执行:
另外写一个启动类,并提前添加用户:test,zhangsan,lisi,wangwu
@GetMapping("/start/multi")
public void startProcess(){
identityService.setAuthenticatedUserId("test");
List<String> leaders = new ArrayList<>();
leaders.add("zhangsan");
leaders.add("lisi");
leaders.add("wangwu");
VariableMap variableMap = Variables.createVariables();
variableMap.put("leaders",leaders);
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("Process_multi_task",variableMap);
log.info("启动成功,\nInstanceId:{}\nDefinitionId:{}\nInstanceId:{}\nstarter:{}",
processInstance.getRootProcessInstanceId(),
processInstance.getProcessDefinitionId(),
processInstance.getProcessInstanceId(),
identityService.getCurrentAuthentication() == null ? null : identityService.getCurrentAuthentication().getUserId()
);
}
如果我们在使用张三前先用李四的账号,会发现虽然有但不能评价。
3.11.2 多实例内置变量
同init的时候后可以直接用内置变量starter,多任务中可以使用如下内置变量:
- nrOfActiveInstances:当前活动的实例数量
- loopCounter:循环计数器,办理人在列表中的索引
- nrOfInstances:多实例任务中总共的实例数目
- nrOfCompletedInstances:已经完成的实例数量
3.11.3 并行执行
如下图修改,只要有两个人同意就可以。
此时评价将不分先后,此外可以看到同时开始了循流程
3.12 脚本任务
Camunda支持大多是JSR-223的脚本引擎。比如JavaScript、Groovy等。
对前面请假流程增加扣年假这个操作。
innline 即在如下图中写入
External则是外部,通过路径引用,需要给出返回变量名。
需要用如下前缀:
classpath:// 也就是需要放置于工程目录下,通过Springboot的逻辑加载
deployment:// 也就是需要放置于模型扫描目录下,一同部署于数据库
附录
camunda中文-官方文档
camunda内部构造
camunda英文-官方文档
blibli-camunda工作流实战课程
camunda数据库表结构介绍