FlowableワークフローエンジンとSpring Bootの連携実装

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_で始まるテーブルに保存されます。

タグ: Flowable spring-boot BPMN Workflow Java

7月26日 17:09 投稿