最近项目中需要用到工作流审批流程,业务功能比较简单,就是员工请假,领导审批同意或者驳回的操作。本来准备自己做一套简单的审批流程(数据库记录下状态的这种),但是考虑到后期的拓展性,可能会有多审批、加签等复杂的操作,还是决定使用工作流框架,最后选择了Activiti。
Activiti是一种轻量级,可嵌入的BPM引擎,而且还设计适用于可扩展的云架构。可以和springboot完美结合。
首先需要了解它的7 个服务接口和28张表:
服务接口 | 说明 |
RepositoryService | 仓库服务,用于管理仓库,比如部署或删除流程定义、读取流程资源等。 |
IdentifyService | 身份服务,管理用户、组以及它们之间的关系。 |
RuntimeService | 运行时服务,管理所有正在运行的流程实例、任务等对象。 |
TaskService | 任务服务,管理任务。 |
FormService | 表单服务,管理和流程、任务相关的表单。 |
HistroyService | 历史服务,管理历史数据。 |
ManagementService | 引擎管理服务,比如管理引擎的配置、数据库和作业等核心对象 |
表 | 说明 |
ACT_RE_* | RE’表示repository。 这个前缀的表包含了流程定义和流程静态资源 (图片,规则,等等) |
ACT_RU_* | RU’表示runtime。这些运行时的表,包含流程实例,任务,变量,异步任务,等运行中的数据。 Activiti只在流程实例执行过程中保存这些数据,在流程结束时就会删除这些记录。 这样运行时表可以一直很小速度很快。 |
ACT_ID_* | ‘ID’表示identity。 这些表包含身份信息,比如用户,组等等。 |
ACT_HI_* | ‘HI’表示history。 这些表包含历史数据,比如历史流程实例, 变量,任务等等。 |
ACT_GE_* | 通用数据, 用于不同场景下,如存放资源文件。 |
表结构详细介绍:https://blog.csdn.net/qq_38011415/article/details/101127222
首先安装下可视化插件,我的用是Idea开发,在插件中心安装actiBPM插件:
这样打开BPM文案可以以图形化的方式查看流程:
如果流程图出现中文乱码,请移步:https://blog.csdn.net/amandalm/article/details/81196710
一、增加依赖
org.activiti
activiti-spring-boot-starter-basic
6.0.0
mybatis
org.mybatis
避坑:https://blog.csdn.net/HXNLYW/article/details/103694280
二、增加配置信息
spring:
# 数据库配置
datasource:
type: com.alibaba.druid.pool.DruidDataSource
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
datasource:
master:
url: xxx
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: xxx
activiti:
url: xxx
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: xxx
# 工作流
activiti:
# 自动部署验证设置:
# true(默认)自动部署流程
# false 不自动部署,需要手动部署发布流程
check-process-definitions: true
# 可选值为: false,true,create-drop,drop-create
# 默认为true。为true表示activiti会对数据库中的表进行更新操作,如果不存在,则进行创建。
database-schema-update: true
自定义数据源
在启动类增加如下内容
@Bean
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.activiti")
public DataSource activitiDataSource() {
return new DruidDataSource();
}
@Bean
public SpringProcessEngineConfiguration springProcessEngineConfiguration(
PlatformTransactionManager transactionManager,
SpringAsyncExecutor springAsyncExecutor) throws IOException {
return baseSpringProcessEngineConfiguration(
activitiDataSource(),
transactionManager,
springAsyncExecutor);
}
三、排除安全配置类
@SpringBootApplication(exclude={
org.activiti.spring.boot.SecurityAutoConfiguration.class
})
如果不排除这个配置类会报如下错误:
Caused by: java.io.FileNotFoundException: class path resource [org/springframework/security/config/annotation/authentication/configurers/GlobalAuthenticationConfigurerAdapter.class] cannot be opened because it does not exist
at org.springframework.core.io.ClassPathResource.getInputStream(ClassPathResource.java:180)
at org.springframework.core.type.classreading.SimpleMetadataReader.(SimpleMetadataReader.java:51)
四、新增bpm流程文件
如果spring.activiti.check-process-definitions配置设置为true,需要在resource目录下新建processes文件夹(activiti默认访问此文件下的流程文件),并新增bpm流程文件。
否则会报流程文件找不到:
Caused by: java.io.FileNotFoundException: class path resource [processes/] cannot be resolved to URL because it does not exist
at org.springframework.core.io.ClassPathResource.getURL(ClassPathResource.java:195)
到此,配置就已经完成了,启动项目,会自动生成28张表。
本文以常见的请假流程为案例实现一个简单的请假审批流程。
逻辑封装
/**
* @author gourd
* @description 工作流服务
* @date 2018/10/30 11:25
**/
@Service
@Slf4j
@Transactional(rollbackFor = Exception.class)
@DS(value = "activiti")
public class WorkFlowServiceImpl implements WorkFlowService {
@Autowired
private RepositoryService repositoryService;
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private HistoryService historyService;
@Autowired
private ProcessEngine processEngine;
public static final String DEAL_USER_ID_KEY = "dealUserId";
public static final String DELEGATE_STATE = "PENDING";
/**
* 启动工作流
*
* @param pdKey
* @param businessKey
* @param variables
* @return
*/
@Override
public String startWorkflow(String pdKey, String businessKey, Map variables) {
ProcessDefinition processDef = getLatestProcDef(pdKey);
if (processDef == null) {
// 部署流程
processEngine.getRepositoryService()
.createDeployment()//创建部署对象
.name(pdKey)
.addClasspathResource("processes/"+pdKey+".bpmn")
.deploy();
processDef = getLatestProcDef(pdKey);
}
ProcessInstance process = runtimeService.startProcessInstanceById(processDef.getId(), businessKey, variables);
return process.getId();
}
/**
* 继续流程
*
* @param taskId
* @param variables
*/
@Override
public void continueWorkflow(String taskId, Map variables){
//根据taskId提取任务
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
DelegationState delegationState = task.getDelegationState();
if(delegationState != null && DELEGATE_STATE.equals(delegationState.toString())){
// 委托任务,先需要被委派人处理完成任务
taskService.resolveTask(taskId,variables);
}else {
// 当前受理人
String dealUserId =variables.get(DEAL_USER_ID_KEY).toString();
// 签收
taskService.claim(taskId, dealUserId);
}
// 设置参数
taskService.setVariables(taskId, variables);
// 完成
taskService.complete(taskId);
}
/**
* 委托流程
* @param taskId
* @param variables
*/
@Override
public void delegateWorkflow(String taskId, Map variables){
// 受委托人
String dealUserId =variables.get(DEAL_USER_ID_KEY).toString();
// 委托
taskService.delegateTask(taskId, dealUserId);
}
/**
* 结束流程
* @param pProcessInstanceId
*/
@Override
public void endWorkflow(String pProcessInstanceId,String deleteReason){
// 结束流程
runtimeService.deleteProcessInstance(pProcessInstanceId, deleteReason);
}
/**
* 获取当前的任务节点
* @param pProcessInstanceId
*/
@Override
public String getCurrentTask(String pProcessInstanceId){
Task task = taskService.createTaskQuery().processInstanceId(pProcessInstanceId).active().singleResult();
return task.getId();
}
/**
*
* 根据用户id查询待办流程实例ID集合
*
*/
@Override
public List findUserProcessIds(String userId, String pdKey, Integer pageNo, Integer pageSize) {
List resultTask;
if(pageSize == 0 ){
// 不分页
resultTask = taskService.createTaskQuery().processDefinitionKey(pdKey)
.taskCandidateOrAssigned(userId).list();
}else {
resultTask = taskService.createTaskQuery().processDefinitionKey(pdKey)
.taskCandidateOrAssigned(userId).listPage(pageNo-1,pageSize);
}
//根据流程实例ID集合
List processInstanceIds = resultTask.stream()
.map(task -> task.getProcessInstanceId())
.collect(Collectors.toList());
return processInstanceIds == null ? new ArrayList<>() : processInstanceIds;
}
/**
* 获取流程图像,已执行节点和流程线高亮显示
*/
@Override
public void getProcessImage(String pProcessInstanceId, HttpServletResponse response) {
log.info("[开始]-获取流程图图像");
// 设置页面不缓存
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("image/png");
InputStream imageStream = null;
try (OutputStream os = response.getOutputStream()){
// 获取历史流程实例
HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery()
.processInstanceId(pProcessInstanceId).singleResult();
if (historicProcessInstance == null) {
throw new ServiceException("获取流程实例ID[" + pProcessInstanceId + "]对应的历史流程实例失败!");
} else {
// 获取流程历史中已执行节点,并按照节点在流程中执行先后顺序排序
List historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(pProcessInstanceId).orderByHistoricActivityInstanceId().asc().list();
// 已执行的节点ID集合
List executedActivityIdList = new ArrayList();
int index = 1;
log.info("获取已经执行的节点ID");
for (HistoricActivityInstance activityInstance : historicActivityInstanceList) {
executedActivityIdList.add(activityInstance.getActivityId());
log.info("第[" + index + "]个已执行节点=" + activityInstance.getActivityId() + " : " +activityInstance.getActivityName());
index++;
}
// 获取流程定义
BpmnModel bpmnModel = repositoryService.getBpmnModel(historicProcessInstance.getProcessDefinitionId());
// 已执行的线集合
List flowIds = getHighLightedFlows(bpmnModel, historicActivityInstanceList);
// 流程图生成器
ProcessDiagramGenerator pec = processEngine.getProcessEngineConfiguration().getProcessDiagramGenerator();
// 获取流程图图像字符流(png/jpg)
imageStream = pec.generateDiagram(bpmnModel, "jpg", executedActivityIdList, flowIds, "宋体", "微软雅黑", "黑体", null, 2.0);
int bytesRead = 0;
byte[] buffer = new byte[8192];
while ((bytesRead = imageStream.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
}
log.info("[完成]-获取流程图图像");
} catch (Exception e) {
log.error("【异常】-获取流程图失败!",e);
}finally {
if(imageStream != null){
try {
imageStream.close();
} catch (IOException e) {
log.error("关闭流异常:",e);
}
}
}
}
public List getHighLightedFlows(BpmnModel bpmnModel, List historicActivityInstances) {
// 24小时制
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 用以保存高亮的线flowId
List highFlows = new ArrayList();
for (int i = 0; i < historicActivityInstances.size() - 1; i++) {
// 对历史流程节点进行遍历
// 得到节点定义的详细信息
FlowNode activityImpl = (FlowNode) bpmnModel.getMainProcess().getFlowElement(historicActivityInstances.get(i).getActivityId());
// 用以保存后续开始时间相同的节点
List sameStartTimeNodes = new ArrayList();
FlowNode sameActivityImpl1 = null;
// 第一个节点
HistoricActivityInstance activityImpl_ = historicActivityInstances.get(i);
HistoricActivityInstance activityImp2_;
for (int k = i + 1; k <= historicActivityInstances.size() - 1; k++) {
// 后续第1个节点
activityImp2_ = historicActivityInstances.get(k);
if (activityImpl_.getActivityType().equals("userTask") && activityImp2_.getActivityType().equals("userTask") &&
df.format(activityImpl_.getStartTime()).equals(df.format(activityImp2_.getStartTime()))) {
// 都是usertask,且主节点与后续节点的开始时间相同,说明不是真实的后继节点
} else {
//找到紧跟在后面的一个节点
sameActivityImpl1 = (FlowNode) bpmnModel.getMainProcess().getFlowElement(historicActivityInstances.get(k).getActivityId());
break;
}
}
// 将后面第一个节点放在时间相同节点的集合里
sameStartTimeNodes.add(sameActivityImpl1);
for (int j = i + 1; j < historicActivityInstances.size() - 1; j++) {
// 后续第一个节点
HistoricActivityInstance activityImpl1 = historicActivityInstances.get(j);
// 后续第二个节点
HistoricActivityInstance activityImpl2 = historicActivityInstances.get(j + 1);
if (df.format(activityImpl1.getStartTime()).equals(df.format(activityImpl2.getStartTime()))) {
// 如果第一个节点和第二个节点开始时间相同保存
FlowNode sameActivityImpl2 = (FlowNode) bpmnModel.getMainProcess().getFlowElement(activityImpl2.getActivityId());
sameStartTimeNodes.add(sameActivityImpl2);
} else {// 有不相同跳出循环
break;
}
}
// 取出节点的所有出去的线
List pvmTransitions = activityImpl.getOutgoingFlows();
// 对所有的线进行遍历
for (SequenceFlow pvmTransition : pvmTransitions) {
// 如果取出的线的目标节点存在时间相同的节点里,保存该线的id,进行高亮显示
FlowNode pvmActivityImpl = (FlowNode) bpmnModel.getMainProcess().getFlowElement(pvmTransition.getTargetRef());
if (sameStartTimeNodes.contains(pvmActivityImpl)) {
highFlows.add(pvmTransition.getId());
}
}
}
return highFlows;
}
/**
* 获取最新版本流程
*
* @param modelName
* @return
*/
private ProcessDefinition getLatestProcDef(String modelName) {
return repositoryService.createProcessDefinitionQuery().processDefinitionKey(modelName).
latestVersion().singleResult();
}
}
controller层调用(个人喜欢用swagger调试)
/**
* @author gourd
*/
@RestController
@Api(tags = "activiti",description = "工作流控制器")
@RequestMapping("/activiti")
@Slf4j
public class ActivitiController {
@Autowired
private WorkFlowService workFlowService;
@PostMapping("/qj-apply")
@ApiOperation(value="启动请假流程")
public BaseResponse startWorkflow(@RequestParam(required = false) String pdKey){
Map param = new HashMap(4){
{
put("applyUserId","001");
put("approveUserIds", Arrays.asList("001","002","003"));
}};
if(StringUtils.isBlank(pdKey)){
pdKey="QjFlow";
}
// 启动流程
String pdId = workFlowService.startWorkflow(pdKey, "QJ001", param);
// 获取请假申请任务节点
String Id = workFlowService.getCurrentTask(pdId);
// 完成请假申请任务节点
Map continueParam = new HashMap(2){
{
put("dealUserId",param.get("applyUserId"));
}};
workFlowService.continueWorkflow(Id,continueParam);
return BaseResponse.ok("请假已提交");
}
@PostMapping("/qj-approve")
@ApiOperation(value="审批请假流程")
public BaseResponse continueWorkflow(@RequestParam String pId,@RequestParam String result){
Map param = new HashMap(2){
{
put("dealUserId","001");
put("result",result);
}};
// 获取请假审批任务节点
String Id = workFlowService.getCurrentTask(pId);
// 完成请假审批任务节点
workFlowService.continueWorkflow(Id,param);
return BaseResponse.ok("审批成功");
}
@PostMapping("/qj-delegate")
@ApiOperation(value="委托请假流程")
public BaseResponse delegateWorkflow(@RequestParam String pId,@RequestParam String userId){
Map param = new HashMap(2){
{
put("dealUserId",userId);
}};
// 获取请假审批任务节点
String Id = workFlowService.getCurrentTask(pId);
// 完成请假审批任务节点
workFlowService.delegateWorkflow(Id,param);
return BaseResponse.ok("委托成功");
}
/**
* 查询用户待办流程实例
* @param userId
* @param pdKey
*/
@GetMapping("/user-process")
@ApiOperation(value="查询用户待办流程实例")
public BaseResponse findUserProcessIds(@RequestParam String userId, @RequestParam(required = false) String pdKey) {
if(StringUtils.isBlank(pdKey)){
pdKey="QjFlow";
}
// 获取流程图
return BaseResponse.ok(workFlowService.findUserProcessIds(userId,pdKey,1,0));
}
/**
* 读取流程资源
* @param pId 流程实例id
*/
@GetMapping("/read-resource")
@ApiOperation(value="读取流程资源")
public void readResource(@RequestParam String pId) {
// 获取流程图
workFlowService.getProcessImage(pId);
}
}
测试截图:
swagger接口文档:
获取流程实例图:http://localhost:8088/gourd/activiti/read-resource?pId=57505
以上就是springboot整合activiti的所有配置和代码,代码均已上传至我的开源项目:gourd-hu;有兴趣的小伙伴可以下载看下,里面整合了许多开发常用的框架和功能。同时本文是我平时学习和工作的一个记录和总结,如有不对的地方,欢迎指正。
====================================================================
代码均已上传至本人的开源项目
spring-cloud-plus:https://blog.csdn.net/HXNLYW/article/details/104635673