(Spring Bean + delegateExpression)实现http服务 的完整可运行项目

项目结构

复制代码
flowable-http-demo/
├── pom.xml
└── src/
    └── main/
        ├── java/com/example/demo/
        │   ├── FlowableHttpApplication.java
        │   ├── config/
        │   │   └── RestTemplateConfig.java
        │   ├── controller/
        │   │   └── ProcessController.java
        │   └── delegate/
        │       ├── HttpServiceTask.java      ← 核心HTTP组件
        │       ├── SuccessHandler.java
        │       └── ErrorHandler.java
        └── resources/
            ├── application.yml
            └── processes/
                └── http-demo.bpmn20.xml

1. pom.xml(依赖)

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>flowable-http-demo</artifactId>
    <version>1.0.0</version>
    <name>flowable-http-demo</name>
    
    <properties>
        <java.version>1.8</java.version>
        <flowable.version>6.8.0</flowable.version>
    </properties>
    
    <dependencies>
        <!-- Flowable Spring Boot Starter -->
        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-spring-boot-starter-process</artifactId>
            <version>${flowable.version}</version>
        </dependency>
        
        <!-- Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- H2 内存数据库(演示用,生产请换MySQL/PostgreSQL) -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- JSON处理 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2. application.yml

XML 复制代码
server:
  port: 8080

spring:
  application:
    name: flowable-http-demo

# Flowable 流程引擎配置
flowable:
  # 自动更新数据库表结构
  database-schema-update: true
  # 自动部署 processes 目录下的流程定义
  deployment:
    resources: classpath*:processes/*.bpmn20.xml
  # 开启异步执行器
  async-executor-activate: true

logging:
  level:
    org.flowable.engine.impl.persistence.entity: debug
    com.example.demo.delegate: debug

3. 核心:HttpServiceTask.java

设计要点

  • 线程安全 :不通过 flowable:field 注入动态值(单例 Bean 会有并发覆盖问题),全部通过 流程变量 读取。

  • 4xx/5xx 不直接抛异常 :先捕获为流程变量,由 BPMN 网关自行判断;只有网络超时/连接失败才抛 BpmnError

  • 支持 ${变量} 模板替换 :URL、Headers、Body 中都可以用 ${token} 引用流程变量。

HttpServiceTask.java

java 复制代码
package com.example.demo.delegate;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.flowable.engine.delegate.BpmnError;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Flowable 自定义 HTTP 服务任务(Spring Bean 单例模式)
 * 通过流程变量接收动态参数,保证线程安全
 */
@Component("httpServiceTask")
public class HttpServiceTask implements JavaDelegate {

    private static final Logger logger = LoggerFactory.getLogger(HttpServiceTask.class);
    private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\{(\\w+)\\}");

    @Autowired
    private RestTemplate restTemplate;
    
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void execute(DelegateExecution execution) {
        String processId = execution.getProcessInstanceId();
        String activityId = execution.getCurrentActivityId();
        logger.info("[HTTP任务] 流程实例: {}, 节点: {}", processId, activityId);

        String responseVar = null;

        try {
            // ========== 1. 读取流程变量(所有动态参数) ==========
            String url = getRequiredVariable(execution, "httpUrl");
            String method = getVariableOrDefault(execution, "httpMethod", "GET");
            String headersJson = getVariableOrDefault(execution, "httpHeaders", "{}");
            String body = getVariableOrDefault(execution, "httpBody", null);
            responseVar = getVariableOrDefault(execution, "httpResponseVariable", "httpResponse");
            String failOnError = getVariableOrDefault(execution, "httpFailOnError", "true");

            // 解析模板变量,如 https://api.example.com/user/${userId}
            url = resolveTemplate(url, execution);
            if (body != null) body = resolveTemplate(body, execution);

            // ========== 2. 构建请求头 ==========
            HttpHeaders httpHeaders = new HttpHeaders();
            // 默认 JSON,除非用户覆盖
            if (!headersJson.contains("Content-Type")) {
                httpHeaders.setContentType(MediaType.APPLICATION_JSON);
            }
            
            Map<String, String> headerMap = objectMapper.readValue(
                headersJson, new TypeReference<Map<String, String>>() {}
            );
            headerMap.forEach((key, value) -> 
                httpHeaders.add(key, resolveTemplate(value, execution))
            );

            // ========== 3. 发送请求 ==========
            HttpMethod httpMethod = HttpMethod.valueOf(method.toUpperCase());
            HttpEntity<String> entity = new HttpEntity<>(body, httpHeaders);
            
            logger.info("[HTTP任务] 请求: {} {}", method, url);
            ResponseEntity<String> response = restTemplate.exchange(
                url, httpMethod, entity, String.class
            );

            // 正常响应 (2xx)
            storeResponse(execution, responseVar, response.getBody(), 
                         response.getStatusCodeValue(), true);

        } catch (HttpClientErrorException | HttpServerErrorException e) {
            // 4xx/5xx:提取错误响应,根据配置决定是否抛 BpmnError
            logger.warn("[HTTP任务] HTTP错误: status={}, body={}", 
                       e.getRawStatusCode(), e.getResponseBodyAsString());
            storeResponse(execution, responseVar, e.getResponseBodyAsString(), 
                         e.getRawStatusCode(), false);
            
            String failOnError = getVariableOrDefault(execution, "httpFailOnError", "true");
            if ("true".equalsIgnoreCase(failOnError)) {
                throw new BpmnError("HTTP_ERROR", 
                    "HTTP返回错误码: " + e.getRawStatusCode() + ", " + e.getResponseBodyAsString());
            }

        } catch (ResourceAccessException e) {
            // 连接超时、网络不可达等
            logger.error("[HTTP任务] 连接失败: {}", e.getMessage());
            throw new BpmnError("HTTP_ERROR", "网络连接失败: " + e.getMessage());

        } catch (Exception e) {
            logger.error("[HTTP任务] 未知异常: {}", e.getMessage(), e);
            throw new BpmnError("HTTP_ERROR", "HTTP任务执行异常: " + e.getMessage());
        }
    }

    // ========== 工具方法 ==========

    private String getRequiredVariable(DelegateExecution execution, String name) {
        Object val = execution.getVariable(name);
        if (val == null) {
            throw new BpmnError("HTTP_CONFIG_ERROR", "缺少必填流程变量: " + name);
        }
        return val.toString();
    }

    private String getVariableOrDefault(DelegateExecution execution, String name, String def) {
        Object val = execution.getVariable(name);
        return val != null ? val.toString() : def;
    }

    /**
     * 将 ${varName} 替换为对应流程变量值
     */
    private String resolveTemplate(String text, DelegateExecution execution) {
        if (text == null || !text.contains("${")) return text;
        Matcher m = VARIABLE_PATTERN.matcher(text);
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            String varName = m.group(1);
            Object varValue = execution.getVariable(varName);
            m.appendReplacement(sb, varValue != null ? Matcher.quoteReplacement(varValue.toString()) : "");
        }
        m.appendTail(sb);
        return sb.toString();
    }

    /**
     * 将响应结果写回流程变量
     */
    private void storeResponse(DelegateExecution execution, String varPrefix, 
                               String body, int statusCode, boolean success) throws IOException {
        execution.setVariable(varPrefix, body);
        execution.setVariable(varPrefix + "_statusCode", statusCode);
        execution.setVariable(varPrefix + "_success", success);

        // 如果响应是 JSON,额外解析为 Map,方便后续表达式使用,如 ${httpResponse_json.data.userName}
        if (body != null && body.trim().startsWith("{")) {
            try {
                Map<String, Object> json = objectMapper.readValue(body, new TypeReference<Map<String, Object>>() {});
                execution.setVariable(varPrefix + "_json", json);
            } catch (Exception ignored) {
                // 非标准 JSON,忽略
            }
        }
        logger.info("[HTTP任务] 响应已存储: {}_statusCode={}, {}_success={}", 
                   varPrefix, statusCode, varPrefix, success);
    }
}

4. RestTemplateConfig.java(生产级配置)

RestTemplateConfig.java

java 复制代码
package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        // 连接超时 5 秒
        factory.setConnectTimeout(5000);
        // 读取超时 10 秒
        factory.setReadTimeout(10000);
        
        RestTemplate restTemplate = new RestTemplate(factory);
        
        // 自定义错误处理器:让 4xx/5xx 也能正常返回给 HttpServiceTask 处理
        restTemplate.setErrorHandler(new org.springframework.web.client.DefaultResponseErrorHandler() {
            @Override
            public void handleError(org.springframework.http.client.ClientHttpResponse response) {
                // 不抛异常,由 HttpServiceTask 自行捕获 HttpClientErrorException
            }
        });
        
        return restTemplate;
    }
}

5. 辅助 Delegate(成功/失败处理)

SuccessHandler.java

java 复制代码
package com.example.demo.delegate;

import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component("successHandler")
public class SuccessHandler implements JavaDelegate {
    
    private static final Logger logger = LoggerFactory.getLogger(SuccessHandler.class);
    
    @Override
    public void execute(DelegateExecution execution) {
        String resp = (String) execution.getVariable("httpResponse");
        logger.info("[业务处理] 接口调用成功,响应内容: {}", resp);
        // 这里可以写数据库、发消息等
    }
}

ErrorHandler.java

java 复制代码
package com.example.demo.delegate;

import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component("errorHandler")
public class ErrorHandler implements JavaDelegate {
    
    private static final Logger logger = LoggerFactory.getLogger(ErrorHandler.class);
    
    @Override
    public void execute(DelegateExecution execution) {
        logger.error("[错误处理] 流程实例 {} 发生HTTP异常,进入补偿逻辑", 
                    execution.getProcessInstanceId());
        // 记录失败日志、发送告警、落补偿表等
    }
}

6. BPMN 流程定义:http-demo.bpmn20.xml

放到 src/main/resources/processes/http-demo.bpmn20.xml,启动时会自动部署。

java 复制代码
<?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:flowable="http://flowable.org/bpmn"
             targetNamespace="http://example.com/http-demo"
             xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL 
                                 https://www.flowable.org/schema/bpmn/bpmn.xsd">

    <process id="httpDemoProcess" name="HTTP调用演示流程" isExecutable="true">
        
        <!-- 开始 -->
        <startEvent id="start" name="开始" />
        <sequenceFlow id="f1" sourceRef="start" targetRef="httpTask" />

        <!-- ========== 核心:HTTP 服务任务 ========== -->
        <!-- 通过 delegateExpression 引用 Spring 单例 Bean -->
        <serviceTask id="httpTask" name="调用外部API" 
                     flowable:delegateExpression="${httpServiceTask}" />
        
        <sequenceFlow id="f2" sourceRef="httpTask" targetRef="checkStatus" />

        <!-- 排他网关:根据状态码分流 -->
        <exclusiveGateway id="checkStatus" name="判断响应状态" />
        
        <sequenceFlow id="f3" sourceRef="checkStatus" targetRef="successTask">
            <conditionExpression xsi:type="tFormalExpression">
                ${httpResponse_statusCode == 200}
            </conditionExpression>
        </sequenceFlow>
        
        <sequenceFlow id="f4" sourceRef="checkStatus" targetRef="failUserTask">
            <conditionExpression xsi:type="tFormalExpression">
                ${httpResponse_statusCode != 200}
            </conditionExpression>
        </sequenceFlow>

        <!-- 成功分支 -->
        <serviceTask id="successTask" name="处理成功结果" 
                     flowable:delegateExpression="${successHandler}" />
        <sequenceFlow id="f5" sourceRef="successTask" targetRef="endSuccess" />
        <endEvent id="endSuccess" name="成功结束" />

        <!-- 失败分支(人工介入) -->
        <userTask id="failUserTask" name="人工处理失败" flowable:assignee="admin" />
        <sequenceFlow id="f6" sourceRef="failUserTask" targetRef="endFail" />
        <endEvent id="endFail" name="失败结束" />

        <!-- ========== 边界错误事件:捕获网络异常 / BpmnError ========== -->
        <boundaryEvent id="httpErrorEvent" attachedToRef="httpTask" cancelActivity="true">
            <errorEventDefinition errorRef="HTTP_ERROR" />
        </boundaryEvent>
        
        <sequenceFlow id="f7" sourceRef="httpErrorEvent" targetRef="errorTask" />
        
        <serviceTask id="errorTask" name="异常补偿处理" 
                     flowable:delegateExpression="${errorHandler}" />
        <sequenceFlow id="f8" sourceRef="errorTask" targetRef="endError" />
        <endEvent id="endError" name="异常结束" />

    </process>
</definitions>

7. 测试接口:ProcessController.java

java 复制代码
package com.example.demo.controller;

import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api")
public class ProcessController {

    @Autowired
    private RuntimeService runtimeService;
    
    @Autowired
    private TaskService taskService;

    /**
     * 启动流程并发起 HTTP 调用
     * 
     * 示例请求体:
     * {
     *   "url": "https://httpbin.org/get",
     *   "method": "GET",
     *   "headers": "{\"Accept\":\"application/json\"}",
     *   "businessId": "ORDER_2024001"
     * }
     */
    @PostMapping("/start")
    public Map<String, Object> startProcess(@RequestBody Map<String, Object> params) {
        Map<String, Object> variables = new HashMap<>();
        
        // 注入 HTTP 任务所需的流程变量
        variables.put("httpUrl", params.getOrDefault("url", "https://httpbin.org/get"));
        variables.put("httpMethod", params.getOrDefault("method", "GET"));
        variables.put("httpHeaders", params.getOrDefault("headers", "{}"));
        variables.put("httpBody", params.get("body"));
        variables.put("httpResponseVariable", "httpResponse");
        variables.put("httpFailOnError", "false"); // 4xx/5xx 不抛错,走网关判断
        
        // 业务变量(可在 URL/Body 模板中使用,如 https://api.example.com/order/${businessId})
        variables.put("businessId", params.get("businessId"));

        ProcessInstance instance = runtimeService
            .startProcessInstanceByKey("httpDemoProcess", variables);

        Map<String, Object> result = new HashMap<>();
        result.put("processInstanceId", instance.getId());
        result.put("message", "流程已启动");
        return result;
    }

    /**
     * 查询流程执行后的变量(看 HTTP 响应)
     */
    @GetMapping("/variables/{processInstanceId}")
    public Map<String, Object> getVariables(@PathVariable String processInstanceId) {
        Map<String, Object> vars = runtimeService.getVariables(processInstanceId);
        
        Map<String, Object> result = new HashMap<>();
        result.put("httpResponse", vars.get("httpResponse"));
        result.put("httpResponse_statusCode", vars.get("httpResponse_statusCode"));
        result.put("httpResponse_success", vars.get("httpResponse_success"));
        result.put("httpResponse_json", vars.get("httpResponse_json"));
        return result;
    }

    /**
     * 查询待办任务(演示失败分支的人工任务)
     */
    @GetMapping("/tasks")
    public List<Task> getTasks(@RequestParam String assignee) {
        return taskService.createTaskQuery()
                .taskAssignee(assignee)
                .list();
    }
}

8. 启动类

java 复制代码
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class FlowableHttpApplication {
    public static void main(String[] args) {
        SpringApplication.run(FlowableHttpApplication.class, args);
    }
}

9. 运行与测试

9.1 启动项目

直接运行 FlowableHttpApplication.main(),Flowable 会自动部署 processes/http-demo.bpmn20.xml

9.2 发起测试调用

复制代码
# 调用 httpbin.org 测试 GET 接口
curl -X POST http://localhost:8080/api/start \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://httpbin.org/get",
    "method": "GET",
    "headers": "{\"Accept\":\"application/json\"}",
    "businessId": "TEST_001"
  }'

返回示例:

JSON

复制代码
{
  "processInstanceId": "2501",
  "message": "流程已启动"
}

9.3 查看 HTTP 响应结果

复制代码
curl http://localhost:8080/api/variables/2501

返回:JSON

XML 复制代码
{
  "httpResponse": "{\"args\":{},\"headers\":{\"Accept\":\"application/json\"},...}",
  "httpResponse_statusCode": 200,
  "httpResponse_success": true,
  "httpResponse_json": {
    "args": {},
    "headers": { "Accept": "application/json" }
  }
}

9.4 测试 POST + Body

复制代码
curl -X POST http://localhost:8080/api/start \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://httpbin.org/post",
    "method": "POST",
    "headers": "{\"Content-Type\":\"application/json\"}",
    "body": "{\"orderId\":\"${businessId}\",\"amount\":100}",
    "businessId": "ORDER_9527"
  }'

注意${businessId} 会在 HttpServiceTask.resolveTemplate() 中被替换为实际值 ORDER_9527

相关推荐
Ai拆代码的曹操2 小时前
Spring 事务 REQUIRES_NEW 嵌套调用:连接池翻倍的秘密
java·后端·spring
wear工程师8 小时前
WebFlux 里能不能直接 block?面试别只答「不能」
spring
凤山老林11 小时前
SpringBoot 3 启用 spring.factories 后如何升级替换原有的 spring.factories ?
spring boot·后端·spring
小罗水12 小时前
第1章 大模型、企业知识库与 RAG 平台概述
java·spring
qq_1851986912 小时前
骆驼任务Flowable + Apache Camel 集成
spring·apache·flowable
lichuangcsdn13 小时前
【Spring AI 学习(一)】spring ai是什么
人工智能·学习·spring·spring ai
吃饱了得干活1 天前
@Transactional 又失效了?把这 8 个坑全填了!
java·后端·spring
今天AI了吗1 天前
Spring AI 框架实战:Java 后端集成大模型的架构设计与工程落地
java·人工智能·python·spring·机器学习
乐观的Terry1 天前
9、发布系统-Webhook自动发布
java·spring boot·spring·spring cloud·mybatis