【Activiti7系列】基于Spring Security的Activiti7工作流管理系统简介及实现(附源码)(下篇)

news2024/11/26 15:43:40

作者:后端小肥肠

上篇:【Activiti7系列】基于Spring Security的Activiti7工作流管理系统简介及实现(上篇)_spring security activiti7-CSDN博客

目录

1.前言

2. 核心代码

2.1. 流程定义模型管理

2.1.1. 新增流程定义模型数据

2.1.2. 通过流程定义模型id部署流程定义

2.1.3. 导出流程定义模型zip压缩包

2.2. 流程定义管理

2.2.1.  更新流程状态:激活(启动)或者挂起(暂停)

2.2.2.  导出流程定义文件(xml,png)

2.2.3. 上传zip、bpmn、xml后缀的文件来进行部署流程定义

2.3. 流程配置管理

2.4. 流程实例管理

2.4.1. 提交申请,启动流程实例

2.4.2. 撤回申请

2.4.3. 挂起或激活流程实例

2.4.4. 通过流程实例id获取历史流程图

2.4.5. 通过流程实例id获取任务办理历史记录

2.5. 任务管理

2.5.1.  查询当前用户的待办任务

 2.5.2. 获取目标节点(下一个节点)

2.5.3. 完成任务

2.5.4. 获取历史任务节点,用于驳回功能

2.5.5. 驳回历史节点

2.6. 请假申请管理

3. 源码地址

4. 结语


1.前言

在《基于Spring Security的Activiti7工作流管理系统简介及实现(上篇)》中,向大家展示了工作流管理系统的功能界面及模块,具体应用场景,在本文中将会讲解该工作流管理系统实现的具体技术细节及核心代码。

本文面向人群为有工作流基础的后端人员,如对您有帮助请三连支持一下小肥肠~

2. 核心代码

本章只做代码简介(部分代码,简单的crud不介绍)及核心代码讲解,文末会提供源代码链接(仅后端)。

2.1. 流程定义模型管理

流程定义模型管理对应前端的模型管理界面,相关接口包括新增流程定义模型数据条件分页查询流程定义模型数据通过流程定义模型id部署流程定义导出流程定义模型zip压缩包删除流程定义模型

2.1.1. 新增流程定义模型数据
    public Result add(ModelAddREQ req) throws Exception {
        /*String name = "请假流程模型";
        String key = "leaveProcess";
        String desc = "请输入描述信息……";*/
        int version = 0;

        // 1. 初始空的模型
        Model model = repositoryService.newModel();
        model.setName(req.getName());
        model.setKey(req.getKey());
        model.setVersion(version);

        // 封装模型json对象
        ObjectNode objectNode  = objectMapper.createObjectNode();
        objectNode.put(ModelDataJsonConstants.MODEL_NAME, req.getName());
        objectNode.put(ModelDataJsonConstants.MODEL_REVISION, version);
        objectNode.put(ModelDataJsonConstants.MODEL_DESCRIPTION, req.getDescription());
        model.setMetaInfo(objectNode.toString());
        // 保存初始化的模型基本信息数据
        repositoryService.saveModel(model);

        // 封装模型对象基础数据json串
        // {"id":"canvas","resourceId":"canvas","stencilset":{"namespace":"http://b3mn.org/stencilset/bpmn2.0#"},"properties":{"process_id":"未定义"}}
        ObjectNode editorNode = objectMapper.createObjectNode();
        ObjectNode stencilSetNode = objectMapper.createObjectNode();
        stencilSetNode.put("namespace", "http://b3mn.org/stencilset/bpmn2.0#");
        editorNode.replace("stencilset", stencilSetNode);
        // 标识key
        ObjectNode propertiesNode = objectMapper.createObjectNode();
        propertiesNode.put("process_id", req.getKey());
        editorNode.replace("properties", propertiesNode);

        repositoryService.addModelEditorSource(model.getId(), editorNode.toString().getBytes("utf-8"));

        return Result.ok(model.getId());
    }

上述代码实现了创建一个基于 Activiti 7 的工作流模型的功能。关键步骤包括初始化模型对象,封装模型的元信息和基础数据为 JSON 字符串,以及将该字符串保存到模型编辑器中。最终返回新创建模型的ID作为结果。

新增流程定义模型数据主要涉及到了 Activiti 7 中的模型管理相关的表,包括:

  1. ACT_RE_MODEL:用于存储模型的基本信息,如模型名称、键、版本等。
  2. ACT_GE_BYTEARRAY:存储模型编辑器的源数据,即模型对象的基础数据 JSON 字符串。

这些表存储了创建的工作流模型的信息,包括其名称、键、版本、元信息和基础数据,以便后续的流程定义和流程实例化。

2.1.2. 通过流程定义模型id部署流程定义
    public Result deploy(String modelId) throws Exception {
        // 1. 查询流程定义模型json字节码
        byte[] jsonBytes = repositoryService.getModelEditorSource(modelId);
        if(jsonBytes == null) {
           return Result.error("模型数据为空,请先设计流程定义模型,再进行部署");
        }
        // 将json字节码转为 xml 字节码,因为bpmn2.0规范中关于流程模型的描述是xml格式的,而activiti遵守了这个规范
        byte[] xmlBytes = bpmnJsonXmlBytes(jsonBytes);
        if(xmlBytes == null) {
            return Result.error("数据模型不符合要求,请至少设计一条主线流程");
        }
        // 2. 查询流程定义模型的图片
        byte[] pngBytes = repositoryService.getModelEditorSourceExtra(modelId);

        // 查询模型的基本信息
        Model model = repositoryService.getModel(modelId);

        // xml资源的名称 ,对应act_ge_bytearray表中的name_字段
        String processName = model.getName() + ".bpmn20.xml";
        // 图片资源名称,对应act_ge_bytearray表中的name_字段
        String pngName = model.getName() + "." + model.getKey() + ".png";

        // 3. 调用部署相关的api方法进行部署流程定义
        Deployment deployment = repositoryService.createDeployment()
                .name(model.getName()) // 部署名称
                .addString(processName, new String(xmlBytes, "UTF-8")) // bpmn20.xml资源
                .addBytes(pngName, pngBytes) // png资源
                .deploy();

        // 更新 部署id 到流程定义模型数据表中
        model.setDeploymentId(deployment.getId());
        repositoryService.saveModel(model);

        return Result.ok();
    }

上述代码实现了根据给定的模型ID部署流程定义的功能。它首先查询模型的 JSON 字节码,并将其转换为符合 BPMN 2.0 规范的 XML 字节码,然后查询模型的图片字节码。接着,通过创建部署对象并添加相应的资源文件进行流程定义的部署,最后更新模型的部署ID,并返回部署成功的结果。 

 通过流程定义模型id部署流程定义涉及了 Activiti 7 中的以下几张表:

  1. ACT_RE_MODEL:用于存储模型的基本信息,如模型名称、键、版本等。
  2. ACT_GE_BYTEARRAY:存储模型的编辑器源数据、XML 格式的流程定义文件以及流程图片等资源数据。
  3. ACT_RE_DEPLOYMENT:存储流程部署的相关信息,如部署名称、部署时间等。
2.1.3. 导出流程定义模型zip压缩包
    public void exportZip(String modelId, HttpServletResponse response) {
        ZipOutputStream zipos = null;
        try {
            // 实例化zip输出流
            zipos = new ZipOutputStream(response.getOutputStream());

            // 压缩包文件名
            String zipName = "模型不存在";

            // 1. 查询模型基本信息
            Model model = repositoryService.getModel(modelId);
            if(model != null) {
                // 2. 查询流程定义模型的json字节码
                byte[] bpmnJsonBytes = repositoryService.getModelEditorSource(modelId);
                // 2.1 将json字节码转换为xml字节码
                byte[] xmlBytes = bpmnJsonXmlBytes(bpmnJsonBytes);
                if(xmlBytes == null) {
                    zipName = "模型数据为空-请先设计流程定义模型,再导出";
                }else {
                    // 压缩包文件名
                    zipName = model.getName() + "." + model.getKey() + ".zip";

                    // 将xml添加到压缩包中(指定xml文件名:请假流程.bpmn20.xml )
                    zipos.putNextEntry(new ZipEntry(model.getName() + ".bpmn20.xml"));
                    zipos.write(xmlBytes);

                    // 3. 查询流程定义模型的图片字节码
                    byte[] pngBytes = repositoryService.getModelEditorSourceExtra(modelId);
                    if(pngBytes != null) {
                        // 图片文件名(请假流程.leaveProcess.png)
                        zipos.putNextEntry(new ZipEntry(model.getName() + "." + model.getKey() + ".png"));
                        zipos.write(pngBytes);
                    }

                }
            }
            response.setContentType("application/octet-stream");
            response.setHeader("Content-Disposition",
                    "attachment; filename=" + URLEncoder.encode(zipName, "UTF-8") + ".zip");
            // 刷出响应流
            response.flushBuffer();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(zipos != null) {
                try {
                    zipos.closeEntry();
                    zipos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

这段代码实现了根据给定的模型ID导出流程定义及相关图片的功能。它首先查询模型的基本信息,包括模型名称和键,然后查询模型的 JSON 字节码,并将其转换为符合 BPMN 2.0 规范的 XML 字节码。接着,将 XML 文件和模型的图片字节码压缩成一个 ZIP 文件,通过 HttpServletResponse 输出给用户进行下载。 

2.2. 流程定义管理

流程定义管理对应前端的流程管理界面,相关接口包括条件分页查询相同key的最新版本的流程定义列表数据更新流程状态:激活(启动)或者挂起(暂停)、删除流程定义导出流程定义文件(xml,png)上传zip、bpmn、xml后缀的文件来进行部署流程定义

2.2.1.  更新流程状态:激活(启动)或者挂起(暂停)

前端界面:

后端代码: 

    public Result updateProcDefState(String ProcDefiId) {
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
                .processDefinitionId(ProcDefiId)
                .singleResult();
        // 判断是否挂起,true则挂起,false则激活
        if(processDefinition.isSuspended()) {
            // 将当前为挂起状态更新为激活状态
            // 参数说明:参数1:流程定义id,参数2:是否激活(true是否级联对应流程实例,激活了则对应流程实例都可以审批),参数3:什么时候激活,如果为null则立即激活,如果为具体时间则到达此时间后激活
            repositoryService.activateProcessDefinitionById(ProcDefiId, true, null);
        }else {
            // 将当前为激活状态更新为挂起状态
            // 参数说明:参数1:流程定义id,参数2:是否挂起(true是否级联对应流程实例,挂起了则对应流程实例都不可以审批),参数3:什么时候挂起,如果为null则立即挂起,如果为具体时间则到达此时间后挂起
            repositoryService.suspendProcessDefinitionById(ProcDefiId, true, null);
        }
        return Result.ok();
    }
2.2.2.  导出流程定义文件(xml,png)
@GetMapping("/export/{type}/{definitionId}")
public void exportFile(@PathVariable String type,
                           @PathVariable String definitionId,
                           HttpServletResponse response) {
        try {
            ProcessDefinition processDefinition = repositoryService.getProcessDefinition(definitionId);

            String resourceName = "文件不存在";

            if("xml".equals(type)) {
                // 获取的是 xml 资源名
                resourceName = processDefinition.getResourceName();
            }else if("png".equals(type)) {
                // 获取 png 图片资源名
                resourceName = processDefinition.getDiagramResourceName();
            }

            // 查询到相关的资源输入流 (deploymentId, resourceName)
            InputStream input =
                    repositoryService.getResourceAsStream(processDefinition.getDeploymentId(), resourceName);

            // 创建输出流
            response.setHeader("Content-Disposition",
                    "attachment; filename=" + URLEncoder.encode(resourceName, "UTF-8"));

            // 流的拷贝放到设置请求头下面,不然文件大于10k可能无法导出
            IOUtils.copy(input, response.getOutputStream());

            response.flushBuffer();
        } catch (Exception e) {
            e.printStackTrace();
            log.error("导出文件失败:{}", e.getMessage());
        }
    }

这段代码实现了根据流程定义ID导出流程定义文件(XML 或 PNG 格式)的功能。它首先根据流程定义ID查询相关的流程定义信息,然后根据用户请求的类型(XML 或 PNG)获取对应的资源名。接着,通过 repositoryService.getResourceAsStream() 方法获取资源的输入流,并将其写入 HttpServletResponse 的输出流中,实现文件的下载。 

2.2.3. 上传zip、bpmn、xml后缀的文件来进行部署流程定义
@PostMapping("/file/deploy")    
public Result deployByFile(@RequestParam("file") MultipartFile file) {
        try {
            // 文件名+后缀名
            String filename = file.getOriginalFilename();
            // 文件后缀名
            String suffix = filename.substring(filename.lastIndexOf(".") + 1).toUpperCase();

            InputStream input = file.getInputStream();

            DeploymentBuilder deployment = repositoryService.createDeployment();
            if("ZIP".equals(suffix)) {
                // zip
                deployment.addZipInputStream(new ZipInputStream(input));
            }else {
                // xml 或 bpmn
                deployment.addInputStream(filename, input);
            }

            // 部署名称
            deployment.name(filename.substring(0, filename.lastIndexOf(".")));

            // 开始部署
            deployment.deploy();

            return Result.ok();
        } catch (IOException e) {
            e.printStackTrace();
            log.error("部署失败:" + e.getMessage());
            return Result.error("部署失败");
        }

    }

这段代码实现了通过上传文件部署流程定义的功能。它接受一个 MultipartFile 对象作为参数,获取上传文件的文件名和后缀名,并根据后缀名判断文件类型(ZIP 或 XML/BPMN)。然后根据文件类型,使用相应的方法将文件内容添加到部署构建器中,设置部署名称,并最终调用 deploy() 方法进行部署。 

2.3. 流程配置管理

流程配置主要是将流程定义与具体的业务(如请假,借款)进行绑定。在实际项目中建议在表中配置死即可。

在上图中,关联路由名对应前端路由名称,关联路由组件名对应前端表单名称:

流程配置绑定表如下图所示:

 只要在上述表中将流程定义KEY和前端参数(路由名,表单名)进行绑定即可。后台代码如下:

   @PutMapping
    public Result saveOrUpdate(@RequestBody ProcessConfig processConfig) {
        boolean b = processConfigService.saveOrUpdate(processConfig);
        if(b) {
            return Result.ok();
        }else {
            return Result.error("操作失败");
        }
    }

2.4. 流程实例管理

流程实例管理对应前端的业务办理界面(请假申请、借款申请),相关接口包括提交申请,启动流程实例撤回申请挂起或激活流程实例通过流程实例id获取申请表单组件名等。

2.4.1. 提交申请,启动流程实例

前端界面:

在本工作流管理系统中,需要在流程启动时动态指定一级审批用户,我这里指定的是username,为了更好的用户体验可以改为指定用户的真实姓名,通过下拉框来选择审批人。

后端代码:

    public Result startProcess(StartREQ req) {
        // 1. 通过业务路由名获取流程配置信息:流程定义key和表单组件名(查询历史审批记录需要)
        ProcessConfig processConfig =
                processConfigService.getByBusinessRoute(req.getBusinessRoute());

        // 2. 表单组件名设置到流程变量中,后面查询历史审批记录需要
        Map<String, Object> variables = req.getVariables(); // 前端已经传递了当前申请信息{entity: {业务申请数据}}
        variables.put("formName", processConfig.getFormName());

        // 判断办理人为空,则直接结束
        List<String> assignees = req.getAssignees();
        if(CollectionUtils.isEmpty(assignees)) {
            return Result.error("请指定审批人");
        }

        // 3. 启动流程实例(提交申请)
        Authentication.setAuthenticatedUserId(UserUtils.getUsername());
        ProcessInstance pi =
                runtimeService.startProcessInstanceByKey(processConfig.getProcessKey(),
                        req.getBusinessKey(), variables);

        // 将流程定义名称 作为 流程实例名称
        runtimeService.setProcessInstanceName(pi.getProcessInstanceId(), pi.getProcessDefinitionName());


        // 4. 设置任务办理人
        List<Task> taskList = taskService.createTaskQuery().processInstanceId(pi.getId()).list();
        for (Task task : taskList) {
            if(assignees.size() == 1) {
                // 如果只能一个办理人,则直接设置为办理人
               taskService.setAssignee(task.getId(), assignees.get(0));
            }else {
                // 多个办理人,则设置为候选人
                for(String assignee: assignees) {
                    taskService.addCandidateUser(task.getId(), assignee);
                }
            }
        }

        // 5. 更新业务状态为:办理中, 和流程实例id
        return businessStatusService.updateState(req.getBusinessKey(),
                BusinessStatusEnum.PROCESS,
                pi.getProcessInstanceId());
    }

这段代码实现了启动流程实例的功能。首先根据业务路由名获取流程配置信息,设置表单组件名到流程变量中。然后判断办理人是否为空,若为空则返回错误信息。接着通过设置认证用户为当前用户启动流程实例,将流程定义名称作为流程实例名称,并设置任务办理人。最后更新业务状态为办理中,并返回更新结果。 

启动流程实例涉及了 Activiti 7 中的以下几张表:

  1. ACT_RU_TASK:用于存储流程任务的运行时信息,包括任务的唯一标识、流程实例ID、任务名称等。
  2. ACT_RU_PROCESS_INSTANCE:存储流程实例的运行时信息,包括流程实例的唯一标识、流程定义ID、当前活动节点等。
  3. ACT_RU_VARIABLE:用于存储流程实例的运行时变量信息,包括流程实例ID、变量名称、变量值等。
  4. ACT_HI_TASKINST:存储历史流程任务的信息,包括任务的执行过程、持续时间等。
  5. ACT_HI_PROCINST:存储历史流程实例的信息,包括流程实例的启动时间、结束时间等。
  6. ACT_HI_ACTINST:存储历史流程执行的信息,包括每个流程实例的执行路径、执行活动的持续时间等。
2.4.2. 撤回申请
    public Result cancel(String businessKey, String procInstId, String message) {
        // 1. 删除当前流程实例
        runtimeService.deleteProcessInstance(procInstId,
                UserUtils.getUsername() + " 主动撤回了当前申请:" + message);

        // 2. 删除历史记录
        historyService.deleteHistoricProcessInstance(procInstId);
        historyService.deleteHistoricTaskInstance(procInstId);

        // 3. 更新业务状态
        return businessStatusService.updateState(businessKey, BusinessStatusEnum.CANCEL, "");
    }

这段代码实现了取消流程实例的功能。它首先通过流程实例ID删除当前运行中的流程实例,并添加一条撤回消息作为删除原因。然后删除相关的历史记录,包括历史流程实例和历史任务实例。最后更新业务状态为取消,并返回更新结果。

撤回申请涉及了 Activiti 7 中的以下几张表:

  1. ACT_RU_PROCESS_INSTANCE:用于存储流程实例的运行时信息,包括流程实例的唯一标识、当前活动节点等。
  2. ACT_HI_PROCINST:存储历史流程实例的信息,包括流程实例的启动时间、结束时间等。
  3. ACT_HI_TASKINST:存储历史任务实例的信息,包括任务的执行过程、持续时间等。
2.4.3. 挂起或激活流程实例

前端界面:

后端代码:

  @PutMapping("/state/{procInstId}")
    public Result updateProcInstState(@PathVariable String procInstId) {
        // 1. 查询指定流程实例的数据
        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
                .processInstanceId(procInstId)
                .singleResult();

        // 2. 判断当前流程实例的状态
        if(processInstance.isSuspended()) {
            // 如果是已挂起,则更新为激活状态
            runtimeService.activateProcessInstanceById(procInstId);
        }else {
            // 如果是已激活,则更新为挂起状态
            runtimeService.suspendProcessInstanceById(procInstId);
        }

        return Result.ok();
    }

这段代码实现了更新流程实例状态的功能。它首先查询指定流程实例的数据,然后判断当前流程实例的状态,若是已挂起则更新为激活状态,若是已激活则更新为挂起状态。最后返回更新结果。 

2.4.4. 通过流程实例id获取历史流程图

前端界面:

后端代码:

    public void getHistoryProcessImage(String prodInstId, HttpServletResponse response) {
        InputStream inputStream = null;
        try {
            // 1.查询流程实例历史数据
            HistoricProcessInstance instance = historyService.createHistoricProcessInstanceQuery()
                    .processInstanceId(prodInstId).singleResult();

            // 2. 查询流程中已执行的节点,按时开始时间降序排列
            List<HistoricActivityInstance> historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery()
                    .processInstanceId(prodInstId)
                    .orderByHistoricActivityInstanceStartTime().desc()
                    .list();

            // 3. 单独的提取高亮节点id ( 绿色)
            List<String> highLightedActivityIdList =
                    historicActivityInstanceList.stream()
                        .map(HistoricActivityInstance::getActivityId).collect(Collectors.toList());

            // 4. 正在执行的节点 (红色)
            List<Execution> runningActivityInstanceList = runtimeService.createExecutionQuery()
                    .processInstanceId(prodInstId).list();

            List<String> runningActivityIdList = new ArrayList<>();
            for (Execution execution : runningActivityInstanceList) {
                if(StringUtils.isNotEmpty(execution.getActivityId())) {
                    runningActivityIdList.add(execution.getActivityId());
                }
            }

            // 获取流程定义Model对象
            BpmnModel bpmnModel = repositoryService.getBpmnModel(instance.getProcessDefinitionId());

            // 实例化流程图生成器
            CustomProcessDiagramGenerator generator = new CustomProcessDiagramGenerator();
            // 获取高亮连线id
            List<String> highLightedFlows = generator.getHighLightedFlows(bpmnModel, historicActivityInstanceList);
            // 生成历史流程图
            inputStream = generator.generateDiagramCustom(bpmnModel, highLightedActivityIdList,
                    runningActivityIdList, highLightedFlows,
                    "宋体", "微软雅黑", "黑体");

            // 响应相关图片
            response.setContentType("image/svg+xml");
            byte[] bytes = IOUtils.toByteArray(inputStream);
            ServletOutputStream outputStream = response.getOutputStream();
            outputStream.write(bytes);
            outputStream.flush();
            outputStream.close();
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            if( inputStream != null){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

这段代码实现了根据流程实例ID获取历史流程图的功能。它首先查询指定流程实例的历史数据和已执行的节点信息,并提取出高亮节点和正在执行的节点的ID列表。然后根据流程定义的模型对象和节点信息,使用自定义的流程图生成器生成历史流程图,并将流程图以 SVG 格式返回给前端。 

2.4.5. 通过流程实例id获取任务办理历史记录

前端界面:

后端代码:

    public Result getHistoryInfoList(String procInstId) {
        // 查询每任务节点历史办理情况
        List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery()
                .processInstanceId(procInstId)
                .orderByHistoricTaskInstanceStartTime()
                .asc()
                .list();

        List<Map<String, Object>> records = new ArrayList<>();
        for (HistoricTaskInstance hti : list) {
            Map<String, Object> result = new HashMap<>();
            result.put("taskId", hti.getId()); // 任务ID
            result.put("taskName", hti.getName()); // 任务名称
            result.put("processInstanceId", hti.getProcessInstanceId()); //流程实例ID
            result.put("startTime", DateUtils.format(hti.getStartTime())); // 开始时间
            result.put("endTime", DateUtils.format(hti.getEndTime())); // 结束时间
            result.put("status", hti.getEndTime() == null ? "待处理": "已处理"); // 状态
            result.put("assignee", hti.getAssignee()); // 办理人

            // 撤回原因
            String message = hti.getDeleteReason();
            if(StringUtils.isEmpty(message)) {
                List<Comment> taskComments = taskService.getTaskComments(hti.getId());
                message = taskComments.stream()
                        .map(m -> m.getFullMessage()).collect(Collectors.joining("。"));
            }
            result.put("message", message);

            records.add(result);
        }

        return Result.ok(records);
    }

这段代码实现了查询指定流程实例的历史任务信息列表的功能。它首先通过历史任务实例查询服务查询指定流程实例的历史任务信息,并按照任务开始时间升序排序。然后遍历历史任务列表,将每个历史任务的相关信息封装到一个 Map 中,并将所有的 Map 组成一个列表返回给调用方,包括任务ID任务名称流程实例ID任务开始时间任务结束时间任务状态办理人以及撤回原因等。 

2.5. 任务管理

任务管理对应前端待办任务和已办任务界面,包含查询当前用户的待办任务获取目标节点(下一个节点)完成任务获取历史任务节点用于驳回功能驳回历史节点等接口。

2.5.1.  查询当前用户的待办任务
 @PostMapping("/list/wait")
    public Result findWaitTask(@RequestBody TaskREQ req) {

        String assignee = UserUtils.getUsername();

        TaskQuery query = taskService.createTaskQuery()
                .taskCandidateOrAssigned(assignee) // 候选人或者办理人
                .orderByTaskCreateTime().asc();

        if(StringUtils.isNotEmpty(req.getTaskName())) {
            query.taskNameLikeIgnoreCase("%" + req.getTaskName() + "%");
        }
        // 分页查询
        List<Task> taskList = query.listPage(req.getFirstResult(), req.getSize());

        long total = query.count();

        List<Map<String, Object>> records = new ArrayList<>();
        for (Task task : taskList) {
            Map<String, Object> result = new HashMap<>();
            result.put("taskId", task.getId());
            result.put("taskName", task.getName());
            result.put("processStatus", task.isSuspended() ? "已暂停": "已启动");
            result.put("taskCreateTime", DateUtils.format(task.getCreateTime()) );
            result.put("processInstanceId", task.getProcessInstanceId());
            result.put("executionId", task.getExecutionId());
            result.put("processDefinitionId", task.getProcessDefinitionId());
            // 任务办理人: 如果是候选人则没有值,办理人才有
            result.put("taskAssignee", task.getAssignee());

            // 查询流程实例
            ProcessInstance pi = runtimeService.createProcessInstanceQuery()
                    .processInstanceId(task.getProcessInstanceId()).singleResult();
            result.put("processName", pi.getProcessDefinitionName());
            result.put("version", pi.getProcessDefinitionVersion());
            result.put("proposer", pi.getStartUserId());
            result.put("businessKey", pi.getBusinessKey());

            records.add(result);
        }


        Map<String, Object> result = new HashMap<>();
        result.put("total", total);
        result.put("records", records);
        return Result.ok(result);
    }

这段代码实现了查询待办任务列表的功能。它首先获取当前用户的用户名作为任务的候选人或办理人,然后根据任务查询条件构建任务查询对象,并按任务创建时间升序排列。接着根据分页参数查询待办任务列表,并统计总数。最后,将待办任务的相关信息(如任务ID任务名称流程状态任务创建时间流程实例ID等)封装到一个列表中,并返回给调用方。 

 2.5.2. 获取目标节点(下一个节点)

本工作流框架支持动态指定审批人,故完成本节点审批时,需要动态获取下一任务节点,方便在本节点通过审批后动态指定下一个节点审批人。

后端代码: 

 @GetMapping("/next/node")
    public Result getNextNodeInfo(@RequestParam String taskId) {
        Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
        // 2. 从当前任务信息中获取此流程定义id,
        String processDefinitionId = task.getProcessDefinitionId();
        // 3. 拿到流程定义id后可获取此bpmnModel对象
        BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);

        // 4. 通过任务节点id,来获取当前节点信息
        FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey());
        // 封装下一个用户任务节点信息
        List<Map<String, Object>> nextNodes = new ArrayList<>();
        getNextNodes(flowElement, nextNodes);

        return Result.ok(nextNodes);
    }

    public void getNextNodes(FlowElement flowElement, List<Map<String, Object>> nextNodes) {
        // 获取当前节点的连线信息
        List<SequenceFlow> outgoingFlows = ((FlowNode) flowElement).getOutgoingFlows();
        // 当前节点的所有下一节点出口
        for (SequenceFlow outgoingFlow : outgoingFlows) {
            // 下一节点的目标元素
            FlowElement nextFlowElement = outgoingFlow.getTargetFlowElement();
            if(nextFlowElement instanceof UserTask) {
                // 用户任务,则获取响应给前端设置办理人或者候选人
                Map<String, Object> node = new HashMap<>();
                node.put("id", nextFlowElement.getId()); // 节点id
                node.put("name", nextFlowElement.getName()); // 节点名称
                nextNodes.add(node);
            }else if(nextFlowElement instanceof EndEvent) {
                break;
            }else if(nextFlowElement instanceof ParallelGateway // 并行网关
                || nextFlowElement instanceof ExclusiveGateway) { // 排他网关
                getNextNodes(nextFlowElement, nextNodes);
            }
        }
    }

 这段代码实现了获取指定任务的下一个节点信息的功能。它首先根据任务ID查询任务信息,然后根据任务信息获取流程定义ID,并通过流程定义ID获取相应的 BPMN 模型对象。接着根据任务节点ID获取当前节点信息,并递归遍历当前节点的连线信息,获取所有下一个节点的信息,将其封装成列表并返回给调用方。

前端返回结果:

2.5.3. 完成任务

前端传入参数:

TaskCompleteREQ 编写:

public class TaskCompleteREQ implements Serializable {

    @ApiModelProperty("任务ID")
    private String taskId;

    @ApiModelProperty("审批意见")
    private String message;

    @ApiModelProperty("下一个节点审批,key: 节点id, vallue:审批人集合,多个人使用英文逗号分隔")
    private Map<String, String> assigneeMap;

    public String getMessage() {
        return StringUtils.isEmpty(message) ? "审批通过": message;
    }

    /**
     * 通过节点id获取审批人集合
     * @param key
     * @return
     */
    public String[] getAssignees(String key) {
        if(assigneeMap == null) {
            return null;
        }
        return assigneeMap.get(key).split(",");
    }

}

完成任务代码:

    @PostMapping("/complete")
    public Result completeTask(@RequestBody TaskCompleteREQ req) {
        String taskId = req.getTaskId();
        //1. 查询任务信息
        org.activiti.api.task.model.Task task = taskRuntime.task(taskId);
        if(task == null) {
            return Result.error("任务不存在或不是您办理的任务");
        }
        String procInstId = task.getProcessInstanceId();
        // 2. 指定任务审批意见
        taskService.addComment(taskId, procInstId, req.getMessage());

        // 3. 完成任务
        taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(taskId).build());

        // 4. 查询下一个任务
        List<Task> taskList = taskService.createTaskQuery().processInstanceId(procInstId).list();

        // 5. 指定办理人
        if(CollectionUtils.isEmpty(taskList)) {
            // task.getBusinessKey() m5版本中没有 值
            HistoricProcessInstance hpi = historyService.createHistoricProcessInstanceQuery()
                    .processInstanceId(procInstId).singleResult();
            // 更新业务状态已完成
            return businessStatusService.updateState(hpi.getBusinessKey(), BusinessStatusEnum.FINISH);
        }else {
            Map<String, String> assigneeMap = req.getAssigneeMap();
            if(assigneeMap == null) {
                // 如果没有办理人,直接将流程实例删除(非法操作)
                return deleteProcessInstance(procInstId);
            }
            // 有办理人
            for (Task t: taskList) {
                if(StringUtils.isNotEmpty(t.getAssignee())) {
                    // 如果当前任务有办理人,则直接忽略,不用指定办理人
                    continue;
                }
                // 根据当前任务节点id获取办理人
                String[] assignees = req.getAssignees(t.getTaskDefinitionKey());
                if(ArrayUtils.isEmpty(assignees)) {
                    // 没有办理人
                    return deleteProcessInstance(procInstId);
                }

                if(assignees.length == 1) {
                    taskService.setAssignee(t.getId(), assignees[0]);
                }else {
                    // 多个作为候选人
                    for(String assignee: assignees) {
                        taskService.addCandidateUser(t.getId(), assignee);
                    }
                }
            }
        }

        return Result.ok();
    }

这段代码实现了完成任务的操作,并根据任务完成情况进行下一步的流程处理。它首先根据任务ID查询任务信息,然后添加任务审批意见并完成任务。接着查询流程实例的下一个任务,如果没有下一个任务则更新业务状态为已完成;如果有下一个任务,则根据指定的办理人信息指派任务给相应的用户或候选人。

2.5.4. 获取历史任务节点,用于驳回功能

本工作流框架支持在审批过程中驳回至之前的任意节点,需要完成这个功能首先我们应该获取运行流程中的历史任务节点。

前端界面:

后端代码:

ps:源代码获取历史任务节点代码有bug,这是我修改以后的,源代码我没改(因为我懒 = =)

    public ResponseStructure getBackNodes(String taskId) {
        try {
            Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
            // 2. 从当前任务信息中获取此流程定义id,
            String processDefinitionId = task.getProcessDefinitionId();
            // 3. 拿到流程定义id后可获取此bpmnModel对象
            BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);
            // 4. 通过任务节点id,来获取当前节点信息
            FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey());
            List<Map<String,Object>>parentNodes=new ArrayList<>();
            getParentNodes(flowElement,parentNodes);
            return ResponseStructure.success(parentNodes);
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseStructure.failed("查询驳回节点失败:" + e.getMessage());
        }
    }
    public void getParentNodes(FlowElement flowElement, List<Map<String, Object>> parentNodes) {
        List<SequenceFlow>incommingFlows=((FlowNode)flowElement).getIncomingFlows();
        for (SequenceFlow incommingFlow : incommingFlows) {
            FlowNode source = (FlowNode)incommingFlow.getSourceFlowElement();
            if(source instanceof ParallelGateway||source instanceof ExclusiveGateway){
                getParentNodes(source,parentNodes);
            }else if(source instanceof StartEvent){
                break;
            }else if(source instanceof UserTask){
                Map<String, Object> node = new HashMap<>();
                node.put("activityId", source.getId()); // 节点id
                node.put("activityName", source.getName()); // 节点名称
                parentNodes.add(node);
                getParentNodes(source,parentNodes);
            }
        }
    }

这段代码实现了获取指定任务可驳回的节点信息的功能。它首先根据任务ID查询当前任务信息,然后根据当前任务的流程定义ID获取BpmnModel对象,通过任务节点ID递归查询父节点信息,将可驳回的节点信息封装成列表返回给调用方。

2.5.5. 驳回历史节点
    @PostMapping("/back")
    public Result backProcess(@RequestParam String taskId,
                              @RequestParam String targetActivityId) {
        try {
            // 1. 查询当前任务信息
            Task task = taskService.createTaskQuery()
                    .taskId(taskId)
                    .taskAssignee(UserUtils.getUsername())
                    .singleResult();
            if(task == null) {
                return Result.error("当前任务不存在或你不是任务办理人");
            }

            String procInstId = task.getProcessInstanceId();

            // 2. 获取流程模型实例 BpmnModel
            BpmnModel bpmnModel = repositoryService.getBpmnModel(task.getProcessDefinitionId());
            // 3. 当前节点信息
            FlowNode curFlowNode = (FlowNode)bpmnModel.getMainProcess().getFlowElement(task.getTaskDefinitionKey());
            // 4. 获取当前节点的原出口连线
            List<SequenceFlow> sequenceFlowList = curFlowNode.getOutgoingFlows();
            // 5. 临时存储当前节点的原出口连线
            List<SequenceFlow> oriSequenceFlows = new ArrayList<>();
            oriSequenceFlows.addAll(sequenceFlowList);
            // 6. 将当前节点的原出口清空
            sequenceFlowList.clear();

            // 7. 获取目标节点信息
            FlowNode targetFlowNode = (FlowNode)bpmnModel.getFlowElement(targetActivityId);
            // 8. 获取驳回的新节点
            // 获取目标节点的入口连线
            List<SequenceFlow> incomingFlows = targetFlowNode.getIncomingFlows();
            // 存储所有目标出口
            List<SequenceFlow> allSequenceFlow = new ArrayList<>();
            for (SequenceFlow incomingFlow : incomingFlows) {
                // 找到入口连线的源头(获取目标节点的父节点)
                FlowNode source = (FlowNode)incomingFlow.getSourceFlowElement();
                List<SequenceFlow> sequenceFlows;
                if(source instanceof ParallelGateway) {
                    // 并行网关: 获取目标节点的父节点(并行网关)的所有出口,
                    sequenceFlows = source.getOutgoingFlows();
                } else {
                    // 其他类型父节点, 则获取目标节点的入口连续
                    sequenceFlows = targetFlowNode.getIncomingFlows();
                }
                allSequenceFlow.addAll(sequenceFlows);
            }

            // 9. 将当前节点的出口设置为新节点
            curFlowNode.setOutgoingFlows(allSequenceFlow);

            // 10. 完成当前任务,流程就会流向目标节点创建新目标任务
            //      删除已完成任务,删除已完成并行任务的执行数据 act_ru_execution
            List<Task> list = taskService.createTaskQuery().processInstanceId(procInstId).list();
            for (Task t : list) {
                if(taskId.equals(t.getId())) {
                    // 当前任务,完成当前任务
                    String message = String.format("【%s 驳回任务 %s => %s】",
                            UserUtils.getUsername(), task.getName(), targetFlowNode.getName());
                    taskService.addComment(t.getId(), procInstId, message);
                    // 完成任务,就会进行驳回到目标节点,产生目标节点的任务数据
                    taskService.complete(taskId);
                    // 删除执行表中 is_active_ = 0的执行数据, 使用command自定义模型
                    DelelteExecutionCommand deleteExecutionCMD = new DelelteExecutionCommand(task.getExecutionId());
                    managementService.executeCommand(deleteExecutionCMD);
                }else {
                    // 删除其他未完成的并行任务
                    // taskService.deleteTask(taskId); // 注意这种方式删除不掉,会报错:流程正在运行中无法删除。
                    // 使用command自定义命令模型来删除,直接操作底层的删除表对应的方法,对应的自定义是否删除
                    DeleteTaskCommand deleteTaskCMD = new DeleteTaskCommand(t.getId());
                    managementService.executeCommand(deleteTaskCMD);
                }
            }

            // 13. 完成驳回功能后,将当前节点的原出口方向进行恢复
            curFlowNode.setOutgoingFlows(oriSequenceFlows);


            // 12. 查询目标任务节点历史办理人
            List<Task> newTaskList = taskService.createTaskQuery().processInstanceId(procInstId).list();
            for (Task newTask : newTaskList) {
                // 取之前的历史办理人
                HistoricTaskInstance oldTargerTask = historyService.createHistoricTaskInstanceQuery()
                        .taskDefinitionKey(newTask.getTaskDefinitionKey()) // 节点id
                        .processInstanceId(procInstId)
                        .finished() // 已经完成才是历史
                        .orderByTaskCreateTime().desc() // 最新办理的在最前面
                        .list().get(0);
                taskService.setAssignee(newTask.getId(), oldTargerTask.getAssignee());
            }

            return Result.ok();
        } catch (Exception e) {
            e.printStackTrace();
            return Result.error("驳回失败:"+ e.getMessage());
        }
    }

这段代码实现了流程任务的驳回功能。它首先查询当前任务信息,然后获取流程模型实例,通过修改当前节点的出口连线为目标节点的入口连线,完成当前任务并删除已完成的其他任务(并行网关),恢复当前节点的原出口方向,最后设置目标任务节点的办理人为之前的历史办理人。 

2.6. 请假申请管理

请假申请管理对应前端请假申请页面,包含新增请假申请、条件分页查询请假申请列表数据、查询请假详情信息、更新请假详情信息接口。接口都很简单,我在这里讲一下业务流程和工作流怎么串接起来。

创建BusinessStatus表:

BusinessStatus表为串接业务流程和工作流的中间表,字段如下图,大家看图自行创建就行:

基于status字段,在代码中创建BusinessStatusEnum枚举:

@Getter
@AllArgsConstructor
public enum BusinessStatusEnum {

    CANCEL(0, "已撤回"), WAIT(1, "待提交"), PROCESS(2, "处理中"),
    FINISH(3, "已完成"), INVALID(4, "已作废"), DELETE(5, "已删除");
    private Integer code;
    private String desc;

    public static BusinessStatusEnum getEumByCode(Integer code){
        if(code == null) return null;

        for(BusinessStatusEnum statusEnum: BusinessStatusEnum.values()) {
            if(statusEnum.getCode() == code) {
                return statusEnum;
            }
        }
        return null;
    }

}

 新增申请,流程审批通过,驳回,需要顺带操作BusinessStatus表。

由上图即可看出哪些申请新增了,哪些还没有绑定流程,哪些流程正在运行,哪些流程已经执行完毕。

到此,源码已经讲解完啦,还有一些比较简单的可以异步源码地址去看。

3. 源码地址

关注gzh:后端小肥肠  免费领取源码资源

4. 结语

本文作为《基于Spring Security的Activiti7工作流管理系统简介及实现》的下半部分,以实例代码及代码讲解展示了工作流管理系统的实现,文末还粘贴了源码地址,如本文对你有帮助,请动动发财的小手点点关注哦~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1808781.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【qsort函数】

前言 我们要学习qsort函数并利用冒泡函数仿照qsort函数 首先我们要了解一下qsort&#xff08;快速排序&#xff09; 这是函数的的基本参数 void qsort (void* base, size_t num, size_t size,int (*compar)(const void*,const void*)); 简单解释一下 base&#xff1a;指向…

自动化搭建专属 AI 绘图服务

通义万相AIGC技术已经比较成熟&#xff0c;结合阿里云的计算和存储产品可以方便的搭建自己专属的 AI 绘图服务。例如《创意加速器&#xff1a;AI 绘画创作》这个解决方案&#xff0c;利用阿里自研的通义万相AIGC技术在 Web 服务中实现先进的图像生成。 AI 绘画服务搭建步骤 从…

【文档智能 RAG】RAG增强之路:增强PDF解析并结构化技术路线方案及思路

前言 现阶段&#xff0c;尽管大模型在生成式问答上取得了很大的成功&#xff0c;但由于大部分的数据都是私有数据&#xff0c;大模型的训练及微调成本非常高&#xff0c;RAG的方式逐渐成为落地应用的一种重要的选择方式。然而&#xff0c;如何准确的对文档进行划分chunks&…

Golang的协程调度器GMP

目录 GMP 含义 设计策略 全局队列 P的本地队列 GMP模型以及场景过程 场景一 场景2 场景三 场景四 场景五 场景六 GMP 含义 协程调度器&#xff0c;它包含了运行协程的资源&#xff0c;如果线程想运行协程&#xff0c;必须先获取P&#xff0c;P中还包含了可运行的G…

时序数据库是Niche Market吗?

引言 DB-Engines的流行程度排行从其评估标准[4]可以看出完全不能够做为市场规模的评估标准。甚至于在知道市场规模后可以用这个排行作为一个避雷手册。毕竟现存市场小&#xff0c;可预见增长规模小&#xff0c;竞争大&#xff0c;创新不足&#xff0c;那只能卷价格&#xff0c…

01、Linux网络设置

目录 1.1 查看及测试网络 1.1.1 查看网络配置 1、查看网络接口地址 2、查看主机状态 3、查看路由表条目 4、查看网络连接qing 1.1.2 测试网络连接 1.测试网络连接 2.跟踪数据包的路由路径 3.测试DNS域名解析 1.2 设置网络地址参数 1.2.1 使用网络配置命令 1.修改网卡…

C# MES通信从入门到精通(11)——C#如何使用Json字符串

前言 我们在开发上位机软件的过程中&#xff0c;经常需要和Mes系统进行数据交互&#xff0c;并且最常用的数据格式是Json&#xff0c;本文就是详细介绍Json格式的类型&#xff0c;以及我们在与mes系统进行交互时如何组织Json数据。 1、在C#中如何调用Json 在C#中调用Json相关…

【题解】—— LeetCode一周小结23

&#x1f31f;欢迎来到 我的博客 —— 探索技术的无限可能&#xff01; &#x1f31f;博客的简介&#xff08;文章目录&#xff09; 【题解】—— 每日一道题目栏 上接&#xff1a;【题解】—— LeetCode一周小结22 3.分糖果 II 题目链接&#xff1a;1103. 分糖果 II 排排坐…

【漏洞复现】用友NC pagesServlet SQL注入漏洞(XVE-2024-13067)

0x01 产品简介 用友NC是由用友公司开发的一套面向大型企业和集团型企业的管理软件产品系列。这一系列产品基于全球最新的互联网技术、云计算技术和移动应用技术&#xff0c;旨在帮助企业创新管理模式、引领商业变革。 0x02 漏洞概述 用友NC /portal/pt/servlet/pagesServlet…

Springboot校园美食推荐系统的开发-计算机毕业设计源码44555

摘要 随着人们生活水平的提高&#xff0c;人们对美食的要求也越来越高&#xff0c;对各类美食信息需求越来越大。因此&#xff0c;结合计算机快速发展、普及&#xff0c;在此基础上制作一个页面简单、美观,功能实用的校园美食推荐系统势在必行&#xff0c;满足用户分享美食的需…

Spring-Security(二)OAuth2认证详解(持续更新)

Spring Security & Oauth2系列&#xff1a; Spring Security&#xff08;一&#xff09; 源码分析及认证流程 Spring Security&#xff08;二&#xff09;OAuth2认证详解及自定义异常处理 文章目录 1、OAuth2.0 简介1.1 OAuth2.0 相关名词解释1.2 四种授权模式 1.3 、OAu…

QT 信号和槽 信号关联到信号示例 信号除了可以绑定槽以外,信号还可以绑定信号

信号除了可以关联到槽函数&#xff0c;还可以关联到类型匹配的信号&#xff0c;实现信号的接力触发。上个示例中因为 clicked 信号没有参数&#xff0c;而 SendMsg 信号有参数&#xff0c;所以不方便直接关联。本小节示范一个信号到信号的关联&#xff0c;将按钮的 clicked 信号…

Python 深度探讨 *args

点击下方卡片&#xff0c;关注“小白玩转Python”公众号 作为Python中最独特的语法之一&#xff0c;*args 在编程过程中给我们带来了很多灵活性和便利性。我认为它们反映了“Pythonic”和“Python之禅”。然而&#xff0c;我发现它们对于学习者&#xff08;尤其是初学者&#x…

DeepSpeed Learning Rate Scheduler

Learning Rate Range Test (LRRT) 训练试跑&#xff0c;该lr scheduler从小到大增长lr&#xff0c;同时记录下validatin loss&#xff1b;人来观察在训练多少step之后&#xff0c;loss崩掉&#xff08;diverge)了&#xff0c;进而为真正跑训练&#xff0c;挑选合适的lr区间&…

一、Electron 环境初步搭建

新建一个文件夹&#xff0c;然后进行 npm init -y 进行初始化&#xff0c;然后我们在进行 npm i electron --save-dev , 此时我们按照官网的教程进行一个初步的搭建&#xff0c; 1.在 package.json 文件进行修改 {"name": "electron-ui","version…

嵌入式应用之FIFO模块原理与实现

FIFO介绍与原理 FIFO是First-In First-Out的缩写&#xff0c;它是一个具有先入先出特点的缓冲区。FIFO在嵌入式应用的非常广泛&#xff0c;可以说有数据收发的地方&#xff0c;基本就有FIFO的存在。或者为了降低CPU负担&#xff0c;提高数据处理效率&#xff0c;可以在积累到一…

使用 Scapy 库编写 TCP FIN 洪水攻击脚本

一、介绍 TCP FIN洪水攻击是一种分布式拒绝服务攻击&#xff08;DDoS&#xff09;&#xff0c;攻击者通过向目标服务器发送大量伪造的TCP FIN&#xff08;终止&#xff09;数据包&#xff0c;使目标服务器不堪重负&#xff0c;无法正常处理合法请求。FIN包通常用于关闭一个TCP…

电路笔记 : 嘉立创EDA 导入、查找、设计管理器(快速寻找网络标签)功能+DRC错误检查和处理

导入功能 查找功能 可查找多种类型&#xff0c;如原件名称、网络标签等 设计管理器 图层查看 DRC错误 规则设置 线距问题 大多数PCB制造商能够可靠地生产5 mil间距的走线和间隙。这是一个常见的标准&#xff0c;适合大多数消费级和工业级电子产品。在5 mil以上的间距&#xff…

操作系统复习-存储管理之虚拟内存

虚拟内存概述 有些进程实际需要的内存很大&#xff0c;超过物理内存的容量。多道程序设计&#xff0c;使得每个进程可用物理内存更加稀缺。不可能无限增加物理内存&#xff0c;物理内存总有不够的时候。虚拟内存是操作系统内存管理的关键技术。使得多道程序运行和大程序运行称…

Collections工具类及其案例

package exercise;public class Demo1 {public static void main(String[] args) {//可变参数//方法形参的个数是可以发生变化的//格式&#xff1a;属性类型...名字//int...argsint sum getSum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);System.out.println(sum);}//底层&#xff1a;可…