15 KiB
15 KiB
插件开发指南
版本: v1.0
日期: 2025-01-13
目标: 指导开发者快速上手节点插件开发
一、快速开始(5分钟创建你的第一个插件)
1.1 前提条件
- JDK 17+
- Maven 3.6+
- 基本的Java和Spring知识
- 已有Flowable DevOps项目运行环境
1.2 创建插件类
在 src/main/java/com/flowable/devops/workflow/node/ 目录下创建新类:
package com.flowable.devops.workflow.node;
import com.flowable.devops.entity.NodeType;
import com.flowable.devops.workflow.model.*;
import com.flowable.devops.workflow.plugin.NodePlugin;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 示例插件:文本处理节点
*
* 功能:
* - 转大写
* - 转小写
* - 反转字符串
*/
@Slf4j
@NodePlugin(
id = "text_processor",
name = "textProcessor",
displayName = "Text Processor",
category = NodeType.NodeCategory.TRANSFORM,
version = "1.0.0",
author = "Your Name",
description = "文本处理工具,支持大小写转换和字符串反转",
icon = "font-size",
enabled = true
)
@Component
public class TextProcessorNode implements WorkflowNode {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public NodeType getMetadata() {
NodeType nodeType = new NodeType();
nodeType.setId("text_processor");
nodeType.setName("textProcessor");
nodeType.setDisplayName("Text Processor");
nodeType.setCategory(NodeType.NodeCategory.TRANSFORM);
nodeType.setIcon("font-size");
nodeType.setDescription("文本处理工具");
nodeType.setImplementationClass(this.getClass().getName());
nodeType.setEnabled(true);
// 字段定义
nodeType.setFields(createFieldsJson());
// 输出结构
nodeType.setOutputSchema(createOutputSchemaJson());
return nodeType;
}
/**
* 创建字段定义
*/
private com.fasterxml.jackson.databind.JsonNode createFieldsJson() {
ArrayNode fields = objectMapper.createArrayNode();
// 输入文本
ObjectNode inputField = objectMapper.createObjectNode();
inputField.put("name", "text");
inputField.put("label", "Input Text");
inputField.put("type", "text");
inputField.put("required", true);
inputField.put("supportsExpression", true);
inputField.put("placeholder", "Enter text to process...");
fields.add(inputField);
// 操作类型
ObjectNode operationField = objectMapper.createObjectNode();
operationField.put("name", "operation");
operationField.put("label", "Operation");
operationField.put("type", "select");
operationField.put("required", true);
operationField.put("defaultValue", "uppercase");
ArrayNode operations = objectMapper.createArrayNode();
operations.add("uppercase");
operations.add("lowercase");
operations.add("reverse");
operationField.set("options", operations);
fields.add(operationField);
return fields;
}
/**
* 创建输出结构
*/
private com.fasterxml.jackson.databind.JsonNode createOutputSchemaJson() {
ObjectNode schema = objectMapper.createObjectNode();
schema.put("type", "object");
ObjectNode properties = objectMapper.createObjectNode();
ObjectNode result = objectMapper.createObjectNode();
result.put("type", "string");
result.put("description", "处理后的文本");
properties.set("result", result);
ObjectNode operation = objectMapper.createObjectNode();
operation.put("type", "string");
operation.put("description", "执行的操作");
properties.set("operation", operation);
schema.set("properties", properties);
return schema;
}
@Override
public NodeExecutionResult execute(NodeInput input, NodeExecutionContext context) {
LocalDateTime startTime = LocalDateTime.now();
try {
// 1. 获取输入参数
String text = input.getStringRequired("text");
String operation = input.getString("operation", "uppercase");
log.info("执行文本处理: operation={}, text={}", operation, text);
// 2. 执行操作
String result;
switch (operation) {
case "uppercase":
result = text.toUpperCase();
break;
case "lowercase":
result = text.toLowerCase();
break;
case "reverse":
result = new StringBuilder(text).reverse().toString();
break;
default:
throw new IllegalArgumentException("Unknown operation: " + operation);
}
// 3. 构建输出
Map<String, Object> output = new HashMap<>();
output.put("result", result);
output.put("operation", operation);
LocalDateTime endTime = LocalDateTime.now();
log.info("文本处理完成: {} -> {}", text, result);
return NodeExecutionResult.success(output, startTime, endTime);
} catch (Exception e) {
LocalDateTime endTime = LocalDateTime.now();
log.error("文本处理失败: {}", e.getMessage(), e);
return NodeExecutionResult.failed(e.getMessage(), e.getClass().getSimpleName(), startTime, endTime);
}
}
@Override
public void validate(NodeInput input) {
// 验证必需参数
input.getStringRequired("text");
String operation = input.getString("operation");
if (operation != null &&
!operation.equals("uppercase") &&
!operation.equals("lowercase") &&
!operation.equals("reverse")) {
throw new IllegalArgumentException("Invalid operation: " + operation);
}
}
}
1.3 测试插件
重启应用:
mvn spring-boot:run
查看日志:
========================================
开始初始化插件系统...
========================================
✓ 加载内置插件: 2 个
✓ http_request: HTTP Request (1.0.0)
✓ text_processor: Text Processor (1.0.0)
========================================
插件清单:
========================================
【API接口】(1 个)
✓ 📦 HTTP Request v1.0.0 - Flowable Team (INTERNAL)
【数据转换】(1 个)
✓ 📦 Text Processor v1.0.0 - Your Name (INTERNAL)
========================================
调用API测试:
# 获取所有节点类型
curl http://localhost:8080/api/node-types
# 应该能看到新的text_processor节点
二、核心概念
2.1 插件注解 (@NodePlugin)
@NodePlugin(
id = "unique_id", // 必需:全局唯一ID
name = "nodeName", // 必需:代码引用名
displayName = "Display Name", // 必需:UI显示名
category = NodeCategory.API, // 必需:分类
version = "1.0.0", // 版本号
author = "Your Name", // 作者
description = "描述", // 功能描述
icon = "api", // 图标
enabled = true // 默认启用
)
参数说明:
| 参数 | 类型 | 必需 | 说明 | 示例 |
|---|---|---|---|---|
| id | String | ✅ | 节点唯一ID | "http_request" |
| name | String | ✅ | 代码引用名 | "httpRequest" |
| displayName | String | ✅ | UI显示名 | "HTTP Request" |
| category | NodeCategory | ✅ | 分类 | API, DATABASE, LOGIC, NOTIFICATION, TRANSFORM, OTHER |
| version | String | ❌ | 版本号 | "1.0.0" |
| author | String | ❌ | 作者 | "Your Name" |
| description | String | ❌ | 描述 | "发送HTTP请求" |
| icon | String | ❌ | 图标 | "api", "database", "mail" |
| enabled | boolean | ❌ | 默认启用 | true |
2.2 节点接口 (WorkflowNode)
所有插件必须实现 WorkflowNode 接口:
public interface WorkflowNode {
/**
* 获取节点元数据
* 包含字段定义、输出结构等
*/
NodeType getMetadata();
/**
* 执行节点
* @param input 输入参数(表达式已解析)
* @param context 执行上下文
* @return 执行结果
*/
NodeExecutionResult execute(NodeInput input, NodeExecutionContext context);
/**
* 验证节点配置(可选)
* @param input 输入参数
*/
default void validate(NodeInput input) {
// 默认不验证
}
}
2.3 节点元数据 (NodeType)
定义节点的UI表现和数据结构:
NodeType nodeType = new NodeType();
nodeType.setId("my_node");
nodeType.setDisplayName("My Node");
nodeType.setCategory(NodeCategory.API);
nodeType.setFields(fieldsJson); // 字段定义
nodeType.setOutputSchema(schemaJson); // 输出结构
字段定义示例:
[
{
"name": "url",
"label": "URL",
"type": "text",
"required": true,
"supportsExpression": true,
"placeholder": "https://api.example.com"
},
{
"name": "method",
"label": "Method",
"type": "select",
"options": ["GET", "POST", "PUT", "DELETE"],
"defaultValue": "GET"
}
]
输出结构示例 (JSON Schema):
{
"type": "object",
"properties": {
"statusCode": {
"type": "number",
"description": "HTTP状态码"
},
"body": {
"type": "object",
"description": "响应体"
}
}
}
2.4 执行结果 (NodeExecutionResult)
返回节点执行结果:
// 成功
return NodeExecutionResult.success(output, startTime, endTime);
// 失败
return NodeExecutionResult.failed(errorMessage, errorType, startTime, endTime);
// 跳过
return NodeExecutionResult.skipped(reason, startTime, endTime);
三、最佳实践
3.1 节点设计原则
✅ 无状态设计
// ✅ 推荐:所有状态通过参数传递
public NodeExecutionResult execute(NodeInput input, NodeExecutionContext context) {
String url = input.getString("url");
// ...
}
// ❌ 禁止:在节点中保存状态
private String lastUrl; // 不要这样!
✅ 幂等性
// 多次执行相同输入应得到相同结果
// 对于非幂等操作(如发送邮件),应该有幂等性保护机制
✅ 错误处理
try {
// 业务逻辑
return NodeExecutionResult.success(output, startTime, endTime);
} catch (Exception e) {
log.error("执行失败: {}", e.getMessage(), e);
return NodeExecutionResult.failed(e.getMessage(), e.getClass().getSimpleName(), startTime, endTime);
}
✅ 日志记录
log.info("开始执行HTTP请求: {}", url);
log.debug("请求参数: {}", params);
log.error("请求失败: {}", e.getMessage(), e);
3.2 字段定义最佳实践
支持表达式:
{
"name": "url",
"supportsExpression": true, // 允许使用 ${nodes.xxx.output.yyy}
"supportsFieldMapping": true // 允许使用字段映射选择器
}
提供默认值:
{
"name": "timeout",
"type": "number",
"defaultValue": 30000
}
完整的字段类型:
text: 单行文本textarea: 多行文本number: 数字boolean: 布尔值select: 下拉选择code: 代码编辑器key_value: 键值对列表
3.3 输出结构设计
清晰的层次结构:
{
"type": "object",
"properties": {
"data": {
"type": "object",
"description": "主数据"
},
"metadata": {
"type": "object",
"description": "元数据"
}
}
}
提供详细的字段说明:
{
"statusCode": {
"type": "number",
"description": "HTTP状态码 (200-599)"
}
}
四、高级功能
4.1 访问上游节点数据
@Override
public NodeExecutionResult execute(NodeInput input, NodeExecutionContext context) {
// 获取上游节点的输出
Map<String, Object> upstreamData = context.getNodeOutput("upstream_node_id");
if (upstreamData != null) {
Object someValue = upstreamData.get("output");
// 使用上游数据...
}
// ...
}
4.2 访问环境变量
String apiKey = context.getEnv("API_KEY");
4.3 访问工作流变量
Map<String, Object> workflowVars = context.getVariables();
Object inputData = workflowVars.get("input");
4.4 异步执行(高级)
// 注意:第一期不支持,使用同步执行
// 第二期可以使用CompletableFuture实现异步
五、测试
5.1 单元测试
@SpringBootTest
public class TextProcessorNodeTest {
@Autowired
private TextProcessorNode node;
@Test
public void testUppercase() {
// 准备输入
Map<String, Object> config = Map.of(
"text", "hello",
"operation", "uppercase"
);
NodeInput input = new NodeInput(config);
// 准备上下文
NodeExecutionContext context = NodeExecutionContext.builder()
.nodeId("test_node")
.build();
// 执行
NodeExecutionResult result = node.execute(input, context);
// 验证
assertEquals(NodeExecutionResult.ExecutionStatus.SUCCESS, result.getStatus());
assertEquals("HELLO", result.getOutput().get("result"));
}
}
5.2 集成测试
创建完整的工作流进行测试:
{
"name": "测试文本处理",
"nodes": [
{
"id": "node1",
"type": "text_processor",
"config": {
"text": "Hello World",
"operation": "uppercase"
}
}
],
"edges": []
}
六、常见问题
Q1: 如何添加新的字段类型?
A: 修改前端的字段渲染组件,后端只需要在 fields JSON中定义即可。
Q2: 节点可以调用外部服务吗?
A: 可以,但建议使用 WebClient(异步)而不是 RestTemplate(同步)。
Q3: 如何处理大文件?
A: 避免在节点输出中保存大文件,使用文件引用(URL或路径)。
Q4: 节点可以访问数据库吗?
A: 可以通过 Spring 注入 Repository,但不推荐直接访问,建议通过 Service 层。
Q5: 如何debug插件?
A:
- 增加日志输出
- 使用IDE断点调试
- 查看节点执行日志表
七、示例插件库
7.1 数据库查询节点
参考:DatabaseQueryNode.java(待实现)
7.2 发送邮件节点
参考:SendEmailNode.java(待实现)
7.3 Slack通知节点
参考:SlackNotificationNode.java(待实现)
八、发布插件
8.1 内置插件
直接提交代码到主仓库即可。
8.2 外部插件(第二期)
- 打包为JAR
- 上传到插件市场
- 用户下载安装
下一步:查看 06-插件化架构设计.md 了解系统架构。