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
<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&userId=10000List tasks for the user to obtain the instance ID:
/holidays/tasks?userId=10000View the current process diagram:
/view/image?instanceId=341ee217-5df2-11eb-afe9-00d861e5b732Advance 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.
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.