Activiti7工作流+SpringBoot

2023-10-26

文章目录

一. 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版本如下:
在这里插入图片描述
    下载离线安装包(在线安装始终失败,应该出于网络限制),地址:http://www.activiti.org/designer/archived/activiti-designer-5.18.0.zip
    离线安装包安装安装依然提示相关包找不到,于是下载另外三个依赖包(org.eclipse.emf.transaction_1.4.0.v20100331-1738.jar、org.eclipse.emf.validation_1.7.0.201306111341.jar、org.eclipse.emf.workspace_1.5.1.v20120328-0001.jar,在Maven仓库可以找到),放到Eclipse的plugin目录下,继续安装,如下:
在这里插入图片描述
    确定后勾选相关选项,完成安装,重启Eclipse,New->Other
在这里插入图片描述
    出现以上标志,则安装完成。

三. 项目搭建

1. 新建Spring Boot工程

    (我的Eclipse已经安装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>https://artifacts.alfresco.com/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=false
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    # SQLException: XAER_INVAL: Invalid arguments (or unsupported command)问题
    xa:
      properties:
        pinGlobalTxToPhysicalConnection: true
        useServerPrepStmts: true

5.3 Activiti相关配置

  # 参考配置https://www.cnblogs.com/liaojie970/p/8857710.html
  activiti:
    # 自动建表
    database-schema: ACTIVITI
    database-schema-update: true
    history-level: full
    db-history-used: true

    注意:
    database-schema-update表示启动时检查数据库表,不存在则创建
    history-level表示哪种情况下使用历史表,这里配置为full表示全部记录历史,方便绘制流程图
    db-history-used为true表示使用历史表,如果不配置,则工程启动后可以检查数据库,只建立了17张表,历史表没有建立,则流程图及运行节点无法展示(暂未找到可行方式)

5.4 附上application.yml完整配置:

# 服务配置
server:
  display-name: actdemo
  port: 8085
  
# Spring相关配置
spring:
  ##数据库连接信息
  datasource:
    # 数据源配置
    url: jdbc:mysql://127.0.0.1:3306/activity?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 888
    driver-class-name: com.mysql.jdbc.Driver
    
    # SQLException: XAER_INVAL: Invalid arguments (or unsupported command)问题
    xa:
      properties:
        pinGlobalTxToPhysicalConnection: true
        useServerPrepStmts: true

  thymeleaf:
    mode: HTML
    encoding: utf-8
    # 禁用缓存
    cache: false
  application:
    # 注册应用名
    name: actdemo
  mvc:
    # 静态资源路径
    static-path-pattern: /static/**
  # 参考配置https://www.cnblogs.com/liaojie970/p/8857710.html
  activiti:
    # 自动建表
    database-schema: ACTIVITI
    database-schema-update: true
    history-level: full
    db-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 2018年12月10日上午11:03:36
	 * @version 1.0
	 */
	@RequestMapping(value="/start")
	@ResponseBody
	public 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();
	}

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 2018年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 2018年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 2018年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 2018年12月10日上午11:26:19
	 * @version 1.0
	 */
	public static List<String> getHighLightedFlows(BpmnModel bpmnModel, ProcessDefinitionEntity processDefinitionEntity, List<HistoricActivityInstance> historicActivityInstances) {
		// 用以保存高亮的线flowId
		List<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 2018年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 org.apache.commons.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 2018年12月10日上午9:30:18
 * @version 1.0
 */
@Controller
@RequestMapping("/demo")
public class DemoController {

private static final Logger logger = LoggerFactory.getLogger(DemoController.class);
	
	/** 流程定义和部署相关的存储服务 */
	@Autowired
	private RepositoryService repositoryService;
	
	/** 流程运行时相关的服务 */
	@Autowired
	private RuntimeService runtimeService;
	
	/** 节点任务相关操作接口 */
	@Autowired
	private TaskService taskService;
	
	/** 流程图生成器 */
	@Autowired
	private ProcessDiagramGenerator processDiagramGenerator;

	/** 历史记录相关服务接口 */
	@Autowired
	private HistoryService historyService;

	
	
	/**
	 * <p>跳转到测试主页面</p>
	 * @return String 测试主页面
	 * @author FRH
	 * @time 2018年12月10日上午11:12:28
	 * @version 1.0
	 */
	@RequestMapping(value="/toIndex.html")
	public String toTestPage() {
		return "/index";
	}
	
	
	
	/**
	 * <p>跳转到上级审核页面</p>
	 * @return String 上级审核页面
	 * @author FRH
	 * @time 2018年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 2018年12月10日上午11:12:50
	 * @version 1.0
	 */
	@RequestMapping(value="/start")
	@ResponseBody
	public 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 2018年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 2018年12月10日上午11:15:09
	 * @version 1.0
	 */
	@RequestMapping(value="/employeeApply")
	@ResponseBody
	public String employeeApply(HttpServletRequest request){
		/*
		 * 获取请求参数
		 */
		String taskId = request.getParameter("taskId"); // 任务ID
		String 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);
        taskService.complete(task.getId(), map);
        logger.info("执行【员工申请】环节,流程推动到【上级审核】环节");
        
        /*
         * 返回成功
         */
		return "success";
	}
	
	
	/**
	 * <p>跳转到上级审核页面</p>
	 * @return String 页面
	 * @author FRH
	 * @time 2018年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 2018年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 2018年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);
		taskService.complete(taskId, map);
		return "success";
	}
	
	
	
	/**
	 * <p>部门经理审核</p>
	 * @param request 请求
	 * @return String 受理结果
	 * @author FRH
	 * @time 2018年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);
		taskService.complete(taskId, map);
		return "success";
	}
	
	
	/**
	 * <p>查看任务</p>
	 * @param request 请求
	 * @return String  任务展示页面
	 * @author FRH
	 * @time 2018年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 2018年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 2018年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="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<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>
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Activiti7工作流+SpringBoot 的相关文章

随机推荐

  • flink-cdc 实现oracle 实时同步到kudu

    其实网上也有很多相关话题的代码实现 但是发现有很多坑 在 腾讯官方文档中 有介绍 但是屏蔽了很多细节 我做了以下四点修改才能正常运行 1 前置条件 保证oracle中相关表开启了归档日志和补充日志 因为flink cdc基于debezium
  • IOS 多线程初探(二) - Operation Object

    上次简单介绍了使用NSThread来创建线程的方法 今天简单介绍使用Operation Object来创建线程 Operation Object简介 将要执行的任务 即函数 封装成操作对象NSOperation 并将对象放置到NSOpera
  • [C++STL] 严格弱序(less函数、小于号重载)

    前言 严格弱序 stick weak ordering 在以下场景会涉及到 对一个容器进行排序时 如使用std sort 使用有序关联容器时 如使用std set std map 使用std less时 重载 lt 操作符 小于 其中 st
  • Vue 中使用 v-for 展示不同的图片

    1 示例前提 展示一个数组对象数据 数组对象中没有图片字段 图片字段在本地保存 根据不同的id 下标 展示不同的图片 示例代码 template 中的代码
  • 数字图像处理扭曲效果——挤压效果

    挤压效果 挤压效果是将图像向内挤压 产生收缩变形 挤压效果的实现可以看成是数学极坐标的一种体现 将当前像素点 图像正中心点和过中心点的水平线这三要素画出一个极坐标 然后根据用户指定的挤压度 在当前点与中心点所连的直线上映射出一个像素点 最后
  • VulnHub-Tr0II

    一 信息收集 将靶机部署好之后改成NAT模式 扫描本网段发现目标ip 进一步探测ip 通过进一步的探测发现目标开启了21 22 80三个端口 先去80端口看一眼web服务 一张图没其它内容 扫一下目录看看 挨个访问也没有啥特殊的地方 考虑一
  • PropertyDescriptor获取非标准java bean属性的getter和setter时候的一个问题

    1 import java beans IntrospectionException 2 import java beans PropertyDescriptor 3 import java lang reflect InvocationT
  • 显著性水平对应的临界值_统计学问题 常有的显著性水平a 所对应的Z值是哪些...

    展开全部 z a 2 指的是标准正态分布的双侧临界值 z a 当然就是单侧临界值 a 阿尔法 指62616964757a686964616fe4b893e5b19e31333431356638的是显著水平 一般是0 05 0 01等 而95
  • Java 动态代理,invoke() 自动调用原理,invoke() 参数

    Java 动态代理 invoke 自动调用原理 invoke 参数 本文介绍了静态代理和动态代理的概念 并且用代码 注释 图示和源码的方式来分析动态代理原理和invoke 自动调用原理 学习动态代理 先从静态代理入手 静态代理 假如现在我们
  • 每日一练——Python字典格式读取文件

    现有一个user csv文件 内容如下 name username email 杨洋 yangy yangy sina com 贾子豪 jiazh jiazh 126 com 于飞 yuf yuf 163 com 田宇辰 tianych t
  • HttpGet和HttpPost请求

    HttpPost请求 public static void main String args throws IOException String url null 请求地址 Map
  • 分类预测

    作者简介 热爱科研的Matlab仿真开发者 修心和技术同步精进 matlab项目合作可私信 个人主页 Matlab科研工作室 个人信条 格物致知 内容介绍 极限学习机 作为一种新兴的机器学习方法 已经成为了一个热门的研究方向 随机确定单隐含
  • [机器学习与scikit-learn-23]:算法-聚类-KMeans算法的工作原理

    作者主页 文火冰糖的硅基工坊 文火冰糖 王文兵 的博客 文火冰糖的硅基工坊 CSDN博客 本文网址 目录 第1章 KMeans概述 1 1 什么是簇与质心 1 2 质心的计算过程 第2章 聚合指标 2 1 距离和度量方法 2 2 聚合的意义
  • window.open()打开新页面,并往新页面插入内容

    定义和用法 open 方法用于打开一个新的浏览器窗口或查找一个已命名的窗口 语法 window open URL name specs replace 参数说明参考https www runoob com jsref met win ope
  • 使用Spring的JAVA Mail支持简化邮件发送

    闲来无事 翻看 Spring in Action 发现Spring集成了对JAVA Mail的支持 有点小激动的看了一遍 嗯 话说真的简单了很多 Spring的邮件发送的核心是MailSender接口 在Spring3 0中提供了一个实现类
  • 前端学习之初识HTML以及块级元素和行内元素的区别(1)

    目录 一 什么是HTML HTML是HyperType Markup Language 超文本标记语言 HTML的特点 Html标签注意规范 建议镜面嵌套 不建议交叉嵌套 二 块级元素和行内元素 块级元素 行内元素 一 什么是HTML HT
  • 贪心(1)田忌赛马

    目录 一 贪心算法 1 贪心选择性质 2 最优子结构性质 二 田忌赛马 三 OJ实战 CSU 1722 Race 力扣 870 优势洗牌 HDU 1338 Game Prediction HDU 1052 POJ 2287 UVA 1344
  • 神经网络之LN(Layer Normalization)

    上一篇博客提到了BN不适用于RNN等动态网络以及batchsize较小的时候 而LN可以 这篇博客就对LN做一个简单的介绍 深度网络中的数据维度一般是 N C H W 或者 N H W C 格式 N是batch size H W是featu
  • Redis主从复制及其实现原理

    Redis主从复制及其实现原理 首先 简单介绍一下什么是Redis主从复制 假如我们服务中用到了Redis 并且只有一台Redis服务器 如果某个时刻该Redis服务挂了 那么会导致整个服务的Redis不可用 在此期间 大量的请求将会直接打
  • Activiti7工作流+SpringBoot

    文章目录 一 Activiti相关概念 1 Activiti介绍 2 核心类 2 1 ProcessEngine 2 2 服务 Service 类 2 2 1 TaskService 2 2 2 RepositoryService 2 2