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.
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=10000List 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-00d861e5b732View the updated diagram again using the same image endpoint.
Below are screenshots of the generated diagrams at each stage.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
