FlowableワークフローエンジンとSpring Bootの連携実装
動作環境
- JDK 11
- Spring Boot 2.7.6
- Flowable 6.8.0
- MySQL 8.0.27
BPMNプロセスの作成
1. オンラインフロー図作成ツールの利用
簡単な休假申請プロセスをツールで描画します。ツール:http://www.bpmnmodeler.com/
2. 完成したプロセス定義XML
<?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: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"
xmlns:flowable="http://flowable.org/bpmn"
targetNamespace="http://bpmn.io/schema/bpmn">
<process id="VacationRequest" name="VacationProcess" isExecutable="true">
<userTask id="ApplicantTask" name="申請" flowable:assignee="${applicant}">
<incoming>RejectFlow</incoming>
</userTask>
<userTask id="ApproverTask" name="承認" flowable:assignee="${approver}" />
<exclusiveGateway id="ApprovalGateway" name="承認判断" />
<endEvent id="ProcessEnd" name="終了">
<incoming>EndFlow</incoming>
</endEvent>
<startEvent id="ProcessStart" name="開始" />
<sequenceFlow id="Flow1" sourceRef="ApplicantTask" targetRef="ApproverTask" />
<sequenceFlow id="Flow2" sourceRef="ProcessStart" targetRef="ApplicantTask" />
<sequenceFlow id="Flow3" sourceRef="ApproverTask" targetRef="ApprovalGateway" />
<sequenceFlow id="ApprovalFlow" name="承認" sourceRef="ApprovalGateway" targetRef="NotificationTask">
<conditionExpression xsi:type="tFormalExpression">${approvalStatus=='承認'}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="RejectFlow" name="却下" sourceRef="ApprovalGateway" targetRef="ApplicantTask">
<conditionExpression xsi:type="tFormalExpression">${approvalStatus=='却下'}</conditionExpression>
</sequenceFlow>
<serviceTask id="NotificationTask" name="通知" flowable:class="com.example.workflow.service.SendApprovalNotification">
<incoming>ApprovalFlow</incoming>
<outgoing>EndFlow</outgoing>
</serviceTask>
<sequenceFlow id="EndFlow" sourceRef="NotificationTask" targetRef="ProcessEnd" />
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_process">
<bpmndi:BPMNPlane id="BPMNPlane_process" bpmnElement="VacationRequest">
<bpmndi:BPMNShape id="BPMNShape_applicantTask" bpmnElement="ApplicantTask">
<omgdc:Bounds x="304" y="122" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BPMNShape_approverTask" bpmnElement="ApproverTask">
<omgdc:Bounds x="465" y="122" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BPMNShape_approvalGateway" bpmnElement="ApprovalGateway" isMarkerVisible="true">
<omgdc:Bounds x="611" y="142" width="40" height="40" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BPMNShape_processEnd" bpmnElement="ProcessEnd">
<omgdc:Bounds x="876" y="149" width="28" height="28" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BPMNShape_processStart" bpmnElement="ProcessStart">
<omgdc:Bounds x="213" y="147" width="30" height="30" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_notification_di" bpmnElement="NotificationTask">
<omgdc:Bounds x="730" y="123" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
Spring Bootプロジェクトの作成
1. pom.xmlの依存関係設定
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>11</java.version>
<spring-boot.version>2.7.6</spring-boot.version>
<flowable.version>6.8.0</flowable.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>${flowable.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2. application.yml設定ファイル
データベース接続とフローエンジンの設定を行います。
server:
port: 8088
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/flowable?serverTimezone=Asia/Shanghai&useSSL=false
flowable:
activity-font-name: 明朝
label-font-name: 明朝
annotation-font-name: 明朝
logging:
level:
org.flowable: DEBUG
3. プロセスファイルの配置
- データベースにflowableスキーマを作成
- プロセスファイルをresources/processes/VacationRequest.bpmn20.xmlとして配置
※現時点ではアプリケーションを起動しないでください。起動時に自動的にテーブルが作成され、プロセスファイルがデプロイされます。
プロセス操作機能の実装
1. プロセス操作Controller
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.*;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.flowable.task.api.TaskQuery;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/workflow")
public class WorkflowController {
@Resource
private RuntimeService runtimeService;
@Resource
private TaskService taskService;
@Resource
private RepositoryService repositoryService;
@Resource
private ProcessEngine processEngine;
@Resource
private HistoryService historyService;
/**
* プロセスのデプロイ
*/
@PostMapping("/deploy")
public Map deployProcess() {
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("processes/VacationRequest.bpmn20.xml")
.name("休假申請プロセス")
.deploy();
Map<String, Object> result = new HashMap<>();
result.put("deploymentId", deployment.getId());
result.put("deploymentName", deployment.getName());
log.info("プロセスデプロイID: {}", deployment.getId());
log.info("プロセスデプロイ名: {}", deployment.getName());
return result;
}
/**
* プロセス定義情報の取得
*/
@PostMapping("/definition/{deploymentId}")
public ProcessDefinition getDefinitionInfo(@PathVariable("deploymentId") String deploymentId) {
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.deploymentId(deploymentId)
.singleResult();
return processDefinition;
}
/**
* プロセス開始
* @param applicant 申請者
* @param approver 承認者
*/
@PostMapping("/start/{applicant}/{approver}")
public String startProcess(
@PathVariable("applicant") String applicant,
@PathVariable("approver") String approver
) {
HashMap<String, Object> variables = new HashMap<>();
variables.put("applicant", applicant);
variables.put("approver", approver);
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("VacationRequest", variables);
StringBuilder result = new StringBuilder();
result.append("プロセス開始 ID: ").append(processInstance.getId()).append("\n");
List<Task> tasks = taskService.createTaskQuery()
.taskAssignee(applicant)
.orderByTaskCreateTime()
.desc()
.list();
for (Task task : tasks) {
result.append("タスクID: ").append(task.getId()).append("\n");
}
log.info(result.toString());
return result.toString();
}
/**
* タスクの実行
* @param action アクション(承認/却下)
* @param assignee 担当者
* @param taskId タスクID(任意)
*/
@PostMapping(value = {"/execute/{action}/{assignee}/{taskId}", "/execute/{action}/{assignee}"})
public String executeTask(
@PathVariable("action") String action,
@PathVariable("assignee") String assignee,
@PathVariable(value = "taskId", required = false) String taskId
) {
HashMap<String, Object> variables = new HashMap<>();
variables.put("approvalStatus", action);
if (!ObjectUtils.isEmpty(taskId)) {
taskService.complete(taskId, variables);
log.info("タスク {} を実行しました", taskId);
}
TaskQuery taskQuery = taskService.createTaskQuery();
List<Task> tasks = taskQuery.taskAssignee(assignee).list();
for (Task task : tasks) {
taskService.complete(task.getId(), variables);
}
return String.format("プロセス実行完了: %s で %s を実行しました",
tasks.stream().map(Task::getId).collect(Collectors.joining(",")), action);
}
/**
* ユーザータスクの一覧取得
* @param user ユーザー名
*/
@PostMapping({"/tasks/{user}", "/tasks/{taskId}/{user}"})
public List<Map<String, Object>> getTasks(
@PathVariable(value = "taskId", required = false) String taskId,
@PathVariable(value = "user") String user
) {
List<Task> taskList = taskService.createTaskQuery()
.processDefinitionKey("VacationRequest")
.taskAssignee(user)
.list();
List<Map<String, Object>> results = new ArrayList<>();
for (Task task : taskList) {
Map<String, Object> taskInfo = new HashMap<>();
taskInfo.put("taskId", task.getId());
taskInfo.put("taskName", task.getName());
taskInfo.put("assignee", task.getAssignee());
taskInfo.put("taskKey", task.getTaskDefinitionKey());
taskInfo.put("owner", task.getOwner());
taskInfo.put("createTime", task.getCreateTime());
results.add(taskInfo);
}
return results;
}
/**
* デプロイしたプロセスの削除
*/
@PostMapping("/remove/{deploymentId}")
public void removeProcess(@PathVariable("deploymentId") String deploymentId) {
repositoryService.deleteDeployment(deploymentId, true);
log.info("プロセスを削除しました: {}", deploymentId);
}
/**
* 履歴情報の取得
*/
@PostMapping("/history/{definitionId}")
public List<HistoricActivityInstance> getHistory(@PathVariable("definitionId") String definitionId) {
List<HistoricActivityInstance> historyList = historyService.createHistoricActivityInstanceQuery()
.processDefinitionId(definitionId)
.finished()
.orderByHistoricActivityInstanceEndTime().asc()
.list();
return historyList;
}
}
2. プロセス図表示Controller
package com.example.workflow.controller;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.*;
import org.flowable.engine.repository.ProcessDefinition;
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.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/workflow")
public class DiagramController {
private final boolean SHOW_SEQUENCE_LABELS = true;
@Resource
private RuntimeService runtimeService;
@Resource
private RepositoryService repositoryService;
@Resource
private ProcessEngine processEngine;
/**
* プロセス定義の取得(PNG/XML)
*/
@GetMapping("/definition/{resourceType}/{definitionId}")
public void getDefinitionDiagram(
HttpServletResponse response,
@PathVariable("resourceType") String resourceType,
@PathVariable("definitionId") String definitionId
) {
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionId(definitionId)
.singleResult();
String resourceName = "";
if ("png".equals(resourceType)) {
resourceName = processDefinition.getDiagramResourceName();
} else if ("xml".equals(resourceType)) {
resourceName = processDefinition.getResourceName();
}
try (
InputStream inputStream = repositoryService.getResourceAsStream(
processDefinition.getDeploymentId(), resourceName);
OutputStream outputStream = response.getOutputStream();
) {
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
} catch (Exception e) {
log.error("プロセス定義の取得に失敗しました", e);
}
}
/**
* リアルタイムプロセス図の取得
*/
@GetMapping("/diagram/{instanceId}")
public void getProcessDiagram(
HttpServletResponse response,
@PathVariable("instanceId") String instanceId
) {
response.setCharacterEncoding("UTF-8");
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId(instanceId)
.singleResult();
if (processInstance == null) {
return;
}
List<Execution> executions = runtimeService.createExecutionQuery()
.processInstanceId(instanceId)
.list();
List<String> activeActivityIds = new ArrayList<>();
for (Execution execution : executions) {
List<String> ids = runtimeService.getActiveActivityIds(execution.getId());
activeActivityIds.addAll(ids);
}
BpmnModel bpmnModel = repositoryService.getBpmnModel(processInstance.getProcessDefinitionId());
ProcessEngineConfiguration config = processEngine.getProcessEngineConfiguration();
ProcessDiagramGenerator diagramGenerator = config.getProcessDiagramGenerator();
byte[] buffer = new byte[1024];
try (
InputStream inputStream = diagramGenerator.generateDiagram(
bpmnModel, "png", activeActivityIds, new ArrayList<>(),
config.getActivityFontName(), config.getLabelFontName(),
config.getAnnotationFontName(), config.getClassLoader(), 1.0, SHOW_SEQUENCE_LABELS);
OutputStream outputStream = response.getOutputStream();
) {
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
} catch (Exception e) {
log.error("プロセス図の取得に失敗しました", e);
}
}
}
3. ServiceTask実装クラス
package com.example.workflow.service;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;
import java.util.Optional;
@Slf4j
public class SendApprovalNotification implements JavaDelegate {
@Override
public void execute(DelegateExecution execution) {
Optional.ofNullable(execution)
.ifPresentOrElse(
e -> log.info("プロセス{}: [休假承認完了]", e.getId()),
() -> log.error("通知送信に失敗しました")
);
}
}
動作確認手順
アプリケーション起動後、データベースにデフォルトで作成されるプロセスデータは削除し、自前でデプロイを行います。
1. プロセスデプロイ
URL: localhost:8088/workflow/deploy
デプロイ成功后、act_re_procdef、act_re_deployment、act_ge_bytearrayテーブルにデータが登録されます。
2. プロセス定義の確認
URL: localhost:8088/workflow/definition/png/{definitionId}
3. プロセスの開始
URL: localhost:8088/workflow/start/employeeA/managerB
このリクエストで、employeeAが休假を申請し、managerBが承認作業を行うプロセス实例が作成されます。
プロセス開始後、act_ru_executionとact_ru_taskテーブルにデータが登録されます。
4. 未処理タスクの確認
- タスク一覧:
localhost:8088/workflow/tasks/employeeA - リアルタイム図:
localhost:8088/workflow/diagram/{instanceId}
5. タスクの実行
申請タスクの実行:
localhost:8088/workflow/execute/申請/employeeA
承認タスクの実行:
localhost:8088/workflow/execute/承認/managerB
承認完了後、SendApprovalNotificationクラスが自動的に実行され、ログに出力されます:
プロセス{instanceId}: [休假承認完了]
プロセス定義XMLのserviceTask要素でflowable:class属性に実装クラスの完全修飾名を指定します:
<serviceTask id="NotificationTask" name="通知" flowable:class="com.example.workflow.service.SendApprovalNotification">
<incoming>ApprovalFlow</incoming>
<outgoing>EndFlow</outgoing>
</serviceTask>
6. 履歴の確認
URL: localhost:8088/workflow/history/{definitionId}
プロセス実行の履歴が返されます。履歴データはact_hi_で始まるテーブルに保存されます。