文章目录 一、Flowable介绍1.简介2.Activiti、Flowable、Camunda二、Flowable实战(集成Flowable Modeler)三、流程的创建和使用1.BPMN基本概念介绍2.业务模型流程创建3.表单创建及使用4.流程的使用5.核心表介绍四、常见报错解决

一、Flowable介绍 1.简介

Flowable是一个使用Java编写的轻量级业务流程引擎。Flowable流程引擎可用于部署BPMN 2.0流程定义, 创建这些流程定义的流程实例,进行查询,访问运行中或历史的流程实例与相关数据,等等。

BPMN:Business Process Modeling Notation,即业务流程建模符号,是一种流程建模的通用和标准语言,用来绘制业务流程图,以便更好地让各部门之间理解业务流程和相互关系。

2.Activiti、Flowable、Camunda

(1)为什么选这三者比较? 三者都是开源免费、社区活跃度比较高的; 三者都是同一个团队的分支,camunda基于activiti5,flowable基于activiti6,activiti5则是基于更早的jbpm4;

(2)优缺点 ①功能比较

②Activiti7以后,对于流程引擎本身及相关引擎功能关注度并不高,核心很大精力放在构建其云生态环境(适配Docker、kubernates,适配Jenkins等devops工具);而Flowable分离出去做了很多引擎相关的完善。

③网上资料数对比,仅以GIT为例: git上activiti的项目(16,618)是flowable(59629)两三倍,flowable是camunda(4077)十来倍

综上所述,Activiti7最大的优势是网上资料多,缺点是功能最少、易用性比较差。camunda最大的优势就是性能比较高,缺点是三者的资料是最少的。flowable是一个比较均衡的方案。

二、Flowable实战(集成Flowable Modeler)

你也可以选择不集成modeler编辑器,那样的话只需要引入flowable的starter和添加ProcessEngineConfig配置,在后面的流程的创建和使用中也能运行

1.源码下载,后面会用到 地址:https://github.com/flowable/flowable-engine/releases/tag/flowable-6.4.1/

2.引入依赖,我这里用的版本是6.4.1,替换下面的参数即可

org.springframework.boot spring-boot-starter-web 2.1.3.RELEASE org.springframework.boot spring-boot-starter-test 2.1.3.RELEASE test org.flowable flowable-spring-boot-starter-basic ${flowable.version} org.flowable flowable-ui-modeler-rest ${flowable.version} org.flowable flowable-ui-modeler-logic ${flowable.version} org.flowable flowable-ui-modeler-conf ${flowable.version} org.slf4j slf4j-api 1.7.21 org.slf4j slf4j-log4j12 1.7.21 org.springframework.boot spring-boot-configuration-processor 3.0.1 org.projectlombok lombok 1.18.0 provided com.alibaba druid 1.0.24 compile org.springframework.boot spring-boot-starter-jdbc 2.1.3.RELEASE mysql mysql-connector-java 8.0.31

3.后端代码集成 需要这些文件,其中Security的包名不能变,否则不能生效,无法免登录 AppDispatcherServletConfiguration.java

/* Licensed under the Apache License, Version 2.0 (the “License”); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an “AS IS” BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */package com.example.flowable.config;import org.flowable.ui.modeler.rest.app.EditorGroupsResource;import org.flowable.ui.modeler.rest.app.EditorUsersResource;import org.flowable.ui.modeler.rest.app.StencilSetResource;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.FilterType;import org.springframework.scheduling.annotation.EnableAsync;import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;import org.springframework.web.servlet.i18n.SessionLocaleResolver;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;@Configuration@ComponentScan(value = { “org.flowable.ui.modeler.rest.app”, // 不加载 rest,因为 getAccount 接口需要我们自己实现// “org.flowable.ui.common.rest”},excludeFilters = { // 移除 EditorUsersResource 与 EditorGroupsResource,因为不使用 IDM 部分 @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = EditorUsersResource.class), @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = EditorGroupsResource.class), // 配置文件用自己的 @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StencilSetResource.class),})@EnableAsyncpublic class AppDispatcherServletConfiguration implements WebMvcRegistrations { private static final Logger LOGGER = LoggerFactory.getLogger(AppDispatcherServletConfiguration.class); @Bean public SessionLocaleResolver localeResolver() { return new SessionLocaleResolver(); } @Bean public LocaleChangeInterceptor localeChangeInterceptor() { LOGGER.debug(“Configuring localeChangeInterceptor”); LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor(); localeChangeInterceptor.setParamName(“language”); return localeChangeInterceptor; } @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { LOGGER.debug(“Creating requestMappingHandlerMapping”); RequestMappingHandlerMapping requestMappingHandlerMapping = new RequestMappingHandlerMapping(); requestMappingHandlerMapping.setUseSuffixPatternMatch(false); requestMappingHandlerMapping.setRemoveSemicolonContent(false); Object[] interceptors = { localeChangeInterceptor() }; requestMappingHandlerMapping.setInterceptors(interceptors); return requestMappingHandlerMapping; }}

ApplicationConfiguration.java

/* Licensed under the Apache License, Version 2.0 (the “License”); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an “AS IS” BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */package com.example.flowable.config;import org.flowable.ui.common.service.idm.RemoteIdmService;import org.flowable.ui.modeler.properties.FlowableModelerAppProperties;import org.flowable.ui.modeler.servlet.ApiDispatcherServletConfiguration;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.boot.web.servlet.ServletRegistrationBean;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.FilterType;import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;import org.springframework.web.servlet.DispatcherServlet;@Configuration@EnableConfigurationProperties(FlowableModelerAppProperties.class)@ComponentScan(basePackages = {// “org.flowable.ui.modeler.conf”, // 不引入 conf “org.flowable.ui.modeler.repository”, “org.flowable.ui.modeler.service”,// “org.flowable.ui.modeler.security”, //授权方面的都不需要// “org.flowable.ui.common.conf”, // flowable 开发环境内置的数据库连接// “org.flowable.ui.common.filter”, // IDM 方面的过滤器 “org.flowable.ui.common.service”, “org.flowable.ui.common.repository”, //// “org.flowable.ui.common.security”,//授权方面的都不需要 “org.flowable.ui.common.tenant” },excludeFilters = { // 移除 RemoteIdmService @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = RemoteIdmService.class)})public class ApplicationConfiguration { @Bean public ServletRegistrationBean modelerApiServlet(ApplicationContext applicationContext) { AnnotationConfigWebApplicationContext dispatcherServletConfiguration = new AnnotationConfigWebApplicationContext(); dispatcherServletConfiguration.setParent(applicationContext); dispatcherServletConfiguration.register(ApiDispatcherServletConfiguration.class); DispatcherServlet servlet = new DispatcherServlet(dispatcherServletConfiguration); ServletRegistrationBean registrationBean = new ServletRegistrationBean(servlet, “/api/*”); registrationBean.setName(“Flowable Modeler App API Servlet”); registrationBean.setLoadOnStartup(1); registrationBean.setAsyncSupported(true); return registrationBean; }}

FlowableStencilSetResource.java

/* Licensed under the Apache License, Version 2.0 (the “License”); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an “AS IS” BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */package com.example.flowable.config;import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.ObjectMapper;import org.flowable.ui.common.service.exception.InternalServerErrorException;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping(“/app”)public class FlowableStencilSetResource { private static final Logger LOGGER = LoggerFactory.getLogger(FlowableStencilSetResource.class); @Autowired protected ObjectMapper objectMapper; @RequestMapping(value = “/rest/stencil-sets/editor”, method = RequestMethod.GET, produces = “application/json”) public JsonNode getStencilSetForEditor() { try { JsonNode stencilNode = objectMapper.readTree(this.getClass().getClassLoader().getResourceAsStream(“stencilset/stencilset_bpmn.json”)); return stencilNode; } catch (Exception e) { LOGGER.error(“Error reading bpmn stencil set json”, e); throw new InternalServerErrorException(“Error reading bpmn stencil set json”); } } @RequestMapping(value = “/rest/stencil-sets/cmmneditor”, method = RequestMethod.GET, produces = “application/json”) public JsonNode getCmmnStencilSetForEditor() { try { JsonNode stencilNode = objectMapper.readTree(this.getClass().getClassLoader().getResourceAsStream(“stencilset/stencilset_cmmn.json”)); return stencilNode; } catch (Exception e) { LOGGER.error(“Error reading bpmn stencil set json”, e); throw new InternalServerErrorException(“Error reading bpmn stencil set json”); } }}

ProcessEngineConfig.java

package com.example.flowable.config;import lombok.Data;import org.flowable.engine.ProcessEngine;import org.flowable.engine.ProcessEngineConfiguration;import org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;/** * 流程引擎配置文件 * @author: jijh * @create: 2022-12-12 16:49 **/@Configuration@ConfigurationProperties(prefix = “spring.datasource”)@Datapublic class ProcessEngineConfig { private Logger logger = LoggerFactory.getLogger(ProcessEngineConfig.class); private String url; private String driverClassName; private String username; private String password; private String publicKey; /** * 初始化流程引擎 * @return */ @Primary @Bean(name = “processEngine”) public ProcessEngine initProcessEngine() { logger.info(“=============================ProcessEngineBegin=============================”); // 流程引擎配置 ProcessEngineConfiguration cfg = null; try { cfg = new StandaloneProcessEngineConfiguration() .setJdbcUrl(url) .setJdbcUsername(username) //.setJdbcPassword(ConfigTools.decrypt(publicKey, password)) .setJdbcPassword(password) .setJdbcDriver(driverClassName) // 初始化基础表,不需要的可以改为 DB_SCHEMA_UPDATE_FALSE .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE) // 默认邮箱配置 // 发邮件的主机地址,先用 QQ 邮箱 //.setMailServerHost(“smtp.qq.com”) // POP3/SMTP服务的授权码 //.setMailServerPassword(“xxxxxxx”) // 默认发件人 //.setMailServerDefaultFrom(“836369078@qq.com”) // 设置发件人用户名 //.setMailServerUsername(“管理员”) // 解决流程图乱码 .setActivityFontName(“宋体”) .setLabelFontName(“宋体”) .setAnnotationFontName(“宋体”); } catch (Exception e) { e.printStackTrace(); } // 初始化流程引擎对象 ProcessEngine processEngine = cfg.buildProcessEngine(); logger.info(“=============================ProcessEngineEnd=============================”); return processEngine; }}

FlowableApplication.java

package com.example.flowable;import com.example.flowable.config.AppDispatcherServletConfiguration;import com.example.flowable.config.ApplicationConfiguration;import org.flowable.ui.modeler.conf.DatabaseConfiguration;import org.mybatis.spring.annotation.MapperScan;import org.springframework.boot.SpringApplication;import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;import org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.context.annotation.Import;//启用全局异常拦截器@Import(value={ // 引入修改的配置 ApplicationConfiguration.class, AppDispatcherServletConfiguration.class, // 引入 DatabaseConfiguration 表更新转换 DatabaseConfiguration.class})// Eureka 客户端@EnableDiscoveryClient@MapperScan(“com.example.*.dao”)// 移除 Security 自动配置// Spring Cloud 为 Finchley 版本// @SpringBootApplication(exclude={SecurityAutoConfiguration.class})// Spring Cloud 为 Greenwich 版本@SpringBootApplication(exclude={ SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, SecurityFilterAutoConfiguration.class})public class FlowableApplication { public static void main(String[] args) { SpringApplication.run(FlowableApplication.class, args); }}

SecurityUtils.java

/* Licensed under the Apache License, Version 2.0 (the “License”); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an “AS IS” BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */package org.flowable.ui.common.security;import org.flowable.idm.api.User;import org.flowable.ui.common.model.RemoteUser;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.context.SecurityContext;import org.springframework.security.core.context.SecurityContextHolder;import java.util.ArrayList;import java.util.List;/** * Utility class for Spring Security. */public class SecurityUtils { private static User assumeUser; private SecurityUtils() { } /** * Get the login of the current user. */ public static String getCurrentUserId() { User user = getCurrentUserObject(); if (user != null) { return user.getId(); } return null; } /** * @return the {@link User} object associated with the current logged in user. */ public static User getCurrentUserObject() { if (assumeUser != null) { return assumeUser; } RemoteUser user = new RemoteUser(); user.setId(“admin”); user.setDisplayName(“Administrator”); user.setFirstName(“Administrator”); user.setLastName(“Administrator”); user.setEmail(“admin@flowable.com”); user.setPassword(“123456”); List pris = new ArrayList(); pris.add(DefaultPrivileges.ACCESS_MODELER); pris.add(DefaultPrivileges.ACCESS_IDM); pris.add(DefaultPrivileges.ACCESS_ADMIN); pris.add(DefaultPrivileges.ACCESS_TASK); pris.add(DefaultPrivileges.ACCESS_REST_API); user.setPrivileges(pris); return user; } public static FlowableAppUser getCurrentFlowableAppUser() { FlowableAppUser user = null; SecurityContext securityContext = SecurityContextHolder.getContext(); if (securityContext != null && securityContext.getAuthentication() != null) { Object principal = securityContext.getAuthentication().getPrincipal(); if (principal instanceof FlowableAppUser) { user = (FlowableAppUser) principal; } } return user; } public static boolean currentUserHasCapability(String capability) { FlowableAppUser user = getCurrentFlowableAppUser(); for (GrantedAuthority grantedAuthority : user.getAuthorities()) { if (capability.equals(grantedAuthority.getAuthority())) { return true; } } return false; } public static void assumeUser(User user) { assumeUser = user; } public static void clearAssumeUser() { assumeUser = null; }}

4.前端代码集成 目录结构如下:

static下的代码来自源码包的flowable-engine-flowable-6.4.1\modules\flowable-ui-modeler\flowable-ui-modeler-app\src\main\resources\static下面

resource\static\scripts\configuration\url-conf.js需要修改:

/* Licensed under the Apache License, Version 2.0 (the “License”); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an “AS IS” BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */var FLOWABLE = FLOWABLE || {};/* * Contains methods to retrieve the (mostly) base urls of the different end points. * Two of the methods #getImageUrl and #getModelThumbnailUrl are exposed in the $rootScope for usage in the HTML views. */FLOWABLE.APP_URL = { /* ACCOUNT URLS */ getAccountUrl: function () { return FLOWABLE.CONFIG.contextRoot + ‘/login/rest/account’; }, getLogoutUrl: function () { return FLOWABLE.CONFIG.contextRoot + ‘/app/logout’; }, /* MODEL URLS */ getModelsUrl: function (query) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/models’ + (query || “”); }, getModelUrl: function (modelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/models/’ + modelId; }, getModelModelJsonUrl: function (modelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/models/’ + modelId + ‘/model-json’; }, getModelBpmn20ExportUrl: function (modelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/models/’ + modelId + ‘/bpmn20?version=’ + Date.now(); }, getCloneModelsUrl: function (modelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/models/’ + modelId + ‘/clone’; }, getModelHistoriesUrl: function (modelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/models/’ + modelId + ‘/history’; }, getModelHistoryUrl: function (modelId, modelHistoryId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/models/’ + modelId + ‘/history/’ + modelHistoryId; }, getModelHistoryModelJsonUrl: function (modelId, modelHistoryId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/models/’ + modelId + ‘/history/’ + modelHistoryId + ‘/model-json’; }, getModelHistoryBpmn20ExportUrl: function (modelId, modelHistoryId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/models/’ + modelId + ‘/history/’ + modelHistoryId + ‘/bpmn20?version=’ + Date.now(); }, getCmmnModelDownloadUrl: function (modelId, modelHistoryId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/models/’ + modelId + (modelHistoryId ? ‘/history/’ + modelHistoryId : ”) + ‘/cmmn?version=’ + Date.now(); }, getModelParentRelationsUrl: function (modelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/models/’ + modelId + ‘/parent-relations’; }, /* APP DEFINITION URLS */ getAppDefinitionImportUrl: function (renewIdmIds) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/app-definitions/import?renewIdmEntries=’ + renewIdmIds; }, getAppDefinitionTextImportUrl: function (renewIdmIds) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/app-definitions/text/import?renewIdmEntries=’ + renewIdmIds; }, getAppDefinitionUrl: function (modelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/app-definitions/’ + modelId; }, getAppDefinitionModelImportUrl: function (modelId, renewIdmIds) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/app-definitions/’ + modelId + ‘/import?renewIdmEntries=’ + renewIdmIds; }, getAppDefinitionModelTextImportUrl: function (modelId, renewIdmIds) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/app-definitions/’ + modelId + ‘/text/import?renewIdmEntries=’ + renewIdmIds; }, getAppDefinitionPublishUrl: function (modelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/app-definitions/’ + modelId + ‘/publish’; }, getAppDefinitionExportUrl: function (modelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/app-definitions/’ + modelId + ‘/export?version=’ + Date.now(); }, getAppDefinitionBarExportUrl: function (modelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/app-definitions/’ + modelId + ‘/export-bar?version=’ + Date.now(); }, getAppDefinitionHistoryUrl: function (modelId, historyModelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/app-definitions/’ + modelId + ‘/history/’ + historyModelId; }, getModelsForAppDefinitionUrl: function () { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/models-for-app-definition’; }, getCmmnModelsForAppDefinitionUrl: function () { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/cmmn-models-for-app-definition’; }, /* PROCESS INSTANCE URLS */ getProcessInstanceModelJsonUrl: function (modelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/process-instances/’ + modelId + ‘/model-json’; }, getProcessInstanceModelJsonHistoryUrl: function (historyModelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/process-instances/history/’ + historyModelId + ‘/model-json’; }, /* PROCESS DEFINITION URLS */ getProcessDefinitionModelJsonUrl: function (processDefinitionId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/process-definitions/’ + processDefinitionId + ‘/model-json’; }, /* PROCESS MODEL URLS */ getImportProcessModelUrl: function () { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/import-process-model’; }, getImportProcessModelTextUrl: function () { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/import-process-model/text’; }, /* DECISION TABLE URLS */ getDecisionTableModelsUrl: function () { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/decision-table-models’; }, getDecisionTableImportUrl: function () { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/decision-table-models/import-decision-table’; }, getDecisionTableTextImportUrl: function () { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/decision-table-models/import-decision-table-text’; }, getDecisionTableModelUrl: function (modelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/decision-table-models/’ + modelId; }, getDecisionTableModelValuesUrl: function (query) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/decision-table-models/values?’ + query; }, getDecisionTableModelsHistoryUrl: function (modelHistoryId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/decision-table-models/history/’ + modelHistoryId; }, getDecisionTableModelHistoryUrl: function (modelId, modelHistoryId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/decision-table-models/’ + modelId + ‘/history/’ + modelHistoryId; }, /* FORM MODEL URLS */ getFormModelsUrl: function () { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/form-models’; }, getFormModelValuesUrl: function (query) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/form-models/values?’ + query; }, getFormModelUrl: function (modelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/form-models/’ + modelId; }, getFormModelHistoryUrl: function (modelId, modelHistoryId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/form-models/’ + modelId + ‘/history/’ + modelHistoryId; }, /* CASE MODEL URLS */ getCaseModelsUrl: function (query) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/case-models’ + (query || “”); }, getCaseModelImportUrl: function () { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/import-case-model’; }, getCaseModelTextImportUrl: function () { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/import-case-model/text’; }, getCaseInstancesHistoryModelJsonUrl: function (modelHistoryId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/case-instances/history/’ + modelHistoryId + ‘/model-json’; }, getCaseInstancesModelJsonUrl: function (modelId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/case-instances/’ + modelId + ‘/model-json’; }, getCaseDefinitionModelJsonUrl: function (caseDefinitionId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/case-definitions/’ + caseDefinitionId + ‘/model-json’; }, /* IMAGE URLS (exposed in rootscope in app.js */ getImageUrl: function (imageId) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/image/’ + imageId; }, getModelThumbnailUrl: function (modelId, version) { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/models/’ + modelId + ‘/thumbnail’ + (version ? “?version=” + version : “”); }, /* OTHER URLS */ getEditorUsersUrl: function () { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/editor-users’; }, getEditorGroupsUrl: function () { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/editor-groups’; }, getAboutInfoUrl: function () { return FLOWABLE.CONFIG.contextRoot + ‘/app/rest/about-info’; }};

stencilset下面的文件是汉化文件 https://download.csdn.net/download/tttalk/87347577 5.其余配置 application.yml

spring: application: name: flowable-service main: allow-circular-references: true datasource: url: jdbc:mysql://127.0.0.1:3306/flowable2?serverTimezone=Asia/Shanghai&useUnicode=true&nullCatalogMeansCurrent=true&characterEncoding=UTF-8&useSSL=false username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource# flowable 配置flowable: # 关闭异步,不关闭历史数据的插入就是异步的,会在同一个事物里面,无法回滚 # 开发可开启会提高些效率,上线需要关闭 async-executor-activate: false

log4j.properties

log4j.rootLogger=DEBUG, CAlog4j.appender.CA=org.apache.log4j.ConsoleAppenderlog4j.appender.CA.layout=org.apache.log4j.PatternLayoutlog4j.appender.CA.layout.ConversionPattern= %d{ hh:mm:ss,SSS} [%t] %-5p %c %x – %m%n

6.启动项目 进入http://localhost:8080/,进入下面页面则算启动成功了在这里插入图片描述

三、流程的创建和使用 1.BPMN基本概念介绍

可以去BPMN官网学习相关知识 https://www.bpmn.org/ (1)流对象(Flow Objects):是定义业务流程的主要图形元素,包括三种:事件、活动、网关

事件(Events):指的是在业务流程的运行过程中发生的事情,分为: 开始:表示一个流程的开始 中间:发生的开始和结束事件之间,影响处理的流程 结束:表示该过程结束

活动(Activities):包括任务和子流程两类。子流程在图形的下方中间外加一个小加号(+)来区分。

网关(Gateways):用于表示流程的分支与合并。

排他网关:只有一条路径会被选择 并行网关:所有路径会被同时选择 包容网关:可以同时执行多条线路,也可以在网关上设置条件 事件网关:专门为中间捕获事件设置的,允许设置多个输出流指向多个不同的中间捕获事件。当流程执行到事件网关后,流程处于等待状态,需要等待抛出事件才能将等待状态转换为活动状态。

(2)数据(Data):数据主要通过四种元素表示

数据对象(Data Objects) 数据输入(Data Inputs) 数据输出(Data Outputs) 数据存储(Data Stores)

(3)连接对象(Connecting Objects):流对象彼此互相连接或者连接到其他信息的方法主要有三种

顺序流:用一个带实心箭头的实心线表示,用于指定活动执行的顺序

信息流:用一条带箭头的虚线表示,用于描述两个独立的业务参与者(业务实体/业务角色)之间发送和接受的消息流动

关联:用一根带有线箭头的点线表示,用于将相关的数据、文本和其他人工信息与流对象联系起来。用于展示活动的输入和输出

(4)泳道(Swimlanes):通过泳道对主要的建模元素进行分组,将活动划分到不同的可视化类别中来描述由不同的参与者的责任与职责。

2.业务模型流程创建

我这里自己创建了一个流程,如果自己嫌麻烦可以直接使用我的(右上角导入)BPMN的XML文件即可,但是form不会生效

报销审批

在这里插入图片描述 折线绘制需要用到这个:在这里插入图片描述 每一个节点都需要分配用户 在这里插入图片描述 我这里id直接写死了,这个在代码里可以自己指派 在这里插入图片描述

分配完去左上角验证,没有什么警告之类的就可以 在这里插入图片描述

3.表单创建及使用

这个功能可能使用的比较少,一般在前端系统开发自己的表单 进入http://localhost:8080/#/forms

在这里插入图片描述 在流程中使用,只需要填表单key即可,记得分配用户 在这里插入图片描述

4.流程的使用

流程和表单等保存在act_de_model,可以去数据库里查看 (1)代码实现 在这里插入图片描述 在recource下的新建process放入我们刚刚画好的流程图 在这里插入图片描述

FlowableController .java

package com.example.flowable.controller;import com.example.flowable.service.FlowService;import com.google.common.collect.Maps;import org.flowable.engine.history.HistoricActivityInstance;import org.flowable.task.api.Task;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;import java.io.ByteArrayOutputStream;import java.io.FileOutputStream;import java.util.HashMap;import java.util.List;import java.util.Map;@RestController@RequestMapping(“/flowable”)public class FlowableController { @Resource private FlowService flowService; //部署流程 @RequestMapping(value = “/createProcess/{processDefinitionKey}”, method = {RequestMethod.GET}) public String createProcess(@PathVariable(“processDefinitionKey”) String processDefinitionKey) { //已知processDefinitionKey,可以通过act_de_model获取processName,我这里偷懒写死了 String processName = “报销审批流程”; String path = “process/”+processName+”.bpmn20.xml”; flowService.createProcess(processDefinitionKey,path); return “流程部署成功”; } //发起流程 @RequestMapping(value = “/apply/{processDefinitionKey}”, method = {RequestMethod.GET}) public String apply(@PathVariable(“processDefinitionKey”) String processDefinitionKey) throws Exception { Map map = new HashMap(); String processId = flowService.applyProcess(processDefinitionKey,map); System.out.println(processId);//17501 return “流程发起成功,流程id为”+processId; } //生成流程图,标红的框是当前流程走到的地方 @RequestMapping(value = “/getPng/{processId}”, method = {RequestMethod.GET}) public String getPng(@PathVariable(“processId”) String processId) throws Exception { ByteArrayOutputStream out = flowService.genProcessDiagram(processId); FileOutputStream fileOutputStream = new FileOutputStream(“D:\\process” + processId + “.png”); fileOutputStream.write(out.toByteArray()); return “流程图生成成功”; } //查询待办流程列表 @RequestMapping(value = “/flowList/{userId}”, method = {RequestMethod.GET}) public Object flowList(@PathVariable(“userId”) String userId) { List list = flowService.todoList(userId); System.out.println(list.toString()); return list.toString(); } //流程审批通过或退回 @RequestMapping(value = “/approveProcess/{userId}/{taskId}”, method = {RequestMethod.GET}) public void approveProcess(@PathVariable(“taskId”) String taskId,@PathVariable(“userId”) String userId){ Map map = Maps.newHashMap(); //这个map可以放在表单里传过来 map.put(“approval”,”1″); //map.put(“approval”,”0″); flowService.approveProcess(taskId,userId,map); } //流程退回某一结点 @RequestMapping(value = “/withdrawProcess/{taskId}/{nodeId}”, method = {RequestMethod.GET}) public void withdrawProcess(@PathVariable(“taskId”) String taskId,@PathVariable(“nodeId”) String nodeId) throws Exception { flowService.withdrawProcess(taskId,nodeId); } //查询历史 @RequestMapping(value = “/historyList/{processId}”, method = {RequestMethod.GET}) public Object historyList(@PathVariable(“processId”) String processId){ Listlist = flowService.historyList(processId); return list.toString(); }}

FlowService .java

package com.example.flowable.service;import org.flowable.engine.history.HistoricActivityInstance;import org.flowable.task.api.Task;import java.io.ByteArrayOutputStream;import java.util.List;import java.util.Map;public interface FlowService { /** * 部署流程 * @param processName 流程定义名 * @param resourcePath 如flowable/process.bpmn * @return */ public void createProcess(String processName, String resourcePath); /** * 发起流程 * @param processName 流程定义名 * @param map 参数 * @return 流程实例ID */ public String applyProcess(String processName,Map map); /** * 查询某用户/群体/角色待办列表 * @return */ public List todoList(String user); /** * 生成流程图 * @param processId 任务ID */ public ByteArrayOutputStream genProcessDiagram(String processId) throws Exception; /** * 完成任务 * @param taskId 任务id * @param user 用户/角色id * @param map 流程变量 */ public void approveProcess(String taskId,String user,Map map); /** * 将节点移动到任意节点上 * @param taskId 任务id * @param taskDefinitionKey 目标节点ID,节点ID在流程画图的时候设置好 * @return */ public void withdrawProcess(String taskId,String taskDefinitionKey) throws Exception; /** * 获取流程的历史节点列表 * 获取的是这个流程实例走过的节点,当然也可以获取到开始节点、网关、线等信息,下面是只过滤了用户任务节点”userTask”的信息 * @param processId 流程ID * @return */ public List historyList(String processId); /** * 删除流程实例 * @param processId 流程实例ID * @return */ public void deleteProcess(String processId); /** * 申领任务 * 其实申领的意思就是当在一个用户组中所有有这个权限的用户都可以同时看到这个待办信息, * 这个待办信息可以理解为公布出来的任务,需要有人去领取这个任务,那么一旦领取这个任务,其他有这个节点操作权限的用户就不会看到这个待办信息, * 因为已经被这个用户领取了 * @param taskId * @param user * @return */ public void claim(String taskId,String user); /** * 取消申领任务 * 一旦取消申领,那么有这个节点操作权限的用户在待办上又可以看到, * 申领和取消申领是一种锁定机制,使得多个用户在待办操作上不会出现执行同一个当前节点的任务 * @param taskId * @return */ public void unClaim(String taskId);}

FlowServiceImpl .java

package com.example.flowable.service.impl;import com.example.flowable.service.FlowService;import lombok.extern.slf4j.Slf4j;import org.flowable.bpmn.model.BpmnModel;import org.flowable.engine.*;import org.flowable.engine.history.HistoricActivityInstance;import org.flowable.engine.repository.Deployment;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.stereotype.Service;import javax.annotation.Resource;import java.io.ByteArrayOutputStream;import java.io.InputStream;import java.util.ArrayList;import java.util.List;import java.util.Map;@Slf4j@Servicepublic class FlowServiceImpl implements FlowService { @Resource private RepositoryService repositoryService; @Resource private RuntimeService runtimeService; @Resource private TaskService taskService; @Resource private ProcessEngine processEngine; @Resource private HistoryService historyService; /** * 部署流程 * @param processName 流程定义名 * @param resourcePath 如flowable/process.bpmn * @return */ @Override public void createProcess(String processName, String resourcePath){ Deployment deployment = repositoryService.createDeployment().name(processName).addClasspathResource(resourcePath).deploy(); } /** * 发起流程 * @return */ @Override public String applyProcess(String processName,Mapmap){ //指定发起人 ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(processName, map); log.info(“流程实例ID:”+processInstance.getProcessInstanceId()); return processInstance.getProcessInstanceId(); } /** * 查询某用户/群体/角色待办列表 * @return */ @Override public List todoList(String user){ List tasks = taskService.createTaskQuery().taskCandidateOrAssigned(user).orderByTaskCreateTime().desc().list(); return tasks; } /** * 生成流程图 * @param processId 任务ID */ @Override public ByteArrayOutputStream genProcessDiagram(String processId) throws Exception { ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processId).singleResult(); //流程走完的不显示图 if (pi == null) { return null; } 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,”bmp”, activityIds,flows,”宋体”,”宋体”,”宋体”,null,1.0,false); ByteArrayOutputStream out = null; byte[] buf = new byte[1024]; int legth = 0; try { out = new ByteArrayOutputStream(); while ((legth = in.read(buf)) != -1) { out.write(buf, 0, legth); } return out; } finally { if (in != null) { in.close(); } if (out != null) { out.close(); } } } /** * 完成任务 * @param taskId 任务id * @param user 用户/角色id * @param map 流程变量 */ @Override public void approveProcess(String taskId,String user,Map map){ //先申领任务,相当于用户将这个流程任务占用,其他在这个用户组里的用户不能看到该流程任务 taskService.claim(taskId,user); //再流转下一个节点 taskService.complete(taskId, map); } /** * 将节点移动到任意节点上 * @param taskId 任务id * @param taskDefinitionKey 目标节点ID,节点ID在流程画图的时候设置好 * @return */ @Override public void withdrawProcess(String taskId,String taskDefinitionKey) throws Exception { //获取当前任务,让其移动到目标节点位置 Task task = taskService.createTaskQuery().taskId(taskId).singleResult(); if(task == null) { throw new Exception(“任务不存在”); } //将节点移动到目标节点 runtimeService.createChangeActivityStateBuilder().processInstanceId(task.getProcessInstanceId()) .moveActivityIdTo(task.getTaskDefinitionKey(), taskDefinitionKey).changeState(); } /** * 获取流程的历史节点列表 * 获取的是这个流程实例走过的节点,当然也可以获取到开始节点、网关、线等信息,下面是只过滤了用户任务节点”userTask”的信息 * @param processId 流程ID * @return */ @Override public List historyList(String processId){ List activities = historyService.createHistoricActivityInstanceQuery() .processInstanceId(processId).activityType(“userTask”).finished() .orderByHistoricActivityInstanceEndTime().desc().list(); return activities; } /** * 删除流程实例 * @param processId 流程实例ID * @return */ @Override public void deleteProcess(String processId){ runtimeService.deleteProcessInstance(processId, “”); } /** * 申领任务 * 其实申领的意思就是当在一个用户组中所有有这个权限的用户都可以同时看到这个待办信息, * 这个待办信息可以理解为公布出来的任务,需要有人去领取这个任务,那么一旦领取这个任务,其他有这个节点操作权限的用户就不会看到这个待办信息, * 因为已经被这个用户领取了 * @param taskId * @param user * @return */ @Override public void claim(String taskId,String user){ taskService.claim(taskId,user); } /** * 取消申领任务 * 一旦取消申领,那么有这个节点操作权限的用户在待办上又可以看到, * 申领和取消申领是一种锁定机制,使得多个用户在待办操作上不会出现执行同一个当前节点的任务 * @param taskId * @return */ @Override public void unClaim(String taskId){ taskService.unclaim(taskId); }}

(2)测试 ①首先是流程部署 进入http://localhost:8080/flowable/createProcess/a999 可以看到流程部署成功,此时act_re_deployment和act_ge_bytearray会看到数据记录 ②发起流程 进入http://localhost:8080/flowable/apply/a999 发起成功后act_ru_execution会有记录,并且能看到流程id,这个id后面会使用到 在这里插入图片描述

③生成流程图 http://localhost:8080/flowable/getPng/35005 可以看到当前流程的进度在哪,我这里把生成的流程图放在D盘下面了 在这里插入图片描述

④查看当前用户的待办 进入http://localhost:8080/flowable/flowList/userid2,注意userid2是我设置的用户申请里的分配用户,在流程图里是candidateUsers,可以看到task_id,后面会用到 在这里插入图片描述 ⑤用户进行审批或退回,我这里默认写死通过 进入http://localhost:8080/flowable/approveProcess/userid2/35010 ⑥再次查看流程图 http://localhost:8080/flowable/getPng/35005 可以看到流程走到下一个节点了

在这里插入图片描述 再次进入http://localhost:8080/flowable/flowList/userid2,发现task_id变化了 在这里插入图片描述 进入http://localhost:8080/flowable/flowList/leader1,发现下一节点的用户也有任务了,这个id后面会用到 在这里插入图片描述

⑦退回某一特定节点,其中32503是task_id,sid-5946EBF9-CCA9-41D5-A1B9-812886784183是节点id 进入http://localhost:8080/flowable/withdrawProcess/35022/sid-5946EBF9-CCA9-41D5-A1B9-812886784183 在这里插入图片描述 然后再次查看流程图,发现流程变化了 http://localhost:8080/flowable/getPng/35005 在这里插入图片描述

5.核心表介绍

(1)表名分类 ACT_RE_* repository-静态信息数据。如流程定义、流程的资源(图片,规则等)。 ACT_RU_* runtime-运行数据。存储着流程变量,用户任务,变量,职责(job)等运行时的数据。flowable只存储实例执行期间的运行时数据,当流程实例结束时,将删除这些记录。这就保证了这些运行时的表小且快。 ACT_ID_* identity-组织机构数据。包含标识的信息,如用户,用户组,等等。 ACT_HI_* history-历史数据。包括流程实例,变量,任务,等等。 ACT_GE_* general-通用数据。各种情况都使用的数据。

(2)核心表 部署内容表:act_ge_bytearray 此表和ACT_RE_DEPLOYMENT是多对一的关系 部署ID表:act_re_deployment 流程表:act_re_procdef 历史节点表:act_hi_actinst 历史任务流程实例信息 :act_hi_taskinst 流程变量数据表:act_ru_variable 历史变量表:act_hi_varinst 流程实例历史:act_hi_procinst 历史流程人员表:act_hi_identitylink 运行时流程人员表:act_ru_identitylink 运行时任务节点表:act_ru_task

(3)流程启动到结束数据库变化 部署完毕后,act_re_deployment表中会有一条部署记录,记录这次部署的基本信息,然后是act_ge_bytearray表中有两条记录,记录的是本次上传的bpmn文件和对应的图片文件,每条记录都有act_re_deployment表的外键关联,然后是act_re_procdef表中有一条记录,记录的是该bpmn文件包含的基本信息,包含act_re_deployment表外键。

流程启动,首先向act_ru_execution表中插入一条记录,记录的是这个流程定义的执行实例,其中id和proc_inst_id相同都是流程执行实例id,也就是本次执行这个流程定义的id,包含流程定义的id外键。

然后向act_ru_task插入一条记录,记录的是第一个任务的信息,也就是开始执行第一个任务。包括act_ru_execution表中的execution_id外键和proc_inst_id外键,也就是本次执行实例id。

然后向act_hi_procinst表和act_hi_taskinst表中各插入一条记录,记录的是本次执行实例和任务的历史记录:

任务提交后,首先向act_ru_variable表中插入变量信息,包含本次流程执行实例的两个id外键,但不包括任务的id,因为setVariable方法设置的是全局变量,也就是整个流程都会有效的变量:

当流程中的一个节点任务完成后,进入下一个节点任务,act_ru_task表中这个节点任务的记录被删除,插入新的节点任务的记录。

同时act_ru_execution表中的记录并没有删除,而是将正在执行的任务变成新的节点任务。

同时向act_hi_var_inst和act_hi_taskinst插入历史记录。

整个流程执行完毕,act_ru_task,act_ru_execution和act_ru_variable表相关记录全被清空。

全程有一个表一直在记录所有动作,就是act_hi_actinst表

四、常见报错解决

1.自动建表提示 表已存在 Table ‘act_id_property’ already exists mysql连接地址后面加上&nullCatalogMeansCurrent=true,如果还是不行,可以新建一个库再试试

2.集成SpringBoot项目报错 “SLF4J: Class path contains multiple SLF4J bindings.” flowable 集成依赖 rest,logic,conf 的三个jar包加上下面的片段

org.slf4j slf4j-api

3.集成SpringBoot的项目报错”NoClassDefFoundError: org/springframework/core/ErrorCoded” 将Flowable中Spring的相关包剔除

org.springframework spring-beans org.springframework spring-core org.springframework spring-context


比丘资源网 » Flowable教程

发表回复

提供最优质的资源集合

立即查看 了解详情