工作流程引擎之flowable(集成springboot)
0、背景
现状:公司各部门业务系统有各自的工作流引擎,也有cross function的业务在不同系统或OA系统流转,没有统一的去规划布局统一的BPM解决方案,近期由于一个项目引发朝着整合统一的BPM方案,特了解一下市面上比较主流的开源和收费的工作流引擎。本文主要介绍开源的工作流引擎flowable.
1、开源工作流引擎比较
开源工作流引擎是一种用于管理和自动化业务流程的软件,它可以帮助用户实现业务流程的可视化设计、流程编排、任务调度、监控和优化等功能。本文将介绍几种常见的开源工作流引擎,并进行比较。目前市场上比较主流的开源流程引擎有:Activiti、Camunda、Flowable。
1.1、Activiti
Activiti是一个轻量级的开源工作流引擎,采用Java语言开发,基于BPMN 2.0规范,支持嵌入式部署和分布式部署。Activiti提供了丰富的API和插件,支持与Java应用程序进行集成。它还提供了Web界面和REST API,可以方便地进行流程设计、部署、调度和监控。Activiti具有以下优点:
- 易用性和灵活性:Activiti提供了简单易用的流程设计器和API,支持多种流程模型和任务类型,可以满足不同场景和需求的使用。
- 可伸缩性和性能:Activiti支持嵌入式和分布式部署,可以扩展集群规模以支持更大的业务流程和更高的并发量。此外,它还提供了优化和缓存机制,可以提高性能和响应速度。
- 社区支持和生态系统:Activiti拥有庞大的社区和活跃的开发者,提供了丰富的插件和工具,可以扩展其功能和使用。例如,Activiti Explorer可以用于流程设计和管理,Camunda Modeler可以用于编辑BPMN模型,Flowable Task可以用于任务管理等。
- 安全性和可靠性:Activiti提供了可靠的安全性控制,可以对数据进行访问控制和加密,以满足不同场景和需求的安全要求。它还提供了事务管理和错误处理机制,可以保证业务流程的可靠性和稳定性。
1.2、Camunda
Camunda是一个强大的开源工作流引擎,采用Java语言开发,支持BPMN 2.0规范和CMMN规范。Camunda提供了丰富的API和插件,支持与Java应用程序进行集成。它还提供了Web界面和REST API,可以方便地进行流程设计、部署、调度和监控。Camunda具有以下优点:
- 功能丰富和灵活性:Camunda提供了丰富的功能和灵活的流程设计,支持多种流程模型和任务类型,可以满足不同场景和需求的使用。
- 可伸缩性和性能:Camunda支持嵌入式和分布式部署,可以扩展集群规模以支持更大的业务流程和更高的并发量。此外,它还提供了优化和缓存机制,可以提高性能和响应速度。
- 社区支持和生态系统:Camunda拥有庞大的社区和活跃的开发者,提供了丰富的插件和工具,可以扩展其功能和使用。例如,Camunda Modeler可以用于编辑BPMN模型,Camunda Cockpit可以用于任务管理和流程监控,Camunda Tasklist可以用于任务列表等。
- 可扩展的架构和API:Camunda采用可扩展的架构和API,可以方便地进行集成和扩展。例如,它支持自定义流程引擎插件、外部任务处理器、表单引擎等。
- 安全性和可靠性:Camunda提供了可靠的安全性控制,可以对数据进行访问控制和加密,以满足不同场景和需求的安全要求。它还提供了事务管理和错误处理机制,可以保证业务流程的可靠性和稳定性。
1.3、Flowable
Flowable是一个开源的轻量级工作流引擎,基于Activiti 5.x版本开发,支持BPMN 2.0规范和CMMN规范。Flowable提供了丰富的API和插件,支持与Java应用程序进行集成。它还提供了Web界面和REST API,可以方便地进行流程设计、部署、调度和监控。Flowable具有以下优点:
- 易用性和灵活性:Flowable提供了简单易用的流程设计器和API,支持多种流程模型和任务类型,可以满足不同场景和需求的使用。
- 可伸缩性和性能:Flowable支持嵌入式和分布式部署,可以扩展集群规模以支持更大的业务流程和更高的并发量。此外,它还提供了优化和缓存机制,可以提高性能和响应速度。
- 社区支持和生态系统:Flowable拥有庞大的社区和活跃的开发者,提供了丰富的插件和工具,可以扩展其功能和使用。例如,Flowable Modeler可以用于流程设计和管理,Flowable Task可以用于任务管理和流程监控,Flowable Admin可以用于集群管理等。
- 可扩展的架构和API:Flowable采用可扩展的架构和API,可以方便地进行集成和扩展。例如,它支持自定义流程引擎插件、外部任务处理器、表单引擎等。
- 安全性和可靠性:Flowable提供了可靠的安全性控制,可以对数据进行访问控制和加密,以满足不同场景和需求的安全要求。它还提供了事务管理和错误处理机制,可以保证业务流程的可靠性和稳定性。
2、flowable安装
2.1、下载软件
Github链接,目前flowable已经更新到Flowable 7.0.0.M2版本了。

安装方式有很多,可以通过下载zip包也可以通过docker拉取镜像进行部署,本文采用zip的window环境部署。
下载安装包,我下载的是flowable-6.7.0版本,同时需要配合tomcat(选择的是apache-tomcat-9.0.79版本)服务进行部署,tomcat下载地址:


 
下载上面两个软件之后还需要配置flowale的配置数据存储,我选择的mysql数据库,当然他还支持其他数据库类型。mysql数据库是docker镜像运行的,这里不再赘述安装过程,可以看我往期docker mysql安装.
2.2、解压运行
分别解压tomcat和flowable:

 
把flowable-6.7.0\wars目录下的flowable-ui.war拷贝到apache-tomcat-9.0.79\webapps下面启动tomcat startup.bat
 此时会解压war文件,同时会删除war文件,同时会生成新文件apache-tomcat-9.0.79\webapps\flowable-ui.
此时会解压war文件,同时会删除war文件,同时会生成新文件apache-tomcat-9.0.79\webapps\flowable-ui.
2.3、配置数据源
修改apache-tomcat-9.0.79\webapps\flowable-ui\WEB-INF\classes\flowable-default.properties

参考:
server.port=8080
server.servlet.context-path=/flowable-ui
spring.jmx.unique-names=true
# This is needed to force use of JDK proxies instead of using CGLIB
spring.aop.proxy-target-class=false
spring.aop.auto=false
spring.application.name=flowable-ui
spring.banner.location=classpath:/org/flowable/spring/boot/flowable-banner.txt
# The default domain for generating ObjectNames must be specified. Otherwise when multiple Spring Boot applications start in the same servlet container
# all would be created with the same name (com.zaxxer.hikari:name=dataSource,type=HikariDataSource) for example
spring.jmx.default-domain=${spring.application.name}
#
# SECURITY
#
spring.security.filter.dispatcher-types=REQUEST,FORWARD,ASYNC# Expose all actuator endpoints to the web
# They are exposed, but only authenticated users can see /info and /health abd users with access-admin can see the others
management.endpoints.web.exposure.include=*
# Full health details should only be displayed when a user is authorized
management.endpoint.health.show-details=when_authorized
# Only users with role access-admin can access full health details
management.endpoint.health.roles=access-admin
# Spring prefixes the roles with ROLE_. However, Flowable does not have that concept yet, so we need to override that with an empty string
flowable.common.app.role-prefix=#
# SECURITY OAuth2
# Examples are for Keycloak
#
#spring.security.oauth2.resourceserver.jwt.issuer-uri=<keycloakLocation>/auth/realms/<realmName>
#spring.security.oauth2.client.registration.keycloak.client-id=<clientId>
#spring.security.oauth2.client.registration.keycloak.client-secret=<clientSecret>
#spring.security.oauth2.client.registration.keycloak.client-name=Flowable UI Keycloak
#spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
#spring.security.oauth2.client.provider.keycloak.issuer-uri=<keycloakLocation>/auth/realms/<realmName>
#spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username#flowable.common.app.security.type=oauth2
#flowable.common.app.security.oauth2.authorities-attribute=groups
#flowable.common.app.security.oauth2.groups-attribute=userGroups
#flowable.common.app.security.oauth2.default-authorities=access-task
#flowable.common.app.security.oauth2.default-groups=flowableUser
#flowable.common.app.security.oauth2.full-name-attribute=name
#flowable.common.app.security.oauth2.email-attribute=email#
# DATABASE
##spring.datasource.driver-class-name=org.h2.Driver
#spring.datasource.url=jdbc:h2:~/flowable-db/engine-db;AUTO_SERVER=TRUE;AUTO_SERVER_PORT=9093;DB_CLOSE_DELAY=-1spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.56.100:13306/flowable?characterEncoding=UTF-8#spring.datasource.driver-class-name=org.postgresql.Driver
#spring.datasource.url=jdbc:postgresql://localhost:5432/flowable#spring.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver
#spring.datasource.url=jdbc:sqlserver://localhost:1433;databaseName=flowablea#spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
#spring.datasource.url=jdbc:oracle:thin:@localhost:1521:FLOWABLE#spring.datasource.driver-class-name=com.ibm.db2.jcc.DB2Driver
#spring.datasource.url=jdbc:db2://localhost:50000/flowablespring.datasource.username=root
spring.datasource.password=my-secret-pw# JNDI CONFIG# If uncommented, the datasource will be looked up using the configured JNDI name.
# This will have preference over any datasource configuration done below that doesn't use JNDI
#
# Eg for JBoss: java:jboss/datasources/flowableDS
#
#spring.datasource.jndi-name==jdbc/flowableDS# Set whether the lookup occurs in a J2EE container, i.e. if the prefix "java:comp/env/" needs to be added if the JNDI
# name doesn't already contain it. Default is "true".
#datasource.jndi.resourceRef=true#
# Connection pool (see https://github.com/brettwooldridge/HikariCP#configuration-knobs-baby)
#spring.datasource.hikari.poolName=${spring.application.name}
# 10 minutes
spring.datasource.hikari.maxLifetime=600000
# 5 minutes
spring.datasource.hikari.idleTimeout=300000
spring.datasource.hikari.minimumIdle=10
spring.datasource.hikari.maximumPoolSize=50
# test query for H2, MySQL, PostgreSQL and Microsoft SQL Server
#spring.datasource.hikari.connection-test-query=select 1
# test query for Oracle
#spring.datasource.hikari.connection-test-query=SELECT 1 FROM DUAL
# test query for DB2
#spring.datasource.hikari.connection-test-query=SELECT current date FROM sysibm.sysdummy1#
# Default Task Executor (will be used for @Async)
#
spring.task.execution.pool.core-size=2
spring.task.execution.pool.max-size=50
spring.task.execution.pool.queue-capacity=10000
spring.task.execution.thread-name-prefix=flowable-ui-task-Executor-#
# Task scheduling
#
spring.task.scheduling.pool.size=5#
# EMAIL
##flowable.mail.server.host=localhost
#flowable.mail.server.port=1025
#flowable.mail.server.username=
#flowable.mail.server.password=#
# FLOWABLE
#flowable.process.definition-cache-limit=512
#flowable.dmn.strict-mode=false
flowable.process.async.executor.default-async-job-acquire-wait-time=PT5S
flowable.process.async.executor.default-timer-job-acquire-wait-time=PT5Sflowable.cmmn.async.executor.default-async-job-acquire-wait-time=PT5S
flowable.cmmn.async.executor.default-timer-job-acquire-wait-time=PT5S# The maximum file upload limit. Set to -1 to set to 'no limit'. Expressed in bytes
spring.servlet.multipart.max-file-size=10MB
# The maximum request size limit. Set to -1 to set to 'no limit'.
# When multiple files can be uploaded this needs to be more than the 'max-file-size'.
spring.servlet.multipart.max-request-size=10MB# For development purposes, data folder is created inside the sources ./data folder
flowable.content.storage.root-folder=data/
flowable.content.storage.create-root=trueflowable.common.app.idm-admin.user=admin
flowable.common.app.idm-admin.password=testflowable.experimental.debugger.enabled=false# Rest API in task application# If false, disables the rest api in the task app
flowable.task.app.rest-enabled=true# Configures the way user credentials are verified when doing a REST API call:
# 'any-user' : the user needs to exist and the password need to match. Any user is allowed to do the call (this is the pre 6.3.0 behavior)
# 'verify-privilege' : the user needs to exist, the password needs to match and the user needs to have the 'rest-api' privilege
# If nothing set, defaults to 'verify-privilege'
flowable.rest.app.authentication-mode=verify-privilege# Enable form field validation after form submission on the engine side
flowable.form-field-validation-enabled=false# Flowable Admin Properties# Passwords for rest endpoints and master configs are stored encrypted in the database using AES/CBC/PKCS5PADDING
# It needs a 128-bit initialization vector (http://en.wikipedia.org/wiki/Initialization_vector)
# and a 128-bit secret key represented as 16 ascii characters below
#
# Do note that if these properties are changed after passwords have been saved, all existing passwords
# will not be able to be decrypted and the password would need to be reset in the UI.
flowable.admin.app.security.encryption.credentials-i-v-spec=j8kdO2hejA9lKmm6
flowable.admin.app.security.encryption.credentials-secret-spec=9FGl73ngxcOoJvmL
#flowable.admin.app.security.preemptive-basic-authentication=true# Flowable IDM Properties#
# LDAP
#
#flowable.idm.ldap.enabled=true
#flowable.idm.ldap.server=ldap://localhost
#flowable.idm.ldap.port=10389
#flowable.idm.ldap.user=uid=admin, ou=system
#flowable.idm.ldap.password=secret
#flowable.idm.ldap.base-dn=o=flowable
#flowable.idm.ldap.query.user-by-id=(&(objectClass=inetOrgPerson)(uid={0}))
#flowable.idm.ldap.query.user-by-full-name-like=(&(objectClass=inetOrgPerson)(|({0}=*{1}*)({2}=*{3}*)))
#flowable.idm.ldap.query.all-users=(objectClass=inetOrgPerson)
#flowable.idm.ldap.query.groups-for-user=(&(objectClass=groupOfUniqueNames)(uniqueMember={0}))
#flowable.idm.ldap.query.all-groups=(objectClass=groupOfUniqueNames)
#flowable.idm.ldap.query.group-by-id=(&(objectClass=groupOfUniqueNames)(uniqueId={0}))
#flowable.idm.ldap.attribute.user-id=uid
#flowable.idm.ldap.attribute.first-name=cn
#flowable.idm.ldap.attribute.last-name=sn
#flowable.idm.ldap.attribute.email=mail
#flowable.idm.ldap.attribute.group-id=cn
#flowable.idm.ldap.attribute.group-name=cn
#flowable.idm.ldap.cache.group-size=10000
#flowable.idm.ldap.cache.group-expiration=180000#
# Keycloak
#
#flowable.idm.app.keycloak.enabled=true
#flowable.idm.app.keycloak.server=<keycloakLocation>
#flowable.idm.app.keycloak.authentication-realm=master
#flowable.idm.app.keycloak.authentication-user=admin
#flowable.idm.app.keycloak.authentication-password=admin
#flowable.idm.app.keycloak.realm=<realm>#
# DEFAULT ADMINISTRATOR ACCOUNT
#flowable.idm.app.admin.user-id=admin
flowable.idm.app.admin.password=test
flowable.idm.app.admin.first-name=Test
flowable.idm.app.admin.last-name=Administrator
flowable.idm.app.admin.email=test-admin@example-domain.tld# Enable and configure JMS
#flowable.task.app.jms-enabled=true
#spring.activemq.broker-url=tcp://localhost:61616# Enable and configure RabbitMQ
#flowable.task.app.rabbit-enabled=true
#spring.rabbitmq.addresses=localhost:5672
#spring.rabbitmq.username=guest
#spring.rabbitmq.password=guest# Enable and configure Kafka
#flowable.task.app.kafka-enabled=true
#spring.kafka.bootstrap-servers=localhost:9092
默认情况下flowable有没有把mysql驱动程序打入到war包里面,需要手动添加对应的驱动。apache-tomcat-9.0.79\webapps\flowable-ui\WEB-INF\lib 我选择的是mysql-connector-java-5.1.45.jar驱动,具体可以从网上下载也可以通过maven方式从中央仓库拉取。

再次启动tomcat,访问http://127.0.0.1:8080/flowable-ui 初始用户名和密码:admin/test
3、springboot接入flowable
登录flowable之后选择建模器应用程序,添加一个流程,我这边已经添加了一个简单的请假流程。


新入门可以导入我们流程定义,具体文件在看的我资源。重点介绍springboot如何集成flowable,配置后续有机会在单独介绍里程配置说明。
新建springboot项目添加响应的依赖。
 <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.flowable</groupId><artifactId>flowable-spring-boot-starter</artifactId><version>6.7.0</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.45</version></dependency><!-- Flowable 内部日志采用 SLF4J --><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>1.7.21</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>1.7.21</version></dependency></dependencies>
application.yml配置:
spring:datasource:url: jdbc:mysql://192.168.56.100:13306/flowable?useSSL=false&characterEncoding=UTF-8&serverTimezone=GMT%2B8driver-class-name: com.mysql.jdbc.Driverusername: rootpassword: my-secret-pw
# flowable 配置
flowable:# 关闭异步,不关闭历史数据的插入就是异步的,会在同一个事物里面,无法回滚# 开发可开启会提高些效率,上线需要关闭async-executor-activate: false
server:port: 18080Controller
import liquibase.pro.packaged.O;
import liquibase.pro.packaged.U;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.*;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.runtime.ActivityInstance;
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.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;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;/*** @author pyj* @date 2019/10/30*/
@RestController
@RequestMapping("flowable")
public class TestController {@Autowiredprivate RuntimeService runtimeService;@Autowiredprivate TaskService taskService;@Autowiredprivate HistoryService historyService;@Autowiredprivate RepositoryService repositoryService;@Autowiredprivate ProcessEngine processEngine;/*** 创建流程** @param userId* @param days* @param reason* @return*/@GetMapping("add")public String addExpense(String userId, String days, String reason) {Map<String, Object> map = new HashMap<>();map.put("employee", userId);map.put("nrOfHolidays", days);map.put("description", reason);ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("holidayRequest", map);return "提交成功,流程ID为:" + processInstance.getId();}/*** 获取指定用户组流程任务列表** @return*/@GetMapping("listtask")public Object listtask() {StringBuffer btf = new StringBuffer();List<Task> list = taskService.createTaskQuery().list();for (Task task : list){btf.append(task.getId()+"\r\n");System.out.println(task.getId());}return btf.toString();}/*** 获取指定用户组流程任务列表** @param group* @return*/@GetMapping("list")public Object list(String group) {List<Task> list = taskService.createTaskQuery().list();List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup(group).list();return tasks.toString();}/*** 通过/拒绝任务** @param taskId* @param approved 1 :true  2:false* @return*/@GetMapping("apply")public String apply(String taskId, String approved) {Task task = taskService.createTaskQuery().taskId(taskId).singleResult();if (task == null) {return "流程不存在";}Map<String, Object> variables = new HashMap<>();Boolean apply = approved.equals("1") ? true : false;variables.put("approved", apply);taskService.complete(taskId, variables);return "审批是否通过:" + approved;}/*** 查看历史流程记录** @param processInstanceId* @return*/@GetMapping("historyList")public Object getHistoryList(String processInstanceId) {List<HistoricActivityInstance> historicActivityInstances = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).finished().orderByHistoricActivityInstanceEndTime().asc().list();return historicActivityInstances;}/*** 驳回流程实例** @param taskId* @param targetTaskKey* @return*/@GetMapping("rollbask")public String rollbaskTask(String taskId, String targetTaskKey) {Task currentTask = taskService.createTaskQuery().taskId(taskId).singleResult();if (currentTask == null) {return "节点不存在";}List<String> key = new ArrayList<>();key.add(currentTask.getTaskDefinitionKey());runtimeService.createChangeActivityStateBuilder().processInstanceId(currentTask.getProcessInstanceId()).moveActivityIdsToSingleActivityId(key, targetTaskKey).changeState();return "驳回成功...";}/*** 终止流程实例** @param processInstanceId*/public String deleteProcessInstanceById(String processInstanceId) {// ""这个参数本来可以写删除原因runtimeService.deleteProcessInstance(processInstanceId, "");return "终止流程实例成功";}/*** 挂起流程实例** @param processInstanceId 当前流程实例id*/@GetMapping("hangUp")public String handUpProcessInstance(String processInstanceId) {runtimeService.suspendProcessInstanceById(processInstanceId);return "挂起流程成功...";}/*** 恢复(唤醒)被挂起的流程实例** @param processInstanceId 流程实例id*/@GetMapping("recovery")public String activateProcessInstance(String processInstanceId) {runtimeService.activateProcessInstanceById(processInstanceId);return "恢复流程成功...";}/*** 判断传入流程实例在运行中是否存在** @param processInstanceId* @return*/@GetMapping("isExist/running")public Boolean isExistProcIntRunning(String processInstanceId) {ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();if (processInstance == null) {return false;}return true;}/*** 判断流程实例在历史记录中是否存在* @param processInstanceId* @return*/@GetMapping("isExist/history")public Boolean isExistProcInHistory(String processInstanceId) {HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();if (historicProcessInstance == null) {return false;}return true;}/*** 我发起的流程实例列表** @param userId* @return 流程实例列表*/@GetMapping("myTasks")public List<HistoricProcessInstance> getMyStartProcint(String userId) {List<HistoricProcessInstance> list = historyService.createHistoricProcessInstanceQuery().startedBy(userId).orderByProcessInstanceStartTime().asc().list();return list;}/*** 查询流程图** @param httpServletResponse* @param processId* @throws Exception*/@RequestMapping(value = "processDiagram")public void genProcessDiagram(HttpServletResponse httpServletResponse, String processId) throws Exception {List<ActivityInstance> activityInstanceList =  runtimeService.createActivityInstanceQuery().list();for(ActivityInstance activityInstance : activityInstanceList){System.out.println(activityInstance.getId());}System.out.println("=========================================================================");List<ProcessInstance> list =  runtimeService.createProcessInstanceQuery().list();for(ProcessInstance processInstance : list){System.out.println(processInstance.getId());}ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processId).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的IdList<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,true);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提供了几个Service接口和实现类,可以通过service拿到流程的一些定义、流转等信息。
正如TestController里面定义的几个方法,分别是实例化流程,审批流程,查看流程等操作。如下
实例化流程:

审批流程:

拉取当前流程任务清单:

查看流程状态:

具体大家可以参考如下文章有详细的springboot集成指引。
Flowable BPMN 用户手册 (v 6.3.0)
相关文章:
 
工作流程引擎之flowable(集成springboot)
0、背景 现状:公司各部门业务系统有各自的工作流引擎,也有cross function的业务在不同系统或OA系统流转,没有统一的去规划布局统一的BPM解决方案,近期由于一个项目引发朝着整合统一的BPM方案,特了解一下市面上比较主流…...
 
leetcode54. 螺旋矩阵(java)
螺旋矩阵 题目描述解题 收缩法 上期经典算法 题目描述 难度 - 中等 原题链接 - leecode 54 螺旋矩阵 给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。 示例1: 输入:matrix [[1,2,3],[4,5,6],[7…...
go gorm 查询
定义model package mysqltestimport ("errors""fmt""gorm.io/gorm" )type Product struct {gorm.ModelID uint gorm:"primarykey"Name string gorm:"column:name"Price float64 gorm:"column:price_value&quo…...
Flutter GetXController 动态Tabbar 报错问题
场景: 1.Tabbar的内容是接口获取的 2. TabController? tabController;; 在onInit 方法中初始化tabbarController tabController TabController(initialIndex: 0, length: titleDataList.length, vsync: this); 这时候会报一个错误 Controllers l…...
 
Redis(缓存预热,缓存雪崩,缓存击穿,缓存穿透)
目录 一、缓存预热 二、缓存雪崩 三、缓存击穿 四、缓存穿透 一、缓存预热 开过车的都知道,冬天的时候启动我们的小汽车之后不要直接驾驶,先让车子发动机预热一段时间再启动。缓存预热是一样的道理。 缓存预热就是系统启动前,提前将相关的…...
 
UE4/5Niagara粒子特效学习(使用UE5.1,适合新手)
目录 创建空模板 创建粒子 粒子的基础属性 粒子的生命周期 颜色 大小设置 生成的位置 Skeletal Mesh Location的效果: Shape Location 添加速度 添加Noise力场 在生成中添加: 效果: 编辑 在更新中添加: 效果&…...
from moduleA import * 语句 和import moduleA 的区别
from moduleA import * 语句和import moduleA 的区别是: from moduleA import * 语句会将moduleA模块中的所有内容(函数、变量、类等)直接导入到当前模块的命名空间中,这样就可以直接使用它们,而不需要加上模块名的限…...
 
【leetcode 力扣刷题】交换链表中的节点
24. 两两交换链表中的节点 24. 两两交换链表中的节点两两节点分组,反转两个节点连接递归求解 24. 两两交换链表中的节点 题目链接:24. 两两交换链表中的节点 题目内容: 题目中强调不能修改节点内部值,是因为如果不加这个限制的话…...
 
学会Mybatis框架:让你的代码更具灵活性、可维护性、安全性和高效性【二.动态SQL】
🥳🥳Welcome Huihuis Code World ! !🥳🥳 接下来看看由辉辉所写的关于Mybatis的相关操作吧 目录 🥳🥳Welcome Huihuis Code World ! !🥳🥳 一.Mybatis动态SQL如何应用 1.需求 2.…...
Oracle 中 ROWNUM 使用问题记录
ROWNUM 使用问题记录(2023-08-17) Oracle 版本: 19.0.0.0.0 Enterprise现象:今天在项目遇到一个问题,测试人员反馈前一天能看到的数据今天看不到了 用表格举例,这是前一天看到的数据,有9、7、1 这几个数量信息 日期…...
 
MySQL数据库:内置函数
日期函数 规定:日期:年月日 时间:时分秒 函数名称作用描述current_date()当前日期current_time()当前时间current_timestamp()当前时间戳date(datetime)返回datetime参数的日期部分date_add(date,interval d_value_type)在date中添加…...
 
【C++杂货铺】探索string的底层实现
文章目录 一、成员变量二、成员函数2.1 默认构造函数2.2 拷贝构造函数2.3 operator2.4 c_str()2.5 size()2.6 operator[ ]2.7 iterator2.8 reserve2.9 resize2.10 push_back2.11 append2.12 operator2.13 insert2.14 erase2.15 find2.16 substr2.17 operator<<2.18 opera…...
 
c++ day1
定义一个命名空间Myspace,包含以下函数:将一个字符串中的所有单词进行反转,并输出反转后的结果。例如,输入字符串为"Hello World",输出结果为"olleH dlroW",并在主函数内测试该函数。 …...
 
变动的Python爬虫实现
在电商时代,了解商品价格的变动对于购物者和卖家来说都非常重要。本文将分享一种基于Python的实时监控电商平台商品价格变动的爬虫实现方法。通过本文的解决方案和代码示例,您将能够轻松监控商品价格,并及时做出决策。 一、了解需求和目标 在…...
 
mybatis-plus--配置-(sql)日志输出-自动填充-分页-多数据源-逻辑删除
写在前面: 本文主要介绍mybatis-plus的配置,以后在有的时候在补充。欢迎交流。 文章目录 日志输出自动填充分页全局字段配置多数据源 日志输出 调试的时候需要看执行的sql,这时候就很需要日志来记录查看了。 mybatis-plus的日志配置在yml…...
 
数据API服务管理功能:解放数据潜力,提升业务效率
数据API服务的重要性 在数字化时代,数据被认为是企业的重要资产。数据API服务的管理功能能够有效帮助企业实现数据的整合和利用。通过合理的数据API服务管理,企业可以更好地解放数据潜力,提升业务效率。  解放数据潜力 数据API服务管理功…...
 
云南森林火灾vr消防模拟安全演练系统训练消防员火灾和事故的适应和应对能力
据统计,每一场破坏性地震发生后,会引发次生的灾害,而火灾是其中之一。导致火灾的原因,推测是地震时使供电线路短路,引燃易燃物,火灾就随即发生。所以,在日常生活中,定期的消防演练还是非常必要的, VR消防,是VR公司深圳华锐视点利用VR虚拟现实技术,将VR和…...
 
(6)(6.2) 任务命令
文章目录 前言 6.2.1 概述 6.2.2 导航命令 6.2.3 条件命令 6.2.4 DO命令 前言 本文介绍了 Copter、Plane 和 Rover 切换到自动模式时支持的任务指令。 !Warning 这是一项正在进行中的工作,尚未经过全面审核。有关 Copter 的更佳列表,请…...
 
【consul】
consul 一、什么是服务注册与发现1.11.2 二、 什么是consul2.1定义2.2特性2.2.1服务注册与发现:2.2.2健康检查:2.2.3Key/Value存储: 三、consul部署-datacenter :指定数据中心名称,默认是dc1。consul :指定…...
Electron环境搭建
Electron是一个优秀的开源框架,用于构建跨平台的桌面应用程序。它基于Chromium和Node.js,使得开发者可以使用Web技术(HTML、CSS和JavaScript)来构建可在Windows、macOS和Linux等多个操作系统上运行的应用程序。本文将介绍如何搭建…...
Python爬虫实战:研究MechanicalSoup库相关技术
一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...
 
(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)
题目:3442. 奇偶频次间的最大差值 I 思路 :哈希,时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况,哈希表这里用数组即可实现。 C版本: class Solution { public:int maxDifference(string s) {int a[26]…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...
 
3-11单元格区域边界定位(End属性)学习笔记
返回一个Range 对象,只读。该对象代表包含源区域的区域上端下端左端右端的最后一个单元格。等同于按键 End 向上键(End(xlUp))、End向下键(End(xlDown))、End向左键(End(xlToLeft)End向右键(End(xlToRight)) 注意:它移动的位置必须是相连的有内容的单元格…...
 
LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...
第7篇:中间件全链路监控与 SQL 性能分析实践
7.1 章节导读 在构建数据库中间件的过程中,可观测性 和 性能分析 是保障系统稳定性与可维护性的核心能力。 特别是在复杂分布式场景中,必须做到: 🔍 追踪每一条 SQL 的生命周期(从入口到数据库执行)&#…...
 
《Docker》架构
文章目录 架构模式单机架构应用数据分离架构应用服务器集群架构读写分离/主从分离架构冷热分离架构垂直分库架构微服务架构容器编排架构什么是容器,docker,镜像,k8s 架构模式 单机架构 单机架构其实就是应用服务器和单机服务器都部署在同一…...
0x-3-Oracle 23 ai-sqlcl 25.1 集成安装-配置和优化
是不是受够了安装了oracle database之后sqlplus的简陋,无法删除无法上下翻页的苦恼。 可以安装readline和rlwrap插件的话,配置.bahs_profile后也能解决上下翻页这些,但是很多生产环境无法安装rpm包。 oracle提供了sqlcl免费许可,…...
