现状:公司各部门业务系统有各自的工作流引擎,也有cross function的业务在不同系统或OA系统流转,没有统一的去规划布局统一的BPM解决方案,近期由于一个项目引发朝着整合统一的BPM方案,特了解一下市面上比较主流的开源和收费的工作流引擎。本文主要介绍开源的工作流引擎flowable.
开源工作流引擎是一种用于管理和自动化业务流程的软件,它可以帮助用户实现业务流程的可视化设计、流程编排、任务调度、监控和优化等功能。本文将介绍几种常见的开源工作流引擎,并进行比较。目前市场上比较主流的开源流程引擎有:Activiti、Camunda、Flowable。
Activiti是一个轻量级的开源工作流引擎,采用Java语言开发,基于BPMN 2.0规范,支持嵌入式部署和分布式部署。Activiti提供了丰富的API和插件,支持与Java应用程序进行集成。它还提供了Web界面和REST API,可以方便地进行流程设计、部署、调度和监控。Activiti具有以下优点:
Camunda是一个强大的开源工作流引擎,采用Java语言开发,支持BPMN 2.0规范和CMMN规范。Camunda提供了丰富的API和插件,支持与Java应用程序进行集成。它还提供了Web界面和REST API,可以方便地进行流程设计、部署、调度和监控。Camunda具有以下优点:
Flowable是一个开源的轻量级工作流引擎,基于Activiti 5.x版本开发,支持BPMN 2.0规范和CMMN规范。Flowable提供了丰富的API和插件,支持与Java应用程序进行集成。它还提供了Web界面和REST API,可以方便地进行流程设计、部署、调度和监控。Flowable具有以下优点:
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安装.
分别解压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.
修改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=/auth/realms/ #spring.security.oauth2.client.registration.keycloak.client-id= #spring.security.oauth2.client.registration.keycloak.client-secret= #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= /auth/realms/ #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=-1 spring.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/flowable spring.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=PT5S flowable.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=true flowable.common.app.idm-admin.user=admin flowable.common.app.idm-admin.password=test flowable.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= #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= # # 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
登录flowable之后选择建模器应用程序,添加一个流程,我这边已经添加了一个简单的请假流程。
新入门可以导入我们流程定义,具体文件在看的我资源。重点介绍springboot如何集成flowable,配置后续有机会在单独介绍里程配置说明。
新建springboot项目添加响应的依赖。
org.springframework.boot spring-boot-starter-weborg.flowable flowable-spring-boot-starter6.7.0 mysql mysql-connector-java5.1.45 org.slf4j slf4j-api1.7.21 org.slf4j slf4j-log4j121.7.21
application.yml配置:
spring: datasource: url: jdbc:mysql://192.168.56.100:13306/flowable?useSSL=false&characterEncoding=UTF-8&serverTimezone=GMT%2B8 driver-class-name: com.mysql.jdbc.Driver username: root password: my-secret-pw # flowable 配置 flowable: # 关闭异步,不关闭历史数据的插入就是异步的,会在同一个事物里面,无法回滚 # 开发可开启会提高些效率,上线需要关闭 async-executor-activate: false server: port: 18080
Controller
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 { @Autowired private RuntimeService runtimeService; @Autowired private TaskService taskService; @Autowired private HistoryService historyService; @Autowired private RepositoryService repositoryService; @Autowired private ProcessEngine processEngine; /** * 创建流程 * * @param userId * @param days * @param reason * @return */ @GetMapping("add") public String addExpense(String userId, String days, String reason) { Mapmap = 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 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 list = taskService.createTaskQuery().list(); List 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 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 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 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 getMyStartProcint(String userId) { List 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 activityInstanceList = runtimeService.createActivityInstanceQuery().list(); for(ActivityInstance activityInstance : activityInstanceList){ System.out.println(activityInstance.getId()); } System.out.println("========================================================================="); List 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 executions = runtimeService .createExecutionQuery() .processInstanceId(InstanceId) .list(); //得到正在执行的Activity的Id List activityIds = new ArrayList<>(); List flows = new ArrayList<>(); for (Execution exe : executions) { List 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)