Spring Boot整合流程引擎Flowable
一.工作流介绍
官方Flowable网站:Introducing Flowable Open Source · Flowable Open Source Documentation
中文文档:Flowable BPMN 用户手册 (v 6.3.0) (tkjohn.github.io)
1.1 为什么使用工作流
在程序员工作中,或多或少都会遇见审批流程类型的业务需求。一个审批流程可能包含开始->申请->领导审批->老板审批->结束等多个阶段,如果我们用字段去定义每一个流程阶段(0->开始 1->申请 2->领导审批 3->老板审批 4->结束),虽然可以实现流程运行逻辑,但这样业务代码逻辑复杂。如果审批流程还有驳回操作,则还需要加一个是否驳回字段,显然这样实现,成本太大,且不利于维护。
为了解决上述用代码逻辑硬写审批流程而导致的成本大,不利用维护的缺点,工作流因此而生。
1.2 工作流是什么
工作流,是把业务之间的各个步骤以及规则进行抽象和概括性的描述。使用特定的语言为业务流程建模,让其运行在计算机上,并让计算机进行计算和推动。工作流是复杂版本的状态机。
简单状态
上图为工作流退化为基础状态机的例子,路人乙的状态非常简单,站起来->走起来->跑起来->慢下来->站起来,无限循环,如果让我们实现路人乙的状态切换,那么我们只需要用一个字段来记录路人乙当前的状态就好了。
而对于复杂的状态或者状态维度增加且状态流转的条件极为复杂,可能单纯用字段记录状态的实现方式就会不那么理想。如下图:
复杂状态
现在交给路人甲的选择就多了起来,当路人甲获发完工资后的时候,他会根据余额的大小来判断接下来该如何行动,如果数额小于等于5000,那么他决定买一个平板,如果数额小于等于10万,那么路人甲就决定去学习一下购买理财产品,但如果路人甲获得的余额数量超过了30万,他就决定购买一辆宝马,但购买宝马的流程是复杂的,路人甲决定同时完成交首付和贷款的手续。
其实这个流程还不算特别复杂,但到目前为止,单纯用一个字段来表明状态已经无法满足要求了。
工作流解决的痛点在于,解除业务宏观流程和微观逻辑的耦合,让熟悉宏观业务流程的人去制定整套流转逻辑,而让专业的人只需要关心他们应当关心的流程节点,就好比大家要一起修建一座超级体育场,路人甲只需要关心他身边的这一堆砖是怎么堆砌而非整座建筑。
1.3 工作流不能解决的问题
工作流无法解决毫无关系(没有前后关联)的事务。
工作流是一个固定好的框架,大家就按照这个框架来执行流程就行了,但某些情况他本身没有流转顺序的。
比如:路人丙每天需要学习,谈恋爱以及玩游戏,它们之间没有关联性无法建立流程,但可以根据每一项完成的状态决定今天的任务是否完结,这种情况我们需要使用CMMN来建模,它就是专门针对这种情况而设计的,但今天我们不讲这个,而是讲讲BPMN协议。
二.BPMN2.0协议
2.1 什么是BPMN2.0协议
对于业务建模,我们需要一种通用的语言来描绘,这样在沟通上和实现上会降低难度,就像中文、英文一样,BPMN2.0便是一种国际通用的建模语言,他能让自然人轻松阅读,更能被计算机所解析。
2.2 BPMN2.0协议元素介绍
协议中元素的主要分类为,事件-任务-连线-网关。
一个流程必须包含一个事件(如:开始事件)和至少一个结束(事件)。其中网关的作用是流程流转逻辑的控制。任务则分很多类型,他们各司其职,所有节点均由连线联系起来。
下面我就以每种类型的节点简单地概括一下其作用。
- 事件(event)通常用于为流程生命周期中发生的事情建模,图里是【开始、结束】两个圈。
- 顺序流(sequence flow)是流程中两个元素间的连接器。图里是【箭头线段】。
- 网关(gateway)用于控制执行的流向。图里是【菱形(中间有X)】
- 用户任务(user task)用于对需要人工执行的任务进行建模。图里是【矩形】。
**事件(event definition)😗*通常用于为流程生命周期中发生的事情建模。事件总是图形化为圆圈。在BPMN 2.0中,有两种主要的事件分类:*捕获(catching)与抛出(throwing)*事件。
**顺序流(sequence flow)😗*是流程中两个元素间的连接器。在流程执行过程中,一个元素被访问后,会沿着其所有出口顺序流继续执行。这意味着BPMN 2.0的默认是并行执行的:两个出口顺序流就会创建两个独立的、并行的执行路径。
网关:
互斥网关(Exclusive Gateway),又称排他网关,他有且仅有一个有效出口,可以理解为if…else if… else if…else,就和我们平时写代码的一样。
并行网关(Parallel Gateway),他的所有出口都会被执行,可以理解为开多线程同时执行多个任务。
包容性网关(Inclusive Gateway),只要满足条件的出口都会执行,可以理解为 if(…) do, if (…) do, if (…) do,所有的条件判断都是同级别的。
**用户任务(user task)😗*用于对需要人工执行的任务进行建模。当流程执行到达用户任务时,会为指派至该任务的用户或组的任务列表创建一个新任务。
2.3 如何使用BPMN2.0协议
首先,从用户的角度来看,使用者其实只需要关心三件事
-
我如何把我的业务逻辑转化为流程图-即容易理解的绘图工具。
-
我如何使流程流转-即开箱即用的API。
-
我需要引擎告诉我,我现在该处理什么节点-即丰富且鲜明的事件机制。
图中是流程图的整个生命周期,从画图到部署,然后启动流程,流程经过人工或自动的方式流转,最后结束。
三.Flowable简介
3.1 Flowable是什么
Flowable是BPMN2.0协议的一种Java版本的实现,是一个使用Java编写的轻量级业务流程引擎。Flowable流程引擎可用于部署BPMN 2.0流程定义(用于定义流程的行业XML标准), 创建这些流程定义的流程实例,进行查询,访问运行中或历史的流程实例与相关数据,等等。
Flowable可以嵌入Java应用程序中运行,也可以作为服务器、集群运行,更可以提供云服务。
3.2 提供的接口API
flowable提供了7个常见的api,所以只要掌握这些api就可以完成审批业务的开发了
1.FormService 表单数据的管理。
2.RepositoryService 提供了在编辑和发布审批流程的api。主要是模型管理和流程定义的业务api。
3.RuntimeService 处理正在运行的流程。
4.HistoryService 在用户发起审批后,会生成流程实例。historyService为处理流程实例的api,但是其中包括了已经完成的和未完成的流程实例。
5.TaskService 对流程实例的各个节点的审批处理。
6.ManagementService 主要是执行自定义命令。
7.ldentityService 用于身份信息获取和保存,这里主要是获取身份信息。
四.Flowable实战
假设有一个淮师大计科院学生请假需求,需要你设计一个学生请假流程的功能。你该如何设计?
- 我会用flowable来实现
- 创建springboot项目,该项目是启动flowable-ui网站,在本地进行流程画图设计。安装flowable-ui,启动服务进入folwable-ui网站画图,导出图对应的xml文件 (画图)
- 创建springboot项目(flowable),该项目是启动流程应用,将对应的xml文件复制到resources下的processes文件夹中(processes文件夹需自己创建) (部署)
- 启动springboot项目 (启动)
- 通过接口调用,启动流程 (流转)
4.1 搭建项目目录
4.2编写启动类,和相关配置文件
在pom.xml引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- 集中定义依赖版本号 -->
<properties>
<flowable.version>6.7.2</flowable.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--knife4j(swagger) -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.4</version>
</dependency>
<!--swagger-->
<!-- <dependency>-->
<!-- <groupId>io.springfox</groupId>-->
<!-- <artifactId>springfox-swagger2</artifactId>-->
<!-- <version>2.9.2</version>-->
<!-- <!–排除swagger2的annotations和models依赖,然后再引入1.5.21版本的annotations和models依赖–>-->
<!-- <exclusions>-->
<!-- <exclusion>-->
<!-- <groupId>io.swagger</groupId>-->
<!-- <artifactId>swagger-annotations</artifactId>-->
<!-- </exclusion>-->
<!-- <exclusion>-->
<!-- <groupId>io.swagger</groupId>-->
<!-- <artifactId>swagger-models</artifactId>-->
<!-- </exclusion>-->
<!-- </exclusions>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>io.springfox</groupId>-->
<!-- <artifactId>springfox-swagger-ui</artifactId>-->
<!-- <version>2.9.2</version>-->
<!-- </dependency>-->
<!-- <!–引入1.5.21版本的annotations和models依赖–>-->
<!-- <dependency>-->
<!-- <groupId>io.swagger</groupId>-->
<!-- <artifactId>swagger-annotations</artifactId>-->
<!-- <version>1.5.21</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>io.swagger</groupId>-->
<!-- <artifactId>swagger-models</artifactId>-->
<!-- <version>1.5.21</version>-->
<!-- </dependency>-->
<!--flowable工作流依赖-->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>${flowable.version}</version>
</dependency>
<!--idm依赖提供身份认证-->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-ui-idm</artifactId>
<version>${flowable.version}</version>
</dependency>
<!-- modeler绘制流程图 -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-ui-modeler</artifactId>
<version>${flowable.version}</version>
</dependency>
<!-- mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
application.yml文件
server:
port: 8080
flowable:
idm:
app:
admin:
# 登录的用户名
user-id: admin
# 登录的密码
password: admin
# 用户的名字
first-name: w
last-name: j
database-schema-update: true # 没有数据库表的时候生成数据库表 建表后可关闭,下次启动不会再次建表
async-executor-activate: false # 关闭定时任务JOB
spring:
# mysql连接信息
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
#&nullCatalogMeansCurrent=true 这个一定要加在url后,不然flowable自动创建表失败
url: jdbc:mysql://localhost:3306/flowable4?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=Asia/Shanghai&nullCatalogMeansCurrent=true
username: root
password: root
# 默认会启动liquibase, 可关闭
liquibase:
enabled: false
# swagger2:
# enabled: true
# mvc:
# pathmatch:
# matching-strategy: ant_path_matcher
注意:
- &nullCatalogMeansCurrent=true 这个一定要加在url后,不然flowable自动创建表失败
- 在数据库中创建flowable数据库,启动项目时,flowable服务会自动创建对应的表
- 启动项目后 网站的登录用户 密码 user-id: admin password: admin
配置类
防止流程图乱码FlowableConfig
package com.igeek.boot.config;
import org.flowable.spring.SpringProcessEngineConfiguration;
import org.flowable.spring.boot.EngineConfigurationConfigurer;
import org.springframework.context.annotation.Configuration;
/**
*防止流程图乱码配置
*/
@Configuration
public class FlowableConfig implements EngineConfigurationConfigurer<SpringProcessEngineConfiguration> {
@Override
public void configure(SpringProcessEngineConfiguration engineConfiguration) {
engineConfiguration.setActivityFontName("宋体");
engineConfiguration.setLabelFontName("宋体");
engineConfiguration.setAnnotationFontName("宋体");
}
}
返回结果Result
package com.igeek.boot.config;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* TODO
* @since 2023/12/16
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private boolean flag;
private String message;
private Object data;
public Result(boolean flag , String message){
this.flag = flag;
this.message = message;
}
}
swagger2测试
package com.igeek.boot.config;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @Description Swagger配置类
* @Author chenmin
* @Version 1.0
* @Date 2023/3/15 15:17
*
* http://localhost:8080/swagger-ui.html
*/
@Configuration
//开启Swagger2
@EnableSwagger2
public class SwaggerConfig {
@Value("${spring.swagger2.enabled}")
private Boolean enabled;
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(enabled)
.select()
.apis(RequestHandlerSelectors.basePackage("com.igeek.boot"))
.paths(PathSelectors.any())
.build();
}
//Swagger文档信息
public ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("springboot利用swagger2构建api接口文档 "+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
.description("有问题给管理员邮件:[email protected]")
.version("1.0")
.termsOfServiceUrl("https://www.igeekhome.com/")
.build();
}
}
TaskVO封装审批列表查询结果:
package com.igeek.boot.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 2023/12/26 1:27
* @description 审批列表查询结果
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TaskVO {
private String id;
private String day;
private String name;
}
controller请假流程接口:
package com.igeek.boot.controller;
import com.igeek.boot.config.Result;
import com.igeek.boot.vo.TaskVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.*;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.image.ProcessDiagramGenerator;
import org.flowable.task.api.Task;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/leave")
@Slf4j
@Api(tags = "请假流程接口")
public class LeaveController {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private RepositoryService repositoryService;
@Autowired
private ProcessEngine processEngine;
@ApiOperation("学生提交请假申请")
@PostMapping( "add/{day}/{studentUser}")
public Result sub(@ApiParam("请假天数") @PathVariable("day") Integer day , @ApiParam("学生用户名")@PathVariable("studentUser") String studentUser) {
// 学生提交请假申请
Map<String, Object> map = new HashMap<>();
map.put("day", day);
map.put("studentName", studentUser);
// leave为学生请假流程xml文件中的id
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("go", map);
log.info("流程实例ID:" + processInstance.getId());
//完成申请任务
Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
taskService.complete(task.getId());
return new Result(true,"提交成功.流程Id为:" + processInstance.getId()) ;
}
@ApiOperation("辅导员获取审批管理列表")
@GetMapping("teacherList")
public Result teacherList() {
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("a").list();
List<TaskVO> taskVOList = tasks.stream().map(task -> {
Map<String, Object> variables = taskService.getVariables(task.getId());
return new TaskVO(task.getId(), variables.get("day").toString(), variables.get("studentName").toString());
}).collect(Collectors.toList());
log.info("任务列表:" + tasks);
if (tasks == null || tasks.size() == 0) {
return new Result(false,"没有任务");
}
return new Result(true,"获取成功",taskVOList);
}
/**
* 辅导员批准
*
* @param taskId 任务ID
*/
@ApiOperation("辅导员批准")
@GetMapping("teacherApply/{taskId}")
public Result teacherApply(@ApiParam("任务id")@PathVariable("taskId") String taskId) {
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("a").list();
//Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
if (tasks == null) {
return new Result(false, "没有任务");
}
//通过审核
HashMap<String, Object> map = new HashMap<>();
map.put("outcome", "通过");
for (Task task : tasks) {
taskService.complete(task.getId(), map);
}
return new Result(true,"审批成功");
}
/**
* 辅导员拒绝
*/
@ApiOperation("辅导员拒绝")
@GetMapping( "teacherReject/{taskId}")
public Result teacherReject(@ApiParam("任务id")@PathVariable("taskId") String taskId) {
Task task1 = taskService.createTaskQuery().taskCandidateGroup("a").taskId(taskId).singleResult();
//Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
if (task1 == null) {
return new Result(false, "没有任务");
}
//通过审核
HashMap<String, Object> map = new HashMap<>();
map.put("outcome", "驳回");
taskService.complete(task1.getId(), map);
return new Result(true,"审批失败");
}
/**
* 院长获取审批管理列表
*/
@ApiOperation("院长获取审批管理列表")
@GetMapping("deanList")
public Result deanList() {
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("b").list();
List<TaskVO> taskVOList = tasks.stream().map(task -> {
Map<String, Object> variables = taskService.getVariables(task.getId());
return new TaskVO(task.getId(), variables.get("day").toString(), variables.get("studentName").toString());
}).collect(Collectors.toList());
if (tasks == null || tasks.size() == 0) {
return new Result(false,"没有任务");
}
return new Result(true,"获取成功",taskVOList);
}
/**
* 院长批准
* @param taskId
* @return
*/
@ApiOperation("院长批准")
@GetMapping("deanApply/{taskId}")
public Result apply(@ApiParam("任务id")@PathVariable("taskId") String taskId) {
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("b").list();
//Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
if (tasks == null) {
return new Result(false, "没有任务");
}
//通过审核
HashMap<String, Object> map = new HashMap<>();
map.put("outcome", "通过");
for (Task task : tasks) {
taskService.complete(task.getId(), map);
}
return new Result(true,"审批成功");
}
/**
* 院长拒绝
* @param taskId
* @return
*/
@ApiOperation("院长拒绝")
@GetMapping("deanReject/{taskId}")
public Result deanReject(@ApiParam("任务id")@PathVariable("taskId") String taskId) {
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("b").list();
//Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
if (tasks == null) {
return new Result(false, "没有任务");
}
//通过审核
HashMap<String, Object> map = new HashMap<>();
map.put("outcome", "通过");
for (Task task : tasks) {
taskService.complete(task.getId(), map);
}
return new Result(true,"审批成功");
}
/**
* 生成流程图
*
* @param taskId 任务ID
*/
@ApiOperation("生成流程图")
@GetMapping( "processDiagram/{taskId}")
public void genProcessDiagram(HttpServletResponse httpServletResponse,@ApiParam("任务id")@PathVariable("taskId") String taskId) throws Exception {
ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(taskId).singleResult();
//流程走完的不显示图
if (pi == null) {
return;
}
Task task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
//使用流程实例ID,查询正在执行的执行对象表,返回流程实例对象
String InstanceId = task.getProcessInstanceId();
List<Execution> executions = runtimeService
.createExecutionQuery()
.processInstanceId(InstanceId)
.list();
//得到正在执行的Activity的Id
List<String> activityIds = new ArrayList<>();
List<String> flows = new ArrayList<>();
for (Execution exe : executions) {
List<String> ids = runtimeService.getActiveActivityIds(exe.getId());
activityIds.addAll(ids);
}
//获取流程图
BpmnModel bpmnModel = repositoryService.getBpmnModel(pi.getProcessDefinitionId());
ProcessEngineConfiguration engconf = processEngine.getProcessEngineConfiguration();
ProcessDiagramGenerator diagramGenerator = engconf.getProcessDiagramGenerator();
InputStream in = diagramGenerator.generateDiagram(bpmnModel, "png", activityIds, flows, engconf.getActivityFontName(), engconf.getLabelFontName(), engconf.getAnnotationFontName(), engconf.getClassLoader(), 1.0 ,false);
OutputStream out = null;
byte[] buf = new byte[1024];
int legth = 0;
try {
out = httpServletResponse.getOutputStream();
while ((legth = in.read(buf)) != -1) {
out.write(buf, 0, legth);
}
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}
}
## 插件安装
BPMN绘图可视化工具
> Flowable BPMN visualizer
4.3画图接口
引入相关依赖
<!--idm依赖提供身份认证-->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-ui-idm</artifactId>
<version>${flowable.version}</version>
</dependency>
<!-- modeler绘制流程图 -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-ui-modeler</artifactId>
<version>${flowable.version}</version>
</dependency>
配置yml文件:
flowable:
idm:
app:
admin:
# 登录的用户名
user-id: admin
# 登录的密码
password: admin
# 用户的名字
first-name: w
last-name: j
启动项目
@SpringBootApplication
public class BootFlowableApplication {
public static void main(String[] args) {
SpringApplication.run(BootFlowableApplication.class, args);
}
}
访问http://localhost:8080/
这个时候就可以开始设计员工请假流程图 我先画一个完整的出来 将每个步骤对应的信息都截下来 供大家参考
学生请假流程.bpmn20.xml 在resources下创建processes文件夹 复制xml文件到该目录即可
安装Flowable BPMN visualize 插件后会减少xml文件的很多爆红,但爆红不影响对xml文件的读取
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:flowable="http://flowable.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.flowable.org/processdef" exporter="Flowable Open Source Modeler" exporterVersion="6.7.1">
<process id="go" name="学生请假流程" isExecutable="true">
<documentation>学生请假流程</documentation>
<startEvent id="start" name="开始" flowable:formFieldValidation="true"></startEvent>
<userTask id="apply" name="请假申请" flowable:assignee="${studentName}" flowable:formFieldValidation="true">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<userTask id="teacherPass" name="辅导员审批" flowable:candidateGroups="a" flowable:formFieldValidation="true"></userTask>
<exclusiveGateway id="judgeTask" name="判断是否大于2天"></exclusiveGateway>
<endEvent id="over" name="结束"></endEvent>
<userTask id="bossPass" name="院长审批" flowable:candidateGroups="b" flowable:formFieldValidation="true"></userTask>
<sequenceFlow id="sid-7D98C541-9EF4-46A3-81B5-3AE06CE17F05" name="申请流程" sourceRef="apply" targetRef="teacherPass"></sequenceFlow>
<sequenceFlow id="sid-60427D00-F713-4C79-9918-837BE087D8D3" name="流程开始" sourceRef="start" targetRef="apply"></sequenceFlow>
<sequenceFlow id="judgeLess" name="小于2天" sourceRef="judgeTask" targetRef="over">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${day<=2}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="teacherNotPassFlow" name="驳回" sourceRef="teacherPass" targetRef="apply">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${outcome=="驳回"}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="bossPassFlow" name="通过" sourceRef="bossPass" targetRef="over">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${outcome=="通过"}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="judgeMore" name="大于2天" sourceRef="judgeTask" targetRef="bossPass">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${day>2}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="teacherPassFlow" name="通过" sourceRef="teacherPass" targetRef="judgeTask">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${outcome=="通过"}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="bossNotPassFlow" name="驳回" sourceRef="bossPass" targetRef="apply">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${outcome=="驳回"}]]></conditionExpression>
</sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_go">
<bpmndi:BPMNPlane bpmnElement="go" id="BPMNPlane_go">
<bpmndi:BPMNShape bpmnElement="start" id="BPMNShape_start">
<omgdc:Bounds height="30.0" width="30.0" x="134.99999798834327" y="134.99999798834327"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="apply" id="BPMNShape_apply">
<omgdc:Bounds height="80.0" width="100.0" x="314.9999953061343" y="109.99999471008795"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="teacherPass" id="BPMNShape_teacherPass">
<omgdc:Bounds height="80.0" width="100.0" x="539.9999919533731" y="109.99999634921559"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="judgeTask" id="BPMNShape_judgeTask">
<omgdc:Bounds height="40.0" width="40.0" x="809.9999758601194" y="129.99999605119237"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="over" id="BPMNShape_over">
<omgdc:Bounds height="28.0" width="28.0" x="1019.9999848008158" y="135.9999959617854"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="bossPass" id="BPMNShape_bossPass">
<omgdc:Bounds height="80.0" width="100.0" x="779.9999646842491" y="314.9999953061343"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="sid-60427D00-F713-4C79-9918-837BE087D8D3" id="BPMNEdge_sid-60427D00-F713-4C79-9918-837BE087D8D3" flowable:sourceDockerX="15.0" flowable:sourceDockerY="15.0" flowable:targetDockerX="1.0" flowable:targetDockerY="40.0">
<omgdi:waypoint x="164.94999732515438" y="149.99999769211536"></omgdi:waypoint>
<omgdi:waypoint x="314.999994585362" y="149.99999472884903"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-7D98C541-9EF4-46A3-81B5-3AE06CE17F05" id="BPMNEdge_sid-7D98C541-9EF4-46A3-81B5-3AE06CE17F05" flowable:sourceDockerX="50.0" flowable:sourceDockerY="40.0" flowable:targetDockerX="50.0" flowable:targetDockerY="40.0">
<omgdi:waypoint x="414.94999530610096" y="149.9999950739743"></omgdi:waypoint>
<omgdi:waypoint x="539.9999896640127" y="149.99999598496498"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="judgeLess" id="BPMNEdge_judgeLess" flowable:sourceDockerX="20.5" flowable:sourceDockerY="20.5" flowable:targetDockerX="14.0" flowable:targetDockerY="14.0">
<omgdi:waypoint x="849.491962572681" y="150.45319801530812"></omgdi:waypoint>
<omgdi:waypoint x="1020.0000230239541" y="150.03427117012842"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="bossPassFlow" id="BPMNEdge_bossPassFlow" flowable:sourceDockerX="50.0" flowable:sourceDockerY="40.0" flowable:targetDockerX="14.0" flowable:targetDockerY="14.0">
<omgdi:waypoint x="879.9499646841429" y="354.9999953061343"></omgdi:waypoint>
<omgdi:waypoint x="1033.9999848008158" y="354.9999953061343"></omgdi:waypoint>
<omgdi:waypoint x="1033.9999848008158" y="163.94991974938068"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="judgeMore" id="BPMNEdge_judgeMore" flowable:sourceDockerX="20.5" flowable:sourceDockerY="20.5" flowable:targetDockerX="50.0" flowable:targetDockerY="40.0">
<omgdi:waypoint x="830.4534061890572" y="169.4917780809857"></omgdi:waypoint>
<omgdi:waypoint x="830.0976441294755" y="314.9999953061343"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="teacherNotPassFlow" id="BPMNEdge_teacherNotPassFlow" flowable:sourceDockerX="50.0" flowable:sourceDockerY="40.0" flowable:targetDockerX="50.0" flowable:targetDockerY="40.0">
<omgdi:waypoint x="589.9999919533731" y="109.99999634921559"></omgdi:waypoint>
<omgdi:waypoint x="589.9999919533731" y="45.000001534819575"></omgdi:waypoint>
<omgdi:waypoint x="364.9999953061343" y="45.000001534819575"></omgdi:waypoint>
<omgdi:waypoint x="364.9999953061343" y="109.99999471008795"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="bossNotPassFlow" id="BPMNEdge_bossNotPassFlow" flowable:sourceDockerX="50.0" flowable:sourceDockerY="40.0" flowable:targetDockerX="50.0" flowable:targetDockerY="40.0">
<omgdi:waypoint x="779.9999646842491" y="354.9999953061343"></omgdi:waypoint>
<omgdi:waypoint x="364.9999953061343" y="354.9999953061343"></omgdi:waypoint>
<omgdi:waypoint x="364.9999953061343" y="189.94999471008794"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="teacherPassFlow" id="BPMNEdge_teacherPassFlow" flowable:sourceDockerX="50.0" flowable:sourceDockerY="40.0" flowable:targetDockerX="20.5" flowable:targetDockerY="20.5">
<omgdi:waypoint x="639.9499919533686" y="150.10384244811357"></omgdi:waypoint>
<omgdi:waypoint x="810.4583092155458" y="150.45832940661876"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
4.4启动流程代码详解
流程实例
-
现在已经在流程引擎中部署了流程定义,因此可以使用这个流程定义作为“蓝图”启动流程实例。
-
我们使用RuntimeService启动一个流程实例。
-
收集的数据作为一个java.util.Map实例传递,其中的键就是之后用于获取变量的标识符。
-
这个流程实例使用key启动。这个key就是BPMN 2.0 XML文件中设置的id属性。
-
在这个例子里是go。
-
表达式分配
Flowable使用UEL进行表达式解析。UEL代表Unified Expression Language,是EE6规范的一部分.Flowable支持两种UEL表达式: UEL-value 和UEL-method
这两个就是在XML文件中定义的变量
flowable:assignee=“${studentName}”
${outcome==‘通过’}
${day <= 2}
对应的BPMN 2.0 XML属性,我们通过id启动流程
<process id="go" name="学生请假流程" isExecutable="true">
学生对应的BPMN 2.0 XML属性 通过assignee学生可以发起请假流程
<userTask id="apply" name="请假申请" flowable:assignee="${studentName}" flowable:formFieldValidation="true">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
辅导员对应的BPMN 2.0 XML属性 通过candidateGroups可以查询辅导员下可以审批的任务
<userTask id="teacherPass" name="辅导员审批 " flowable:candidateGroups="a" flowable:formFieldValidation="true"></userTask>
院长对应的BPMN 2.0 XML属性 通过candidateGroups可以查询院长下可以审批的任务
<userTask id="principalPass" name="院长审批" flowable:candidateGroups="b" flowable:formFieldValidation="true"></userTask>
学生发起请假流程代码:
@ApiOperation("学生提交请假申请")
@PostMapping( "add/{day}/{studentUser}")
public Result sub(@ApiParam("请假天数") @PathVariable("day") Integer day , @ApiParam("学生用户名")@PathVariable("studentUser") String studentUser) {
// 学生提交请假申请
Map<String, Object> map = new HashMap<>();
map.put("day", day);
map.put("studentName", studentUser);
// leave为学生请假流程xml文件中的id
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("go", map);
log.info("流程实例ID:" + processInstance.getId());
//完成申请任务
Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
taskService.complete(task.getId());
return new Result(true,"提交成功.流程Id为:" + processInstance.getId()) ;
}
查询辅导员审批任务代码:
@ApiOperation("辅导员获取审批管理列表")
@GetMapping("teacherList")
public Result teacherList() {
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("a").list();
List<TaskVO> taskVOList = tasks.stream().map(task -> {
Map<String, Object> variables = taskService.getVariables(task.getId());
return new TaskVO(task.getId(), variables.get("day").toString(), variables.get("studentName").toString());
}).collect(Collectors.toList());
log.info("任务列表:" + tasks);
if (tasks == null || tasks.size() == 0) {
return new Result(false,"没有任务");
}
return new Result(true,"获取成功",taskVOList);
}
查询院长审批任务代码:
@ApiOperation("院长获取审批管理列表")
@GetMapping("deanList")
public Result deanList() {
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("b").list();
List<TaskVO> taskVOList = tasks.stream().map(task -> {
Map<String, Object> variables = taskService.getVariables(task.getId());
return new TaskVO(task.getId(), variables.get("day").toString(), variables.get("studentName").toString());
}).collect(Collectors.toList());
if (tasks == null || tasks.size() == 0) {
return new Result(false,"没有任务");
}
return new Result(true,"获取成功",taskVOList);
}
辅导员进行审批任务代码(通过或驳回):
@ApiOperation("辅导员批准")
@GetMapping("teacherApply/{taskId}")
public Result teacherApply(@ApiParam("任务id")@PathVariable("taskId") String taskId) {
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("a").list();
if (tasks == null) {
return new Result(false, "没有任务");
}
//通过审核
HashMap<String, Object> map = new HashMap<>();
map.put("outcome", "通过");
for (Task task : tasks) {
taskService.complete(task.getId(), map);
}
return new Result(true,"审批成功");
}
院长进行审批任务代码(通过或驳回):
@ApiOperation("院长批准")
@GetMapping("deanApply/{taskId}")
public Result apply(@ApiParam("任务id")@PathVariable("taskId") String taskId) {
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("b").list();
if (tasks == null) {
return new Result(false, "没有任务");
}
//通过审核
HashMap<String, Object> map = new HashMap<>();
map.put("outcome", "通过");
for (Task task : tasks) {
taskService.complete(task.getId(), map);
}
return new Result(true,"审批成功");
}
查询流程进度图
@ApiOperation("生成流程图")
@GetMapping( "processDiagram/{taskId}")
public void genProcessDiagram(HttpServletResponse httpServletResponse,@ApiParam("任务id")@PathVariable("taskId") String taskId) throws Exception {
ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(taskId).singleResult();
//流程走完的不显示图
if (pi == null) {
return;
}
Task task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
//使用流程实例ID,查询正在执行的执行对象表,返回流程实例对象
String InstanceId = task.getProcessInstanceId();
List<Execution> executions = runtimeService
.createExecutionQuery()
.processInstanceId(InstanceId)
.list();
//得到正在执行的Activity的Id
List<String> activityIds = new ArrayList<>();
List<String> flows = new ArrayList<>();
for (Execution exe : executions) {
List<String> ids = runtimeService.getActiveActivityIds(exe.getId());
activityIds.addAll(ids);
}
//获取流程图
BpmnModel bpmnModel = repositoryService.getBpmnModel(pi.getProcessDefinitionId());
ProcessEngineConfiguration engconf = processEngine.getProcessEngineConfiguration();
ProcessDiagramGenerator diagramGenerator = engconf.getProcessDiagramGenerator();
InputStream in = diagramGenerator.generateDiagram(bpmnModel, "png", activityIds, flows, engconf.getActivityFontName(), engconf.getLabelFontName(), engconf.getAnnotationFontName(), engconf.getClassLoader(), 1.0 ,false);
OutputStream out = null;
byte[] buf = new byte[1024];
int legth = 0;
try {
out = httpServletResponse.getOutputStream();
while ((legth = in.read(buf)) != -1) {
out.write(buf, 0, legth);
}
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}
五.表结构讲解
工作流程的相关操作都是操作存储在对应的表结构中,为了能更好的弄清楚Flowable的实现原理和细节,我们有必要先弄清楚Flowable的相关表结构及其作用。在Flowable中的表结构在初始化的时候会创建五类表结构,具体如下:
- ACT_RE :'RE’表示 repository。 这个前缀的表包含了流程定义和流程静态资源 (图片,规则,等等)。
- ACT_RU:'RU’表示 runtime。 这些运行时的表,包含流程实例,任务,变量,异步任务,等运行中的数据。 Flowable只在流程实例执行过程中保存这些数据, 在流程结束时就会删除这些记录。 这样运行时表可以一直很小速度很快。
- ACT_HI:'HI’表示 history。 这些表包含历史数据,比如历史流程实例, 变量,任务等等。
- ACT_GE: GE 表示 general。 通用数据, 用于不同场景下
- ACT_ID: ’ID’表示identity(组织机构)。这些表包含标识的信息,如用户,用户组,等等。
具体的表结构的含义:
表分类 | 表名 | 解释 |
---|---|---|
一般数据 | ||
[ACT_GE_BYTEARRAY] | 通用的流程定义和流程资源 | |
[ACT_GE_PROPERTY] | 系统相关属性 | |
流程历史记录 | ||
[ACT_HI_ACTINST] | 历史的流程实例 | |
[ACT_HI_ATTACHMENT] | 历史的流程附件 | |
[ACT_HI_COMMENT] | 历史的说明性信息 | |
[ACT_HI_DETAIL] | 历史的流程运行中的细节信息 | |
[ACT_HI_IDENTITYLINK] | 历史的流程运行过程中用户关系 | |
[ACT_HI_PROCINST] | 历史的流程实例 | |
[ACT_HI_TASKINST] | 历史的任务实例 | |
[ACT_HI_VARINST] | 历史的流程运行中的变量信息 | |
流程定义表 | ||
[ACT_RE_DEPLOYMENT] | 部署单元信息 | |
[ACT_RE_MODEL] | 模型信息 | |
[ACT_RE_PROCDEF] | 已部署的流程定义 | |
运行实例表 | ||
[ACT_RU_EVENT_SUBSCR] | 运行时事件 | |
[ACT_RU_EXECUTION] | 运行时流程执行实例 | |
[ACT_RU_IDENTITYLINK] | 运行时用户关系信息,存储任务节点与参与者的相关信息 | |
[ACT_RU_JOB] | 运行时作业 | |
[ACT_RU_TASK] | 运行时任务 | |
[ACT_RU_VARIABLE] | 运行时变量表 | |
用户组表 | ||
[ACT_ID_BYTEARRAY] | 二进制数据表 | |
[ACT_ID_GROUP] | 用户组信息表 | |
[ACT_ID_INFO] | 用户信息详情表 | |
[ACT_ID_MEMBERSHIP] | 人与组关系表 | |
[ACT_ID_PRIV] | 权限表 | |
[ACT_ID_PRIV_MAPPING] | 用户或组权限关系表 | |
[ACT_ID_PROPERTY] | 属性表 | |
[ACT_ID_TOKEN] | 记录用户的token信息 | |
[ACT_ID_USER] | 用户表 |