项目结构
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。