Spring Boot integrated process engine Flowable (with source code address)

1. Import dependencies

flowable depends on:

<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>6.7.2</version>
</dependency>

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>flowable_study</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.6</version>
    </parent>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.8</version>
        </dependency>

        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-spring-boot-starter</artifactId>
            <version>6.7.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.5</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>

    </dependencies>

</project>

Note: The above is the pom.xml file of the entire project, which can be modified according to your actual situation. If you only want to add Flowable to the existing environment, you can directly import the top Flowable dependency

2. Database configuration

Flowable needs to connect to the database and will create a new table when the project starts. So we configure in application.yml:

spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    username: root
    password: 199692
    url: jdbc:mysql://127.0.0.1:3306/flowable?serverTimezone=Asia/Shanghai &useSSL=false

After the configuration is successful, when we start the project, it will automatically create a table for us, as shown in the figure:

3. Download the plug-in Flowable BPMN visualizer

If the IDE chooses IDEA, you can download the plug-in Flowable BPMN visualizer.
Shortcut key Ctrl + Alt + S Open the settings, find Plugins, select Marketplace, enter Flowable BPMN visualizer, and finally Click Install.

Click Apply when the installation is complete, and finally click OK to close the window.

4. Drawing

We create a new file in the resources directory of the project, and we can see that there is an additional New Flowable BPMN 2.0 file option. We select it and create a new file for the leave process. The file name is: ask_for_leave, which can be based on Name your own business.

After the creation, we right-click the file and select View BPMN (Flowable) Diagram :

Open the visual interface and start drawing the flow chart.

Right-click the blank space to create various events and processes.

The figure shows a simple schematic diagram of the leave process:

When drawing a picture, we need to change some of its properties below, such as: ID, Name, etc., so that we can see the name and meaning of the code later.
After drawing, we can see that the content of the file will be automatically generated for us:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http ://www.w3.org/2001/XMLSchema" xmlns:flowable="http://flowable.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI " xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage=" http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.flowable.org/processdef">
  <process id="ask_for_leave" name="ask_for_leave" isExecutable="true">
    <startEvent id="start_leave" name="start leave"/>
    <userTask id="employee" name="employee" flowable:assignee="#{employee}"/>
    <sequenceFlow id="flow_start" sourceRef="start_leave" targetRef="employee"/>
    <userTask id="leader" name="Leader" flowable:assignee="#{leader}"/>
    <sequenceFlow id="leave" sourceRef="employee" targetRef="leader" name="leave"/>
    <exclusiveGateway id="leader_judge"/>
    <sequenceFlow id="leader_audit" sourceRef="leader" targetRef="leader_judge" name="leader audit"/>
    <serviceTask id="send_message" flowable:exclusive="true" name="Send Leave Failed Message" flowable:class="src.com.dxc.service.AskForLeaveFail"/>
    <sequenceFlow id="leader_reject" sourceRef="leader_judge" targetRef="send_message" name="reject">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='rejected'}]]></conditionExpression>
    </sequenceFlow>
    <userTask id="manager" name="General Manager" flowable:assignee="#{manager}"/>
    <sequenceFlow id="leader_agree" sourceRef="leader_judge" targetRef="manager" name="agree">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='agree'}]]></conditionExpression>
    </sequenceFlow>
    <endEvent id="leave fail" name="leave_fail"/>
    <sequenceFlow id="flow_fail_end" sourceRef="send_message" targetRef="Leave Failed"/>
    <exclusiveGateway id="manager_judge"/>
    <sequenceFlow id="manager_audit" sourceRef="manager" targetRef="manager_judge" name="manager_judge"/>
    <sequenceFlow id="manager_rejet" sourceRef="manager_judge" targetRef="send_message" name="reject">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='rejected'}]]></conditionExpression>
    </sequenceFlow>
    <endEvent id="leave success" name="leave_success"/>
    <sequenceFlow id="manager_agree" sourceRef="manager_judge" targetRef="leave success" name="agree">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='agree'}]]></conditionExpression>
    </sequenceFlow>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_ask_for_leave">
    <bpmndi:BPMNPlane bpmnElement="ask_for_leave" id="BPMNPlane_ask_for_leave">
      <bpmndi:BPMNShape id="shape-ad5ec363-de3b-4ede-883e-629cbe497515" bpmnElement="start_leave">
        <omgdc:Bounds x="-555.0" y="-580.0" width="30.0" height="30.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="shape-0d5a8676-83b3-46ea-a710-b73fb38a6ded" bpmnElement="employee">
        <omgdc:Bounds x="-452.44943" y="-605.0" width="100.0" height="80.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="edge-bf2b83dc-ad4f-4ffa-ba9d-82d8128c2dde" bpmnElement="flow_start">
        <omgdi:waypoint x="-525.0" y="-565.0"/>
        <omgdi:waypoint x="-485.0497" y="-565.0"/>
        <omgdi:waypoint x="-452.44943" y="-565.0"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="shape-d00296f1-1a06-4052-bd3c-d373c1f9ad74" bpmnElement="leader">
        <omgdc:Bounds x="-260.0" y="-605.00006" width="100.0" height="80.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="edge-6a821f76-9583-438b-810e-5a8b48466ebb" bpmnElement="leave">
        <omgdi:waypoint x="-352.44943" y="-565.0"/>
        <omgdi:waypoint x="-260.0" y="-565.00006"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="shape-57050807-ffe2-4a4d-a60b-eb224575ef4b" bpmnElement="leader_judge">
        <omgdc:Bounds x="-75.0" y="-585.0001" width="40.0" height="40.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="edge-795a0427-2f83-4607-a40d-4b359790275e" bpmnElement="leader_audit">
        <omgdi:waypoint x="-160.0" y="-565.00006"/>
        <omgdi:waypoint x="-75.0" y="-565.0001"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="shape-35bcb335-0516-4c89-9148-ead6c956472d" bpmnElement="send_message">
        <omgdc:Bounds x="-105.0" y="-465.0" width="100.0" height="80.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="edge-c6b5f535-d7f2-4816-8d3a-e8499ad0c923" bpmnElement="leader_reject">
        <omgdi:waypoint x="-55.0" y="-545.0001"/>
        <omgdi:waypoint x="-55.0" y="-465.0"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="shape-79a775c0-5c7a-4adf-b544-9954eedb08f6" bpmnElement="manager">
        <omgdc:Bounds x="50.0" y="-605.0001" width="100.0" height="80.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="edge-57cd436f-f129-4044-b65c-637cf63465a6" bpmnElement="leader_agree">
        <omgdi:waypoint x="-35.0" y="-565.0001"/>
        <omgdi:waypoint x="50.0" y="-565.0001"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="shape-37759eb0-7cf1-4da0-8aba-39c8443675ac" bpmnElement="Leave Failed">
        <omgdc:Bounds x="-240.0" y="-440.0" width="30.0" height="30.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="edge-3acbccbb-9a56-4ff0-86b1-f2f6641f125c" bpmnElement="flow_fail_end">
        <omgdi:waypoint x="-105.0" y="-425.0"/>
        <omgdi:waypoint x="-210.0" y="-425.0"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="shape-b3da428e-37fc-410e-a90f-40eb0230e892" bpmnElement="manager_judge">
        <omgdc:Bounds x="250.0" y="-585.0001" width="40.0" height="40.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="edge-e56b296d-0690-44e3-bdaf-6e3864a826ca" bpmnElement="manager_audit">
        <omgdi:waypoint x="150.0" y="-565.0001"/>
        <omgdi:waypoint x="250.0" y="-565.0001"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="edge-a806347f-3f50-49da-ab26-d571a6104c0c" bpmnElement="manager_rejet">
        <omgdi:waypoint x="270.0" y="-545.0001"/>
        <omgdi:waypoint x="269.99997" y="-424.99997"/>
        <omgdi:waypoint x="-5.0" y="-425.0"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="shape-416e3b9e-2e1a-489a-808e-fa5fd3f8c691" bpmnElement="leave successfully">
        <omgdc:Bounds x="395.0" y="-580.00006" width="30.0" height="30.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="edge-33f48358-b60a-4ea8-bd80-9729cfdf6a11" bpmnElement="manager_agree">
        <omgdi:waypoint x="290.0" y="-565.0001"/>
        <omgdi:waypoint x="395.0" y="-565.00006"/>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

Friends who don’t want to draw can directly copy my content.
In this file, we only need to pay attention to the content wrapped by tags, which is a complete process definition.
In fact, the content of XML is very easy to understand. According to the label and the flow chart, you can know the specific meaning:

  • : Indicates a complete workflow.
  • : The starting position in the workflow, which is the green button in the figure.
  • : The end position in the workflow, which is the red button in the figure.
  • : Represents a task review node (team leader, manager, etc.), There is a flowable:assignee attribute on this node, which indicates who should handle this node , when calling in the Java code in the future, we need to specify the ID of the corresponding handler or other unique tags.
  • : This is a service task. In a specific implementation, this task can do anything. Here we use the flowable:class attribute to configure a class, which means that if you go When reaching this node, the method in this class will be executed automatically.
  • : Logical judgment node, which is equivalent to the diamond box in the flowchart.
  • : A line linking each node, the sourceRef attribute indicates the starting node of the line, and the targetRef attribute indicates the node the line points to , the lines in our figure belong to this.

According to the above explanation of each tag, the generated XML file can be easily understood.

5. Code testing

Gitee code address: https://gitee.com/du_xin_cheng/flowable_study

Startup class:

package src.com.dxc;

import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.DeploymentBuilder;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import javax.annotation.Resource;
import java.io.InputStream;

/**
 * Startup class
 *
 * @Author xincheng.du
 * @Date 2023/5/22 16:50
 */
@SpringBootApplication
public class Application implements CommandLineRunner {<!-- -->

    public static void main(String[] args) {<!-- -->
        SpringApplication.run(Application.class, args);
    }

    @Resource
    private RepositoryService repositoryService;

    /**
     * Project startup loading process file
     *
     * @param args args
     */
    @Override
    public void run(String... args) {<!-- -->
        InputStream inputStream = this.getClass().getResourceAsStream("/ask_for_leave.bpmn20.xml");
        DeploymentBuilder deploymentBuilder = repositoryService. createDeployment();
        deploymentBuilder.addInputStream("ask_for_leave.bpmn20.xml", inputStream);
        deploymentBuilder.deploy();
    }
}

Initiate leave process parameter class AskForLeaveStartDTO:

package src.com.dxc.model;

import lombok. AllArgsConstructor;
import lombok.Data;
import lombok. NoArgsConstructor;

/**
 * Initiate leave process parameters
 *
 * @Author xincheng.du
 * @Date 2023/5/23 15:00
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AskForLeaveStartDTO {<!-- -->

    /**
     * employee id
     */
    private String employeeId;

    /**
     * employee's name
     */
    private String name;

    /**
     * Reason for leave
     */
    private String reason;

    /**
     * Number of days off
     */
    private Integer days;

}

Audit parameter class AuditDTO:

package src.com.dxc.model;

import lombok. AllArgsConstructor;
import lombok.Data;
import lombok. NoArgsConstructor;

/**
 * Review parameters
 *
 * @Author xincheng.du
 * @Date 2023/5/23 15:12
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuditDTO {<!-- -->

    /**
     * Subordinate id {employee id || leader id}
     */
    private String lowerId;

    /**
     * subordinate name {employee || leader}
     * The name here corresponds to the flowable:assignee attribute under the <userTask> tag in the ask_for_leave.bpmn20.xml file
     */
    private String lowerName;

    /**
     * superior id {team leader id || manager id}
     */
    private String superiorId;

    /**
     * Superior name {leader || manager}
     * The name here corresponds to the flowable:assignee attribute under the <userTask> tag in the ask_for_leave.bpmn20.xml file
     */
    private String superiorName;

    /**
     * Review Result {Agree || Reject}
     */
    private String checkResult;

    /**
     * Whether it is the highest level
     * true->leaderId is the highest level, managerId is empty
     * false->leaderId is not the highest level, managerId is the superior of leaderId
     */
    private Boolean isTop;

}

Leave process interface AskForLeaveService:

package src.com.dxc.service.impl;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.springframework.stereotype.Service;
import src.com.dxc.model.AskForLeaveStartDTO;
import src.com.dxc.model.AuditDTO;
import src.com.dxc.service.AskForLeaveService;

import java.util.*;

/**
 * Leave process
 *
 * @Author xincheng.du
 * @Date 2023/5/23 14:51
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class AskForLeaveServiceImpl implements AskForLeaveService {<!-- -->

    private final RuntimeService runtimeService;

    private final TaskService taskService;

    public static final String EMPLOYEE = "employee";

    public static final String FLOW_KEY = "ask_for_leave";

    public static final String NAME = "name";

    public static final String REASON = "reason";

    public static final String DAYS = "days";

    public static final String CHECK_RESULT = "checkResult";

    @Override
    public void askForLeave(AskForLeaveStartDTO dto) {<!-- -->
        HashMap<String, Object> map = new HashMap<>();
        map.put(EMPLOYEE, dto.getEmployeeId());
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(FLOW_KEY, map);
        runtimeService.setVariable(processInstance.getId(), NAME, dto.getName());
        runtimeService.setVariable(processInstance.getId(), REASON, dto.getReason());
        runtimeService.setVariable(processInstance.getId(), DAYS, dto.getDays());
        log.info("Create leave process processId: {}", processInstance.getId());
    }

    @Override
    public void executeProcess(AuditDTO dto) {<!-- -->
        // Find the task of the subordinate, and then submit it to the superior for approval
        List<Task> list = taskService.createTaskQuery().taskAssignee(dto.getLowerId()).orderByTaskId().desc().list();
        for (Task task : list) {<!-- -->
            log.info("Task ID: {}; Task handler: {}; Whether the task is suspended: {}", task.getId(), task.getAssignee(), task.isSuspended());
            Map<String, Object> map = new HashMap<>();
            // If the current handler is not the top handler, then the superior id of the current handler needs to be specified
            Optional.of(dto.getIsTop()).ifPresent(isTop -> {<!-- -->
                if (Objects. equals(Boolean. FALSE, isTop)) {<!-- -->
                    map.put(dto.getSuperiorName(), dto.getSuperiorId());
                }
            });
            Optional.ofNullable(dto.getCheckResult()).ifPresent(result -> map.put(CHECK_RESULT, result));
            taskService.complete(task.getId(), map);
        }
    }
}

Leave process implementation class AskForLeaveServiceImpl:

package src.com.dxc.service.impl;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.springframework.stereotype.Service;
import src.com.dxc.model.AskForLeaveStartDTO;
import src.com.dxc.model.AuditDTO;
import src.com.dxc.service.AskForLeaveService;

import java.util.*;

/**
 * Leave process
 *
 * @Author xincheng.du
 * @Date 2023/5/23 14:51
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class AskForLeaveServiceImpl implements AskForLeaveService {<!-- -->

    private final RuntimeService runtimeService;

    private final TaskService taskService;

    public static final String EMPLOYEE = "employee";

    public static final String FLOW_KEY = "ask_for_leave";

    public static final String NAME = "name";

    public static final String REASON = "reason";

    public static final String DAYS = "days";

    public static final String CHECK_RESULT = "checkResult";

    @Override
    public void askForLeave(AskForLeaveStartDTO dto) {<!-- -->
        HashMap<String, Object> map = new HashMap<>();
        map.put(EMPLOYEE, dto.getEmployeeId());
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(FLOW_KEY, map);
        runtimeService.setVariable(processInstance.getId(), NAME, dto.getName());
        runtimeService.setVariable(processInstance.getId(), REASON, dto.getReason());
        runtimeService.setVariable(processInstance.getId(), DAYS, dto.getDays());
        log.info("Create leave process processId: {}", processInstance.getId());
    }

    @Override
    public void executeProcess(AuditDTO dto) {<!-- -->
        // Find the task of the subordinate, and then submit it to the superior for approval
        List<Task> list = taskService.createTaskQuery().taskAssignee(dto.getLowerId()).orderByTaskId().desc().list();
        for (Task task : list) {<!-- -->
            log.info("Task ID: {}; Task handler: {}; Whether the task is suspended: {}", task.getId(), task.getAssignee(), task.isSuspended());
            Map<String, Object> map = new HashMap<>();
            // If the current handler is not the top handler, then the superior id of the current handler needs to be specified
            Optional.of(dto.getIsTop()).ifPresent(isTop -> {<!-- -->
                if (Objects. equals(Boolean. FALSE, isTop)) {<!-- -->
                    map.put(dto.getSuperiorName(), dto.getSuperiorId());
                }
            });
            Optional.ofNullable(dto.getCheckResult()).ifPresent(result -> map.put(CHECK_RESULT, result));
            taskService.complete(task.getId(), map);
        }
    }
}

Execution class AskForLeaveFail for leave failure:

package src.com.dxc.service;

import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;

@Slf4j
public class AskForLeaveFail implements JavaDelegate {<!-- -->

    @Override
    public void execute(DelegateExecution execution) {<!-- -->
        log.info("Leave request failed...");
    }

}

To view the process progress Controller, view it by starting the project call interface:

package src.com.dxc.controller;

import lombok.RequiredArgsConstructor;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.ProcessEngine;
import org.flowable.engine.ProcessEngineConfiguration;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.image.ProcessDiagramGenerator;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * View process progress
 *
 * @Author xincheng.du
 * @Date 2023/5/23 15:25
 */
@RestController
@RequiredArgsConstructor
public class ShowController {<!-- -->

    private final RuntimeService runtimeService;

    private final RepositoryService repositoryService;

    private final ProcessEngine processEngine;

    @GetMapping("/showFlowPic")
    public void showPic(HttpServletResponse resp, String processId) throws Exception {<!-- -->
        ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processId).singleResult();
        if (pi == null) {<!-- -->
            return;
        }
        List<Execution> executions = runtimeService
                .createExecutionQuery()
                .processInstanceId(processId)
                .list();

        List<String> activityIds = new ArrayList<>();
        List<String> flows = new ArrayList<>();
        for (Execution exe : executions) {<!-- -->
            List<String> ids = runtimeService.getActiveActivityIds(exe.getId());
            activityIds. addAll(ids);
        }

        BpmnModel bpmnModel = repositoryService.getBpmnModel(pi.getProcessDefinitionId());
        ProcessEngineConfiguration engconf = processEngine.getProcessEngineConfiguration();
        ProcessDiagramGenerator diagramGenerator = engconf.getProcessDiagramGenerator();
        InputStream in = diagramGenerator.generateDiagram(bpmnModel, "png", activityIds, flows, engconf.getActivityFontName(), engconf.getLabelFontName(), engconf.getAnnotationFontName(), engconf.getClassLoader(), 1.0, false);
        OutputStream out = null;
        byte[] buf = new byte[1024];
        int length = 0;
        try {<!-- -->
            out = resp. getOutputStream();
            while ((legth = in.read(buf)) != -1) {<!-- -->
                out.write(buf, 0, legth);
            }
        } finally {<!-- -->
            if (in != null) {<!-- -->
                in. close();
            }
            if (out != null) {<!-- -->
                out. close();
            }
        }
    }
}

Process test class AskForLeaveServiceImplTest :

package src.com.dxc.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import src.com.dxc.Application;
import src.com.dxc.model.AskForLeaveStartDTO;
import src.com.dxc.model.AuditDTO;
import src.com.dxc.service.AskForLeaveService;

@Slf4j
@SpringBootTest(classes = Application. class)
class AskForLeaveServiceImplTest {<!-- -->

    @Autowired
    public AskForLeaveService askForLeaveService;

    /**
     * Initiate leave process
     */
    @Test
    void askForLeave() {<!-- -->
        AskForLeaveStartDTO dto = new AskForLeaveStartDTO();
        dto.setEmployeeId("1");
        dto.setName("xiaoMing");
        dto.setReason("Travel");
        dto.setDays(10);
        askForLeaveService. askForLeave(dto);
    }

    /**
     * Employees submit the process to the team leader
     */
    @Test
    void submit() {<!-- -->
        AuditDTO dto = new AuditDTO();
        dto.setLowerId("1");
        dto.setLowerName("employee");
        dto.setSuperiorId("2");
        dto.setSuperiorName("leader");
        dto.setIsTop(false);
        askForLeaveService. executeProcess(dto);
    }

    /**
     * Team Leader Approval - Agree
     */
    @Test
    void leaderAgree() {<!-- -->
        AuditDTO dto = new AuditDTO();
        dto.setLowerId("2");
        dto.setLowerName("leader");
        dto.setSuperiorId("3");
        dto.setSuperiorName("manager");
        dto.setCheckResult("agree");
        dto.setIsTop(false);
        askForLeaveService. executeProcess(dto);
    }

    /**
     * Team Leader Approval - Rejected
     */
    @Test
    void leaderReject() {<!-- -->
        AuditDTO dto = new AuditDTO();
        dto.setLowerId("2");
        dto.setLowerName("leader");
        dto.setSuperiorId("3");
        dto.setSuperiorName("manager");
        dto.setCheckResult("rejected");
        dto.setIsTop(false);
        askForLeaveService. executeProcess(dto);
    }

    /**
     * Manager Approval - Agree
     */
    @Test
    void managerAgree() {<!-- -->
        AuditDTO dto = new AuditDTO();
        dto.setLowerId("3");
        dto.setLowerName("manager");
        dto.setCheckResult("agree");
        dto.setIsTop(true);
        askForLeaveService. executeProcess(dto);
    }

    /**
     * Manager Approval - Reject
     */
    @Test
    void managerReject() {<!-- -->
        AuditDTO dto = new AuditDTO();
        dto.setLowerId("3");
        dto.setLowerName("manager");
        dto.setCheckResult("rejected");
        dto.setIsTop(true);
        askForLeaveService. executeProcess(dto);
    }
}

You can check whether the current progress is correct by accessing the interface /showFlowPic at each step, and the processId required in the interface is printed by the askForLeave() method processId from the output.