《SpringBoot2.0 实战》系列-集成Activiti6.0(案例详解)

 

前言

最近项目中需要用到工作流审批流程,业务功能比较简单,就是员工请假,领导审批同意或者驳回的操作。本来准备自己做一套简单的审批流程(数据库记录下状态的这种),但是考虑到后期的拓展性,可能会有多审批、加签等复杂的操作,还是决定使用工作流框架,最后选择了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插件:

《SpringBoot2.0 实战》系列-集成Activiti6.0(案例详解)_第1张图片

这样打开BPM文案可以以图形化的方式查看流程:

《SpringBoot2.0 实战》系列-集成Activiti6.0(案例详解)_第2张图片

如果流程图出现中文乱码,请移步:https://blog.csdn.net/amandalm/article/details/81196710

《SpringBoot2.0 实战》系列-集成Activiti6.0(案例详解)_第3张图片

整合

一、增加依赖



    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流程文件。

《SpringBoot2.0 实战》系列-集成Activiti6.0(案例详解)_第4张图片

 否则会报流程文件找不到:

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张表。

《SpringBoot2.0 实战》系列-集成Activiti6.0(案例详解)_第5张图片

 案例

本文以常见的请假流程为案例实现一个简单的请假审批流程。

《SpringBoot2.0 实战》系列-集成Activiti6.0(案例详解)_第6张图片

逻辑封装

/**
 * @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接口文档:

《SpringBoot2.0 实战》系列-集成Activiti6.0(案例详解)_第7张图片

 获取流程实例图:http://localhost:8088/gourd/activiti/read-resource?pId=57505

《SpringBoot2.0 实战》系列-集成Activiti6.0(案例详解)_第8张图片

结尾

以上就是springboot整合activiti的所有配置和代码,代码均已上传至我的开源项目:gourd-hu;有兴趣的小伙伴可以下载看下,里面整合了许多开发常用的框架和功能。同时本文是我平时学习和工作的一个记录和总结,如有不对的地方,欢迎指正。

====================================================================

代码均已上传至本人的开源项目

spring-cloud-plus:https://blog.csdn.net/HXNLYW/article/details/104635673

 

你可能感兴趣的:(springboot2.0实战)