How to Visualize Real-Time Workflow Progress with Spring Boot and Activiti

This tutorial demonstrates how to extend a Spring Boot application with Activiti to query historic process data, configure a diagram generator bean, compute highlighted flows, expose a REST endpoint for real-time process diagram rendering, and verify the workflow through a series of API calls and screenshots.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Visualize Real-Time Workflow Progress with Spring Boot and Activiti

Real-time view of the current workflow progress using Spring Boot and Activiti.

Adding methods to HolidayService

public HistoricProcessInstance queryHistory(String instanceId) {
    return historyService.createHistoricProcessInstanceQuery()
        .processInstanceId(instanceId).singleResult();
}

/**
 * Query historic activity instances by process instance ID
 */
public HistoricActivityInstanceQuery getHistoryActivity(String instanceId) {
    return historyService.createHistoricActivityInstanceQuery()
        .processInstanceId(instanceId);
}

Configure ProcessDiagramGenerator bean

@Configuration
public class ActivitiConfig {

    @Bean
    @ConditionalOnMissingBean
    public ProcessDiagramGenerator processDiagramGenerator() {
        return new DefaultProcessDiagramGenerator();
    }
}

Utility to obtain highlighted flows

public class ActivitiUtils {
    /**
     * Get the list of flow IDs that should be highlighted in the diagram.
     */
    public static List<String> getHighLightedFlows(BpmnModel bpmnModel,
            ProcessDefinitionEntity processDefinitionEntity,
            List<HistoricActivityInstance> historicActivityInstances) {
        List<String> highFlows = new ArrayList<>();
        if (historicActivityInstances == null || historicActivityInstances.size() == 0) {
            return highFlows;
        }
        for (int i = 0; i < historicActivityInstances.size() - 1; i++) {
            HistoricActivityInstance activityImpl_ = historicActivityInstances.get(i);
            List<FlowNode> sameStartTimeNodes = new ArrayList<>();
            FlowNode sameActivityImpl = getNextFlowNode(bpmnModel, historicActivityInstances, i, activityImpl_);
            if (sameActivityImpl != null) sameStartTimeNodes.add(sameActivityImpl);
            for (int j = i + 1; j < historicActivityInstances.size() - 1; j++) {
                HistoricActivityInstance activityImpl1 = historicActivityInstances.get(j);
                HistoricActivityInstance activityImpl2 = historicActivityInstances.get(j + 1);
                if (activityImpl1.getStartTime().getTime() != activityImpl2.getStartTime().getTime()) {
                    break;
                }
                FlowNode sameActivityImpl2 = (FlowNode) bpmnModel.getMainProcess()
                    .getFlowElement(activityImpl2.getActivityId());
                sameStartTimeNodes.add(sameActivityImpl2);
            }
            FlowNode activityImpl = (FlowNode) bpmnModel.getMainProcess()
                .getFlowElement(historicActivityInstances.get(i).getActivityId());
            List<SequenceFlow> pvmTransitions = activityImpl.getOutgoingFlows();
            for (SequenceFlow pvmTransition : pvmTransitions) {
                FlowNode pvmActivityImpl = (FlowNode) bpmnModel.getMainProcess()
                    .getFlowElement(pvmTransition.getTargetRef());
                if (!sameStartTimeNodes.contains(pvmActivityImpl)) continue;
                highFlows.add(pvmTransition.getId());
            }
        }
        return highFlows;
    }

    /**
     * Get the next flow node after the current historic activity.
     */
    private static FlowNode getNextFlowNode(BpmnModel bpmnModel,
            List<HistoricActivityInstance> historicActivityInstances,
            int i, HistoricActivityInstance activityImpl_) {
        FlowNode sameActivityImpl = null;
        if (!"userTask".equals(activityImpl_.getActivityType())) {
            if (i == historicActivityInstances.size()) return sameActivityImpl;
            sameActivityImpl = (FlowNode) bpmnModel.getMainProcess()
                .getFlowElement(historicActivityInstances.get(i + 1).getActivityId());
            return sameActivityImpl;
        }
        for (int k = i + 1; k <= historicActivityInstances.size() - 1; k++) {
            HistoricActivityInstance activityImp2_ = historicActivityInstances.get(k);
            if ("userTask".equals(activityImp2_.getActivityType()) &&
                activityImpl_.getStartTime().getTime() == activityImp2_.getStartTime().getTime()) {
                continue;
            }
            sameActivityImpl = (FlowNode) bpmnModel.getMainProcess()
                .getFlowElement(historicActivityInstances.get(k).getActivityId());
            break;
        }
        return sameActivityImpl;
    }
}

Controller for displaying the process diagram

@RestController
@RequestMapping("/view")
public class ProcessViewController {

    private static Logger logger = LoggerFactory.getLogger(ProcessViewController.class);

    @Resource
    private HolidayService holidayService;
    @Resource
    private RepositoryService repositoryService;
    @Resource
    private ProcessDiagramGenerator processDiagramGenerator;

    @ResponseBody
    @GetMapping("/image")
    public void showImg(String instanceId, HttpServletResponse response) throws Exception {
        response.setContentType("text/html;charset=utf-8");
        if (StringUtils.isEmpty(instanceId)) {
            PrintWriter out = response.getWriter();
            out.write("error");
            out.close();
            return;
        }
        HistoricProcessInstance processInstance = holidayService.queryHistory(instanceId);
        if (processInstance == null) {
            logger.error("流程实例ID:{}没查询到流程实例!", instanceId);
            PrintWriter out = response.getWriter();
            out.write("error instance not exists");
            out.close();
            return;
        }
        BpmnModel bpmnModel = repositoryService.getBpmnModel(processInstance.getProcessDefinitionId());
        HistoricActivityInstanceQuery historyInstanceQuery = holidayService.getHistoryActivity(instanceId);
        List<HistoricActivityInstance> historicActivityInstanceList = historyInstanceQuery
                .orderByHistoricActivityInstanceStartTime().asc().list();
        if (historicActivityInstanceList == null || historicActivityInstanceList.isEmpty()) {
            logger.error("流程实例ID: {}, 没有历史节点信息!", instanceId);
            outputImg(response, bpmnModel, null, null);
            return;
        }
        List<String> executedActivityIdList = historicActivityInstanceList.stream()
                .map(item -> item.getActivityId()).collect(Collectors.toList());
        ProcessDefinitionEntity processDefinition = (ProcessDefinitionEntity) ((RepositoryServiceImpl) repositoryService)
                .getDeployedProcessDefinition(processInstance.getProcessDefinitionId());
        List<String> flowIds = ActivitiUtils.getHighLightedFlows(bpmnModel, processDefinition, historicActivityInstanceList);
        outputImg(response, bpmnModel, flowIds, executedActivityIdList);
    }

    private void outputImg(HttpServletResponse response, BpmnModel bpmnModel,
            List<String> flowIds, List<String> executedActivityIdList) {
        InputStream imageStream = null;
        try {
            imageStream = processDiagramGenerator.generateDiagram(bpmnModel, executedActivityIdList, flowIds,
                    "宋体", "微软雅黑", "黑体", true, "png");
            byte[] buffer = new byte[10 * 1024];
            int len;
            while ((len = imageStream.read(buffer)) != -1) {
                response.getOutputStream().write(buffer, 0, len);
            }
            response.getOutputStream().flush();
        } catch (Exception e) {
            logger.error("流程图输出异常!", e);
        } finally {
            if (imageStream != null) {
                try { imageStream.close(); } catch (IOException e) { e.printStackTrace(); }
            }
        }
    }
}

Testing steps:

Start a new holiday process:

/holidays/start?processDefinitionId=holiday:1:0a012ce6-5df2-11eb-afe9-00d861e5b732&userId=10000

List tasks for the user to obtain the instance ID: /holidays/tasks?userId=10000 View the current process diagram: /view/image?instanceId=341ee217-5df2-11eb-afe9-00d861e5b732 Advance the workflow:

/holidays/apply?days=3&mgr=10001&explain=生病&instanceId=341ee217-5df2-11eb-afe9-00d861e5b732

View the updated diagram again using the same image endpoint.

Below are screenshots of the generated diagrams at each stage.

Diagram step 1
Diagram step 1
Diagram after first view
Diagram after first view
Diagram after advancing
Diagram after advancing
Final diagram
Final diagram
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

workflowSpring BootActivitiprocess diagram
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.