jBPM是目前市场上主流开源工作引擎之一,在创建者Tom Baeyens离开JBoss后,jBPM的下一个版本jBPM5完全放弃了jBPM4的基础代码,基于Drools Flow重头来过,目前官网已经推出了jBPM6的beta版本;Tom Baeyens加入Alfresco后很快推出了新的基于jBPM4的开源工作流系统Activiti。
虽然JBPM和activiti都是使用bpmn格式作为流程定义语言,但二者都相应地利用了bpmn格式的规范扩展了一些自定义的功能,根据这些扩展它们都提供了自己的绑定表单的方式。JBPM核心引擎完全没有关于表单的任何抽象,它的工作机制是通过全局常量,流程变量,任务变量,这些概念十分技术化。
相比之下Activiti则更贴近实际的应用场景,它将为开始节点,以及人工任务提供了表单设置,用户可以设置字段名称,字段类型。通过Activiti的平台可以根据这些设置去生成表单,但如果不使用其平台只使用引擎的话,也支持通过它来表达与第三方表单的关系。这些表单设置的元数据信息也可以通过接口去获取。
文章目录
一. Activiti相关概念 1. Activiti介绍2. 核心类 2.1 ProcessEngine2.2 服务(Service)类 2.2.1 TaskService2.2.2 RepositoryService2.2.3 RuntimeService2.2.3 HistoryService2.3 CommandContextIntercepter或CommandExecutor2.4 核心业务对象3. 上下文组件(Context) 3.1 CommandContext3.2 ProcessEngineConfigurationImpl3.3 ExecutionContext4. 持久化组件5. Event-Listener组件6. Cache组件7. 异步执行组件8. PVM:Process Virtal Machine二. Eclipse插件安装:三. 项目搭建 1. 新建Spring Boot工程2. 引入Activiti相关依赖3. 创建流程图4. 启动工程5. 修改配置 5.1 添加MySQL依赖5.2 修改数据库5.3 Activiti相关配置5.4 附上application.yml完整配置:5.5 启动工程6. 编写实例 6.1 引入Thymeleaf依赖,(前端使用Thymeleaf的配置已经在application.yml中)6.2 创建Controller控制器6.3 流程跟踪与流程图展示6.4 附上DemoController完整代码7. 效果
一. Activiti相关概念
1. Activiti介绍
Activiti是基于Apache许可的开源BPM平台,创始人Tom Baeyens原是JBPM架构师,可以理解为与JBPM出自同一祖师爷。它提供了Eclipse插件,开发可以通过插件直接绘制业务流程图。基于Spring,ibatis等框架,并在此之上构建了非常清晰的开发框架。是由Alfresco软件发布的业务流程管理(BPM)框架,它是覆盖了业务流程管理、工作流、服务协作等领域的一个开源的、灵活的、易扩展的可执行流程语言框架。 本文基于Activiti7的Activiti Core,基于Spring Boot做简单学习总结。(Activiti最新版本向微服务这边靠齐了,并分Activiti Core与Activiti Cloud两块,Activiti Cloud还没研究)
2. 核心类
2.1 ProcessEngine
流程引擎的抽象,可以通过此类获取需要的所有服务。
2.2 服务(Service)类
通过ProcessEngine获取,Activiti将不同生命周期的服务封装在不同Service中,包括定义、部署、运行。通过服务类可获取相关生命周期中的服务信息。
2.2.1 TaskService
流程运行过程中,每个任务节点的相关操作接口,如complete,delete,delegate等。
2.2.2 RepositoryService
流程定义和部署相关的存储服务。
2.2.3 RuntimeService
流程运行时相关的服务,如根据流程好启动流程实例startProcessInstanceByKey。
2.2.3 HistoryService
历史记录相关服务接口。
2.3 CommandContextIntercepter或CommandExecutor
Activiti使用命令模式作为基础开发模式,如Service中调用的各个方法都对应相应的命令对象。Service将请求委托给命令对象,命令对象来命令接受者,接受者接收后执行并返回结果。而CommandContextIntercepter的作用是拦截所有命令,并在命令前后执行一些公共方法。
2.4 核心业务对象
org.activiti.engine.impl.persistence.entity包下的类,包括Task,ProcessInstance,Execution等。会根据不同职责实现相应接口的方法(如需要持久化则继承PersistentObject接口),与传统的实体类不同。
3. 上下文组件(Context)
用来保存生命周期比较长,全局性的信息,类似Application,主要包括如下三类。
3.1 CommandContext
命令上下文,保存每个命令必要的资源,如持久化需要的session。
3.2 ProcessEngineConfigurationImpl
流程引擎相关配置信息,整个引擎全局的配置信息,如数据源DataSource等。该对象为单例,在流程引擎创建的时候初始化。
3.3 ExecutionContext
持有ExecutionEntity对象。
4. 持久化组件
Activiti使用ibatis作OR映射,并在此基础上增加设计了自己的持久化框架。在流程引擎创建时初始化。顶层接口Session、SessionFactory。Session有两个实现类:DbSqlSession,负责sql表达式的执行。AbstractManager负责对象的持久化操作。SessionFactory有两个实现类:DbSqlSessionFactory负责DbSqlSession相关操作,GenericManagerFactory负责AbstractManager相关操作。
5. Event-Listener组件
Activiti允许客户代码介入流程执行,提供了事件监听组件。监听的事件类型可以分为TaskListener、JavaDelegate、Expression、ExecutionListener。ProcessEngineConfigurationImpl持有DelegateInterceptor的某个实例,方便调用handleInvocation。
6. Cache组件
DbSqlSession中有cache的实现,Activiti基于List和Map来做缓存。如查询时先查缓存,没有则直接查询并放入缓存。
7. 异步执行组件
Activiti可以执行任务,JobExecutor为启核心类,JobExecutor包含三个主要属性:JobAcquisitionThread,BlockingQueue,ThreadPoolExecutor。方法ProcessEngines在引擎启动时调用JobExecutor.start,JobAcquisitionThread 线程即开始工作,其run方法不断循环执行AcquiredJobs中的job,执行一次后线程等待一定时间直到超时或者JobExecutor.jobWasAdded方法,因为有新任务而被调用。
8. PVM:Process Virtal Machine
流程虚拟机API暴露了流程虚拟机的POJO核心,流程虚拟机API描述了一个工作流流程必备的组件,这些组件包括:
PvmProcessDefinition:流程的定义,形象点说就是用户画的那个图。静态含义。
PvmProcessInstance:流程实例,用户发起的某个PvmProcessDefinition的一个实例,动态含义。
PvmActivity:流程中的一个节点
PvmTransition:衔接各个节点之间的路径,形象点说就是图中各个节点之间的连接线。
PvmEvent:流程执行过程中触发的事件
二. Eclipse插件安装:
我的Eclipse版本如下:
下载离线安装包(在线安装始终失败,应该出于网络限制),地址:/designer/archived/activiti-designer-5.18.0.zip
离线安装包安装安装依然提示相关包找不到,于是下载另外三个依赖包(org.eclipse.emf.transaction_1.4.0.v0331-1738.jar、org.eclipse.emf.validation_1.7.0.06111341.jar、org.eclipse.emf.workspace_1.5.1.v0328-0001.jar,在Maven仓库可以找到),放到Eclipse的plugin目录下,继续安装,如下:
确定后勾选相关选项,完成安装,重启Eclipse,New->Other
出现以上标志,则安装完成。
三. 项目搭建
1. 新建Spring Boot工程
(我的Eclipse已经安装Spring Boot插件)
然后Next->Next…->Finish即可,然后改application.properties为application.yml(个人习惯)
2. 引入Activiti相关依赖
在pom属性中定义版本号,并添加Activiti相关依赖:
<activiti-dependencies.version>7.0.56</activiti-dependencies.version><dependencyManagement><dependencies><dependency><groupId>org.activiti.dependencies</groupId><artifactId>activiti-dependencies</artifactId><version>${activiti-dependencies.version}</version><scope>import</scope><type>pom</type></dependency></dependencies></dependencyManagement><dependency><groupId>org.activiti</groupId><artifactId>activiti-spring-boot-starter</artifactId></dependency>
由于Activiti默认使用H2数据库,所以需添加H2数据库支持(这里使用此SpringBoot版本默认1.4.197):
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId></dependency>
出现错误:Missing artifact org.activiti:activiti-spring-boot-starter:jar:7.0.56
添加私服仓库地址:
<repositories><repository><id>alfresco</id><name>Activiti Releases</name><url>/nexus/content/repositories/activiti-releases/</url><releases><enabled>true</enabled></releases></repository></repositories>
错误消失。
3. 创建流程图
在此版本Activiti+SpringBoot,默认加载/processes/目录下流程,于是在resources下新建processes目录,并在目录下new->Other,如下:
使用Activiti图编辑工具打开,创建如下流程(创建过程在此不介绍,就是右侧工具栏的运用):
4. 启动工程
看日志:
从日志中可以看出,流程引擎已经默认创建,并可以看到使用的默认数据源是H2的数据源,我们创建的流程也已经部署。
5. 修改配置
在正常使用中,一般系统会有自己的数据库,而不会采用默认内存的H2数据库,这里以MySQL为例。修改application.yml。
5.1 添加MySQL依赖
<!--使用mysql数据库,导入mysql驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency>
5.2 修改数据库
spring:##数据库连接信息datasource:# 数据源配置url: jdbc:mysql://127.0.0.1:3306/activity?useUnicode=true&characterEncoding=utf-8&useSSL=falseusername: rootpassword: rootdriver-class-name: com.mysql.jdbc.Driver# SQLException: XAER_INVAL: Invalid arguments (or unsupported command)问题xa:properties:pinGlobalTxToPhysicalConnection: trueuseServerPrepStmts: true
5.3 Activiti相关配置
# 参考配置/liaojie970/p/8857710.htmlactiviti:# 自动建表database-schema: ACTIVITIdatabase-schema-update: truehistory-level: fulldb-history-used: true
注意:
database-schema-update表示启动时检查数据库表,不存在则创建
history-level表示哪种情况下使用历史表,这里配置为full表示全部记录历史,方便绘制流程图
db-history-used为true表示使用历史表,如果不配置,则工程启动后可以检查数据库,只建立了17张表,历史表没有建立,则流程图及运行节点无法展示(暂未找到可行方式)
5.4 附上application.yml完整配置:
# 服务配置server:display-name: actdemoport: 8085# Spring相关配置spring:##数据库连接信息datasource:# 数据源配置url: jdbc:mysql://127.0.0.1:3306/activity?useUnicode=true&characterEncoding=utf-8&useSSL=falseusername: rootpassword: 888driver-class-name: com.mysql.jdbc.Driver# SQLException: XAER_INVAL: Invalid arguments (or unsupported command)问题xa:properties:pinGlobalTxToPhysicalConnection: trueuseServerPrepStmts: truethymeleaf:mode: HTMLencoding: utf-8# 禁用缓存cache: falseapplication:# 注册应用名name: actdemomvc:# 静态资源路径static-path-pattern: /static/**# 参考配置/liaojie970/p/8857710.htmlactiviti:# 自动建表database-schema: ACTIVITIdatabase-schema-update: truehistory-level: fulldb-history-used: true
5.5 启动工程
观察日志,流程引擎已成功加载,并已使用MySQL数据库,如下:
再看数据库,已经创建25张表(老版本的Activiti创建表有手动执行SQL和通过调用流程引擎创建两种方式,该版本与SpringBoot整合后,启动默认创建需要的表):
注意:
原SpringBoot工程使用版本2.1.1.RELEASE,启动始终失败,各种错误,后改为2.0.4.RELEASE版本,则启动正常。
6. 编写实例
本例子使用Thymeleaf做前端页面展示(SpringBoot推荐使用),并创建控制器Controller调用工作流接口与前端交互。
6.1 引入Thymeleaf依赖,(前端使用Thymeleaf的配置已经在application.yml中)
<!-- Thymeleaf依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>
6.2 创建Controller控制器
创建启动流程方法,主要代码如下
/*** <p>启动请假流程</p>* @return String 流程实例ID* @author FRH* @time 12月10日上午11:03:36* @version 1.0*/@RequestMapping(value="/start")@ResponseBodypublic String start() {// xml中定义的IDString instanceKey = "leaveProcess";logger.info("开启请假流程...");// 设置流程参数,开启流程Map<String,Object> map = new HashMap<String,Object>();map.put("jobNumber","A1001");map.put("busData","bus data");ProcessInstance instance = runtimeService.startProcessInstanceByKey(instanceKey, map);//使用流程定义的key启动流程实例,key对应helloworld.bpmn文件中id的属性值,使用key值启动,默认是按照最新版本的流程定义启动logger.info("启动流程实例成功:{}", instance);logger.info("流程实例ID:{}", instance.getId());logger.info("流程定义ID:{}", instance.getProcessDefinitionId());//验证是否启动成功//通过查询正在运行的流程实例来判断ProcessInstanceQuery processInstanceQuery = runtimeService.createProcessInstanceQuery();//根据流程实例ID来查询List<ProcessInstance> runningList = processInstanceQuery.processInstanceId(instance.getProcessInstanceId()).list();logger.info("根据流程ID查询条数:{}", runningList.size());// 返回流程IDreturn instance.getId();}
6.3 流程跟踪与流程图展示
Activiti流程图展示,使用流程图生成器,本例生成的流程图在页面使用如下方式展示即可:
<embed src="/demo/showImg?instanceId=5070fd58-f859-11e8-a359-484d7ec5762d" style="display:block;width:1000px;height:450px" />
引入相关工具包,版本使用默认版本7.0.65
<!-- Activiti生成流程图 --><dependency><groupId>org.activiti</groupId><artifactId>activiti-image-generator</artifactId></dependency>
调用输出流程图
/*** <p>查看当前流程图</p>* @param instanceId 流程实例* @param response void 响应* @author FRH* @time 12月10日上午11:14:12* @version 1.0*/@ResponseBody@RequestMapping(value="/showImg")public void showImg(String instanceId, HttpServletResponse response) {/** 参数校验*/logger.info("查看完整流程图!流程实例ID:{}", instanceId);if(StringUtils.isBlank(instanceId)) return;/** 获取流程实例*/HistoricProcessInstance processInstance = historyService.createHistoricProcessInstanceQuery().processInstanceId(instanceId).singleResult();if(processInstance == null) {logger.error("流程实例ID:{}没查询到流程实例!", instanceId);return;}// 根据流程对象获取流程对象模型BpmnModel bpmnModel = repositoryService.getBpmnModel(processInstance.getProcessDefinitionId());/** 查看已执行的节点集合* 获取流程历史中已执行节点,并按照节点在流程中执行先后顺序排序*/// 构造历史流程查询HistoricActivityInstanceQuery historyInstanceQuery = historyService.createHistoricActivityInstanceQuery().processInstanceId(instanceId);// 查询历史节点List<HistoricActivityInstance> historicActivityInstanceList = historyInstanceQuery.orderByHistoricActivityInstanceStartTime().asc().list();if(historicActivityInstanceList == null || historicActivityInstanceList.size() == 0) {logger.info("流程实例ID:{}没有历史节点信息!", instanceId);outputImg(response, bpmnModel, null, null);return;}// 已执行的节点ID集合(将historicActivityInstanceList中元素的activityId字段取出封装到executedActivityIdList)List<String> executedActivityIdList = historicActivityInstanceList.stream().map(item -> item.getActivityId()).collect(Collectors.toList());/** 获取流程走过的线*/// 获取流程定义ProcessDefinitionEntity processDefinition = (ProcessDefinitionEntity) ((RepositoryServiceImpl) repositoryService).getDeployedProcessDefinition(processInstance.getProcessDefinitionId());List<String> flowIds = ActivitiUtils.getHighLightedFlows(bpmnModel, processDefinition, historicActivityInstanceList);/** 输出图像,并设置高亮*/outputImg(response, bpmnModel, flowIds, executedActivityIdList);}/*** <p>输出图像</p>* @param response 响应实体* @param bpmnModel 图像对象* @param flowIds 已执行的线集合* @param executedActivityIdList void 已执行的节点ID集合* @author FRH* @time 12月10日上午11:23:01* @version 1.0*/private void outputImg(HttpServletResponse response, BpmnModel bpmnModel, List<String> flowIds, List<String> executedActivityIdList) {InputStream imageStream = null;try {imageStream = processDiagramGenerator.generateDiagram(bpmnModel, executedActivityIdList, flowIds, "宋体", "微软雅黑", "黑体", true, "png");// 输出资源内容到相应对象byte[] b = new byte[1024];int len;while ((len = imageStream.read(b, 0, 1024)) != -1) {response.getOutputStream().write(b, 0, len);}response.getOutputStream().flush();}catch(Exception e) {logger.error("流程图输出异常!", e);} finally { // 流关闭StreamUtils.closeInputStream(imageStream);}}
流程图工具类ActivitiUtils
package com.mypro.activiti.utils;import java.util.ArrayList;import java.util.List;import org.activiti.bpmn.model.BpmnModel;import org.activiti.bpmn.model.FlowNode;import org.activiti.bpmn.model.SequenceFlow;import org.activiti.engine.history.HistoricActivityInstance;import org.activiti.engine.impl.persistence.entity.ProcessDefinitionEntity;/*** <p>Activiti工作流工具类</p>* @author FRH* @time 12月10日上午11:26:02* @version 1.0*/public class ActivitiUtils {/*** <p>获取流程走过的线</p>* @param bpmnModel 流程对象模型* @param processDefinitionEntity 流程定义对象* @param historicActivityInstances 历史流程已经执行的节点,并已经按执行的先后顺序排序* @return List<String> 流程走过的线* @author FRH* @time 12月10日上午11:26:19* @version 1.0*/public static List<String> getHighLightedFlows(BpmnModel bpmnModel, ProcessDefinitionEntity processDefinitionEntity, List<HistoricActivityInstance> historicActivityInstances) {// 用以保存高亮的线flowIdList<String> highFlows = new ArrayList<String>();if(historicActivityInstances == null || historicActivityInstances.size() == 0) return highFlows;// 遍历历史节点for (int i = 0; i < historicActivityInstances.size() - 1; i++) {// 取出已执行的节点HistoricActivityInstance activityImpl_ = historicActivityInstances.get(i);// 用以保存后续开始时间相同的节点List<FlowNode> sameStartTimeNodes = new ArrayList<FlowNode>();// 获取下一个节点(用于连线)FlowNode sameActivityImpl = getNextFlowNode(bpmnModel, historicActivityInstances, i, activityImpl_);//FlowNode sameActivityImpl = (FlowNode) bpmnModel.getMainProcess().getFlowElement(historicActivityInstances.get(i + 1).getActivityId());// 将后面第一个节点放在时间相同节点的集合里if(sameActivityImpl != null) sameStartTimeNodes.add(sameActivityImpl);// 循环后面节点,看是否有与此后继节点开始时间相同的节点,有则添加到后继节点集合for (int j = i + 1; j < historicActivityInstances.size() - 1; j++) {HistoricActivityInstance activityImpl1 = historicActivityInstances.get(j);// 后续第一个节点HistoricActivityInstance activityImpl2 = historicActivityInstances.get(j + 1);// 后续第二个节点if (activityImpl1.getStartTime().getTime() != activityImpl2.getStartTime().getTime()) break;// 如果第一个节点和第二个节点开始时间相同保存FlowNode sameActivityImpl2 = (FlowNode) bpmnModel.getMainProcess().getFlowElement(activityImpl2.getActivityId());sameStartTimeNodes.add(sameActivityImpl2);}// 得到节点定义的详细信息FlowNode activityImpl = (FlowNode) bpmnModel.getMainProcess().getFlowElement(historicActivityInstances.get(i).getActivityId());// 取出节点的所有出去的线,对所有的线进行遍历List<SequenceFlow> pvmTransitions = activityImpl.getOutgoingFlows();for (SequenceFlow pvmTransition : pvmTransitions) {// 获取节点FlowNode pvmActivityImpl = (FlowNode) bpmnModel.getMainProcess().getFlowElement(pvmTransition.getTargetRef());// 不是后继节点if(!sameStartTimeNodes.contains(pvmActivityImpl)) continue;// 如果取出的线的目标节点存在时间相同的节点里,保存该线的id,进行高亮显示highFlows.add(pvmTransition.getId());}}//返回高亮的线return highFlows;}/*** <p>获取下一个节点信息</p>* @param bpmnModel 流程模型* @param historicActivityInstances 历史节点* @param i 当前已经遍历到的历史节点索引(找下一个节点从此节点后)* @param activityImpl_ 当前遍历到的历史节点实例* @return FlowNode 下一个节点信息* @author FRH* @time 12月10日上午11:26:55* @version 1.0*/private static FlowNode getNextFlowNode(BpmnModel bpmnModel, List<HistoricActivityInstance> historicActivityInstances, int i, HistoricActivityInstance activityImpl_) {// 保存后一个节点FlowNode sameActivityImpl = null;// 如果当前节点不是用户任务节点,则取排序的下一个节点为后续节点if(!"userTask".equals(activityImpl_.getActivityType())) {// 是最后一个节点,没有下一个节点if(i == historicActivityInstances.size()) return sameActivityImpl;// 不是最后一个节点,取下一个节点为后继节点sameActivityImpl = (FlowNode) bpmnModel.getMainProcess().getFlowElement(historicActivityInstances.get(i + 1).getActivityId());// 找到紧跟在后面的一个节点// 返回return sameActivityImpl;}// 遍历后续节点,获取当前节点后续节点for (int k = i + 1; k <= historicActivityInstances.size() - 1; k++) {// 后续节点HistoricActivityInstance activityImp2_ = historicActivityInstances.get(k);// 都是userTask,且主节点与后续节点的开始时间相同,说明不是真实的后继节点if("userTask".equals(activityImp2_.getActivityType()) && activityImpl_.getStartTime().getTime() == activityImp2_.getStartTime().getTime()) continue;// 找到紧跟在后面的一个节点sameActivityImpl = (FlowNode) bpmnModel.getMainProcess().getFlowElement(historicActivityInstances.get(k).getActivityId());break;}return sameActivityImpl;}}
6.4 附上DemoController完整代码
package com.mypro.activiti.controller;import java.io.InputStream;import java.util.ArrayList;import java.util.Date;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.stream.Collectors;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.activiti.bpmn.model.BpmnModel;import org.activiti.engine.HistoryService;import org.activiti.engine.RepositoryService;import org.activiti.engine.RuntimeService;import org.activiti.engine.TaskService;import org.activiti.engine.history.HistoricActivityInstance;import org.activiti.engine.history.HistoricActivityInstanceQuery;import org.activiti.engine.history.HistoricProcessInstance;import org.activiti.engine.impl.RepositoryServiceImpl;import org.activiti.engine.impl.persistence.entity.ProcessDefinitionEntity;import org.activiti.engine.runtime.ProcessInstance;import org.activiti.engine.runtime.ProcessInstanceQuery;import org.activiti.engine.task.Task;import org.activiti.image.ProcessDiagramGenerator;import mons.lang3.StringUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import com.mypro.activiti.utils.ActivitiUtils;import com.mypro.activiti.utils.StreamUtils;/*** <p>Activiti控制器</p>* @author FRH* @time 12月10日上午9:30:18* @version 1.0*/@Controller@RequestMapping("/demo")public class DemoController {private static final Logger logger = LoggerFactory.getLogger(DemoController.class);/** 流程定义和部署相关的存储服务 */@Autowiredprivate RepositoryService repositoryService;/** 流程运行时相关的服务 */@Autowiredprivate RuntimeService runtimeService;/** 节点任务相关操作接口 */@Autowiredprivate TaskService taskService;/** 流程图生成器 */@Autowiredprivate ProcessDiagramGenerator processDiagramGenerator;/** 历史记录相关服务接口 */@Autowiredprivate HistoryService historyService;/*** <p>跳转到测试主页面</p>* @return String 测试主页面* @author FRH* @time 12月10日上午11:12:28* @version 1.0*/@RequestMapping(value="/toIndex.html")public String toTestPage() {return "/index";}/*** <p>跳转到上级审核页面</p>* @return String 上级审核页面* @author FRH* @time 12月5日下午2:31:42* @version 1.0*/@RequestMapping(value="/toLeave")public String employeeLeave() {return "/employeeLeave";}/*** <p>启动请假流程(流程key即xml中定义的ID为leaveProcess)</p>* @return String 启动的流程ID* @author FRH* @time 12月10日上午11:12:50* @version 1.0*/@RequestMapping(value="/start")@ResponseBodypublic String start() {/** xml中定义的ID*/String instanceKey = "leaveProcess";logger.info("开启请假流程...");/** 设置流程参数,开启流程*/Map<String,Object> map = new HashMap<String,Object>();map.put("jobNumber","A1001");map.put("busData","bus data");ProcessInstance instance = runtimeService.startProcessInstanceByKey(instanceKey, map);//使用流程定义的key启动流程实例,key对应helloworld.bpmn文件中id的属性值,使用key值启动,默认是按照最新版本的流程定义启动logger.info("启动流程实例成功:{}", instance);logger.info("流程实例ID:{}", instance.getId());logger.info("流程定义ID:{}", instance.getProcessDefinitionId());/** 验证是否启动成功*///通过查询正在运行的流程实例来判断ProcessInstanceQuery processInstanceQuery = runtimeService.createProcessInstanceQuery();//根据流程实例ID来查询List<ProcessInstance> runningList = processInstanceQuery.processInstanceId(instance.getProcessInstanceId()).list();logger.info("根据流程ID查询条数:{}", runningList.size());/** 返回流程ID*/return instance.getId();}/*** <p>查看当前流程图</p>* @param instanceId 流程实例* @param response void 响应* @author FRH* @time 12月10日上午11:14:12* @version 1.0*/@ResponseBody@RequestMapping(value="/showImg")public void showImg(String instanceId, HttpServletResponse response) {/** 参数校验*/logger.info("查看完整流程图!流程实例ID:{}", instanceId);if(StringUtils.isBlank(instanceId)) return;/** 获取流程实例*/HistoricProcessInstance processInstance = historyService.createHistoricProcessInstanceQuery().processInstanceId(instanceId).singleResult();if(processInstance == null) {logger.error("流程实例ID:{}没查询到流程实例!", instanceId);return;}// 根据流程对象获取流程对象模型BpmnModel bpmnModel = repositoryService.getBpmnModel(processInstance.getProcessDefinitionId());/** 查看已执行的节点集合* 获取流程历史中已执行节点,并按照节点在流程中执行先后顺序排序*/// 构造历史流程查询HistoricActivityInstanceQuery historyInstanceQuery = historyService.createHistoricActivityInstanceQuery().processInstanceId(instanceId);// 查询历史节点List<HistoricActivityInstance> historicActivityInstanceList = historyInstanceQuery.orderByHistoricActivityInstanceStartTime().asc().list();if(historicActivityInstanceList == null || historicActivityInstanceList.size() == 0) {logger.info("流程实例ID:{}没有历史节点信息!", instanceId);outputImg(response, bpmnModel, null, null);return;}// 已执行的节点ID集合(将historicActivityInstanceList中元素的activityId字段取出封装到executedActivityIdList)List<String> executedActivityIdList = historicActivityInstanceList.stream().map(item -> item.getActivityId()).collect(Collectors.toList());/** 获取流程走过的线*/// 获取流程定义ProcessDefinitionEntity processDefinition = (ProcessDefinitionEntity) ((RepositoryServiceImpl) repositoryService).getDeployedProcessDefinition(processInstance.getProcessDefinitionId());List<String> flowIds = ActivitiUtils.getHighLightedFlows(bpmnModel, processDefinition, historicActivityInstanceList);/** 输出图像,并设置高亮*/outputImg(response, bpmnModel, flowIds, executedActivityIdList);}/*** <p>员工提交申请</p>* @param request 请求* @return String 申请受理结果* @author FRH* @time 12月10日上午11:15:09* @version 1.0*/@RequestMapping(value="/employeeApply")@ResponseBodypublic String employeeApply(HttpServletRequest request){/** 获取请求参数*/String taskId = request.getParameter("taskId"); // 任务IDString jobNumber = request.getParameter("jobNumber"); // 工号String leaveDays = request.getParameter("leaveDays"); // 请假天数String leaveReason = request.getParameter("leaveReason"); // 请假原因/** 查询任务*/Task task = taskService.createTaskQuery().taskId(taskId).singleResult();if(task == null) {logger.info("任务ID:{}查询到任务为空!", taskId);return "fail";}/** 参数传递并提交申请*/Map<String, Object> map = new HashMap<String, Object>();map.put("days", leaveDays);map.put("date", new Date());map.put("reason", leaveReason);map.put("jobNumber", jobNumber);plete(task.getId(), map);logger.info("执行【员工申请】环节,流程推动到【上级审核】环节");/** 返回成功*/return "success";}/*** <p>跳转到上级审核页面</p>* @return String 页面* @author FRH* @time 12月5日下午2:31:42* @version 1.0*/@RequestMapping(value="/viewTask")public String toHigherAudit(String taskId, HttpServletRequest request) {/** 获取参数*/logger.info("跳转到任务详情页面,任务ID:{}", taskId);if(StringUtils.isBlank(taskId)) return "/higherAudit";/** 查看任务详细信息*/Task task = taskService.createTaskQuery().taskId(taskId).singleResult();if(task == null) {logger.info("任务ID:{}不存在!", taskId);return "/higherAudit";}/** 完成任务*/Map<String, Object> paramMap = taskService.getVariables(taskId);request.setAttribute("task", task);request.setAttribute("paramMap", paramMap);return "higherAudit";}/*** <p>跳转到部门经理审核页面</p>* @param taskId 任务ID* @param request 请求* @return String 响应页面* @author FRH* @time 12月6日上午9:54:34* @version 1.0*/@RequestMapping(value="/viewTaskManager")public String viewTaskManager(String taskId, HttpServletRequest request) {/** 获取参数*/logger.info("跳转到任务详情页面,任务ID:{}", taskId);if(StringUtils.isBlank(taskId)) return "/manageAudit";/** 查看任务详细信息*/Task task = taskService.createTaskQuery().taskId(taskId).singleResult();if(task == null) {logger.info("任务ID:{}不存在!", taskId);return "/manageAudit";}/** 完成任务*/Map<String, Object> paramMap = taskService.getVariables(taskId);request.setAttribute("task", task);request.setAttribute("paramMap", paramMap);return "manageAudit";}/*** <p>上级审核</p>* @param request 请求* @return String 受理结果* @author FRH* @time 12月10日上午11:19:44* @version 1.0*/@ResponseBody@RequestMapping(value="/higherLevelAudit")public String higherLevelAudit(HttpServletRequest request) {/** 获取请求参数*/String taskId = request.getParameter("taskId");String higherLevelOpinion = request.getParameter("sug");String auditStr = request.getParameter("audit");logger.info("上级审核任务ID:{}", taskId);if(StringUtils.isBlank(taskId)) return "fail";/** 查找任务*/Task task = taskService.createTaskQuery().taskId(taskId).singleResult();if(task == null) {logger.info("审核任务ID:{}查询到任务为空!", taskId);return "fail";}/** 设置局部变量参数,完成任务*/Map<String, Object> map = new HashMap<String, Object>();map.put("audit", "1".equals(auditStr) ? false : true);map.put("higherLevelOpinion", higherLevelOpinion);plete(taskId, map);return "success";}/*** <p>部门经理审核</p>* @param request 请求* @return String 受理结果* @author FRH* @time 12月10日上午11:20:44* @version 1.0*/@ResponseBody@RequestMapping(value="/divisionManagerAudit")public String divisionManagerAudit(HttpServletRequest request) {/** 获取请求参数*/String taskId = request.getParameter("taskId");String opinion = request.getParameter("sug");String auditStr = request.getParameter("audit");logger.info("上级审核任务ID:{}", taskId);if(StringUtils.isBlank(taskId)) return "fail";/** 查找任务*/Task task = taskService.createTaskQuery().taskId(taskId).singleResult();if(task == null) {logger.info("审核任务ID:{}查询到任务为空!", taskId);return "fail";}/** 设置局部变量参数,完成任务*/Map<String, Object> map = new HashMap<String, Object>();map.put("audit", "1".equals(auditStr) ? false : true);map.put("managerOpinion", opinion);plete(taskId, map);return "success";}/*** <p>查看任务</p>* @param request 请求* @return String 任务展示页面* @author FRH* @time 12月10日上午11:21:33* @version 1.0*/@RequestMapping(value="/toShowTask")public String toShowTask(HttpServletRequest request) {/** 获取请求参数*/List<Task> taskList = taskService.createTaskQuery().list();if(taskList == null || taskList.size() == 0) {logger.info("查询任务列表为空!");return "/task";}/** 查询所有任务,并封装*/List<Map<String, String>> resultList = new ArrayList<Map<String, String>>();for(Task task : taskList) {Map<String, String> map = new HashMap<String, String>();map.put("taskId", task.getId());map.put("name", task.getName());map.put("createTime", task.getCreateTime().toString());map.put("assignee", task.getAssignee());map.put("instanceId", task.getProcessInstanceId());map.put("executionId", task.getExecutionId());map.put("definitionId", task.getProcessDefinitionId());resultList.add(map);}/** 返回结果*/logger.info("返回集合:{}", resultList.toString());request.setAttribute("resultList", resultList);return "/task";}/*** <p>输出图像</p>* @param response 响应实体* @param bpmnModel 图像对象* @param flowIds 已执行的线集合* @param executedActivityIdList void 已执行的节点ID集合* @author FRH* @time 12月10日上午11:23:01* @version 1.0*/private void outputImg(HttpServletResponse response, BpmnModel bpmnModel, List<String> flowIds, List<String> executedActivityIdList) {InputStream imageStream = null;try {imageStream = processDiagramGenerator.generateDiagram(bpmnModel, executedActivityIdList, flowIds, "宋体", "微软雅黑", "黑体", true, "png");// 输出资源内容到相应对象byte[] b = new byte[1024];int len;while ((len = imageStream.read(b, 0, 1024)) != -1) {response.getOutputStream().write(b, 0, len);}response.getOutputStream().flush();}catch(Exception e) {logger.error("流程图输出异常!", e);} finally { // 流关闭StreamUtils.closeInputStream(imageStream);}}/*** <p>判断流程是否完成</p>* @param processInstanceId 流程实例ID* @return boolean 已完成-true,未完成-false* @author FRH* @time 12月10日上午11:23:26* @version 1.0*/public boolean isFinished(String processInstanceId) {return historyService.createHistoricProcessInstanceQuery().finished().processInstanceId(processInstanceId).count() > 0;}}
7. 效果
看出Activiti默认使用Spring的security,添加配置,关闭安全认证,如下:
# 关闭activiti登录验证security:basic:enabled: false
重启后继续访问,可正常进入首页(如还是无法进入首页,请添加@EnableAutoConfiguration(exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class}),或者从pom.xml中移除Security包):
点击我要请假后,得到流程实例ID,再查看流程图,如下:
附上首页/index.html代码,其它页面相比比较简单:
<!DOCTYPE html><html xmlns="/1999/xhtml" xmlns:th=""><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>首页</title><script th:src="@{/static/jquery.min.js}"></script><script type="text/javascript">$(function(){// 点击菜单$(".show-page").bind("click", function(){$(".main-body").html("");$(".result-div").html("");var url = $(this).attr("url");$.ajax({async : false,cache : false,type : 'POST',url : url,dataType : "html",error : function() {alert('请求失败');},success : function(data) {$(".result-div").html(data);}});});// 点击我要请假,开启流程$(".show-instance").bind("click", function(){$(".main-body").html("");$(".result-div").html("");var url = $(this).attr("url");$.ajax({async : false,cache : false,type : 'POST',url : url,dataType : "html",error : function() {alert('请求失败');},success : function(data) {$("input[name='instanceId']").val(data);}});});// 绑定查看流程图$(".show-img").bind("click", function(){var instanceId = $("input[name='instanceId']").val();if(instanceId == "") {alert("暂无流程!");return;}var imgHtml = '<embed src="/demo/showImg?instanceId=' + instanceId + '" style="display:block;width:1000px;height:450px" />';$(".result-div").html(imgHtml);});// 查看任务$(".show-task").bind("click", function(){$.ajax({async : false,cache : false,type : 'POST',url : "/demo/toShowTask",data : {"aaabbbccc":"aa"},dataType : "html",error : function() {alert('请求失败');},success : function(data) {$(".result-div").html(data);}});});});/*** 员工提交申请*/function toLeave() {$.ajax({async : false,cache : false,type : 'POST',url : "/demo/employeeApply",dataType: "text",data: $(".employee-leave").serialize(),error : function() {alert('请求失败');},success : function(data) {alert(data);}});}/*** 上级审核*/function higherAudit() {$.ajax({async : false,cache : false,type : 'POST',url : "/demo/higherLevelAudit",dataType: "text",data: $(".higher-audit").serialize(),error : function() {alert('请求失败');},success : function(data) {alert(data);}});}/*** 部门经理审核*/function managerAudit() {$.ajax({async : false,cache : false,type : 'POST',url : "/demo/divisionManagerAudit",dataType: "text",data: $(".manager-audit").serialize(),error : function() {alert('请求失败');},success : function(data) {alert(data);}});}/*** 上级审核*/function viewTask(taskId, name) {var url = "/demo/viewTask";if(name != "上级审核") {url = "/demo/viewTaskManager";}$.ajax({async : false,cache : false,type : 'POST',url : url,data : {"taskId" : taskId},dataType : "html",error : function() {alert('请求失败');},success : function(data) {$(".result-div").html(data);}});}</script></head><body><!-- 菜单栏 --><div class="main-menu"><button class="show-instance" url="/demo/start">我要请假</button><button class="show-page" url="/demo/toLeave">开始填单</button><button class="show-img">查看流程图</button><button class="show-task">查看任务</button></div><br/>流程实例ID:<input type="text" name="instanceId"/><br/><!-- 操作栏 --><div class="main-body"></div><br/><!-- 结果栏 --><div class="result-div"><embed src="/demo/showImg?instanceId=5070fd58-f859-11e8-a359-484d7ec5762d" style="display:block;width:1000px;height:450px" /><br><!-- <img src="/static/leave-process.png"/> --></div></body></html>
官方demo:/?target=https%3A///Activiti/Activiti/tree/master/activiti-examples
如果觉得《Activiti流程引擎》对你有帮助,请点赞、收藏,并留下你的观点哦!