本篇将在上一篇应用的基础上开发一个最基本的 ”流程Demo“ !
简单总结一下《Camunda工作流引擎一》中关于表的知识,大体上分为 4 类:
这些表存储了流程相关的各种信息,那么我们如何去对其进行CRUD呢?按照传统的思路,那自然是写 domain、dao、service 去进行对上述表的操作。
但是,实际上 Camunda 已经将上述表的操作给我们封装好了:
可以通过流程引擎去获取到各种 Service,从而进行一些流程操作:RepositoryService,资源管理类;RuntimeService,流程运行管理类;TaskService,任务管理类;HistoryService,历史管理类;ManagerService,引擎管理类等等。。。(在 Spring 项目中更多的是将这些 Service 也交由 Spring 容器进行管理控制)
本篇将按照下述的开发第一个 Camunda 流程Demo 的基本步骤来进行:
整合 Camunda(添加依赖)。
完善必要的配置。
实现业务流程建模(绘制 BPMN )。
工具下载地址: https://camunda.com/download/modeler/
部署流程定义。
启动流程实例。
查询代办任务。
处理代办任务。
结束流程。
1、2两个步骤,在上一篇中已经完成了,这两部也是启动应用、创建相关表的先行条件。以公司请假审批流程为例,直接从第 3 步开始!由简到繁!
首先创建一个开始事件和一个结束事件
,可以修改两个事件的 id 和 name,或者不作任何修改。
接下来就是创建 Task,然后点击右侧的"小扳手"设置为用户任务,可以根据需要修改用户任务的 Id、Name 和 Version Tag,或者不做修改使用默认,重点是 assignee 这个就是任务的处理人,可以有三种方式进行设置:
直接字符串写死。
UEL-value,是 Java EE6 规范的一部分,写法:${变量名} 或者 ${对象.属性名},本篇也将采用这种形式进行设置,以"员工输入"任务(input)为例:
如何传这个变量呢?后面运行流程实例的时候会再次提到!
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 进去:
然后就是使用我们开头说的各种工作流已经帮我们定义好的 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 变量可以变量名相同,没有影响。
设置流程变量的方法也有多种:
启动流程时设置变量:
//键值对形式的变量,值也可以是实现了 serializable 接口的对象
Map<String,Object> params = new HashMap<>();
params.put("xxx","xxx");
params.put("xxx","xxx");
...
ProcessInstance holiday = runtimeService.startProcessInstanceByKey("holiday",params);
完成任务时设置变量:
//依然是键值对形式的 params
taskService.complete(task.getId(),params);
设置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);
就以 ”用户输入“ 这个用户任务为例,为其设置一个任务监听器:
既然设置了 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 接口。
依然是请假流程,除了开始事件和结束事件,还有 input(用户任务—员工输入)、approve1(用户任务—部门审批)、approve2(用户任务—人事审批),以及一个系统任务—数据同步方法(模拟保存请假信息)。
给两个审批人都添加了监听器,当其任务创建时,会调用监听器的 notify()
方法给予 assignee 一个模拟的通知:
public class ApproverNotice implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
System.out.println("给"+ delegateTask.getAssignee() +"发一个消息!");
}
}
如果员工请假天数少于等于 3 天,则只需要部门审批(approve1)之后即可审批结束保存数据,如果请假天数大于 3 天,则还需要人事审批(approve2)之后才能完成审批保存数据:
上图是其中的一条连线(序列流)。
当审批结束,则进入到系统任务(autoSync)保存请假信息:
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