Backend Development 10 min read

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

<code>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);
}
</code>

Configure ProcessDiagramGenerator bean

<code>@Configuration
public class ActivitiConfig {

    @Bean
    @ConditionalOnMissingBean
    public ProcessDiagramGenerator processDiagramGenerator() {
        return new DefaultProcessDiagramGenerator();
    }
}
</code>

Utility to obtain highlighted flows

<code>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;
    }
}
</code>

Controller for displaying the process diagram

<code>@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(); }
            }
        }
    }
}
</code>

Testing steps:

Start a new holiday process:

/holidays/start?processDefinitionId=holiday:1:0a012ce6-5df2-11eb-afe9-00d861e5b732&amp;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&amp;mgr=10001&amp;explain=生病&amp;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
JavaworkflowSpring 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

login 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.