19 KiB
19 KiB
插件化架构设计文档
版本: v1.0
日期: 2025-01-13
目标: 将工作流节点改造为插拔式插件架构
一、设计目标
1.1 核心目标
插拔式节点开发:
- 新增节点无需修改核心代码
- 节点可独立打包、发布、部署
- 支持节点的启用/禁用/卸载
- 节点间完全隔离,互不影响
开发体验优化:
- 节点开发简单,只需实现接口
- 提供完整的开发工具和模板
- 自动生成节点元数据
- 支持热加载,无需重启
生态建设:
- 支持第三方开发者贡献节点
- 插件市场(长期规划)
- 插件质量评级和审核机制
二、插件化方案对比
2.1 方案选择
我们采用 方案1(轻量级插件化)+ 方案2部分特性 的混合方案:
| 特性 | 第一期 | 第二期 | 第三期 |
|---|---|---|---|
| 注解驱动开发 | ✅ | ✅ | ✅ |
| 自动扫描注册 | ✅ | ✅ | ✅ |
| 外部JAR加载 | ❌ | ✅ | ✅ |
| 独立类加载器 | ❌ | ✅ | ✅ |
| 热加载/卸载 | ❌ | ⚠️ 实验 | ✅ |
| 插件依赖管理 | ❌ | ❌ | ✅ |
理由:
- 第一期快速验证,专注核心功能
- 第二期支持外部开发,实现真正的插件化
- 第三期完善生态,支持插件市场
三、架构设计
3.1 整体架构
┌─────────────────────────────────────────────────────────┐
│ 应用启动 │
│ ↓ │
│ PluginManager.initialize() │
│ ↓ │
│ ┌────────────┴────────────┐ │
│ ↓ ↓ │
│ 内置插件扫描 外部插件扫描 │
│ (classpath) (plugins/ 目录) │
│ ↓ ↓ │
│ @NodePlugin plugin.json │
│ ↓ ↓ │
│ └────────────┬────────────┘ │
│ ↓ │
│ PluginRegistry │
│ (注册所有可用插件) │
│ ↓ │
│ ┌────────────┴────────────┐ │
│ ↓ ↓ │
│ NodeTypeRegistry Spring Context │
│ (节点类型元数据) (Bean实例) │
└─────────────────────────────────────────────────────────┘
运行时:
1. 用户在前端拖拽节点 → 调用 NodeTypeRegistry.getMetadata()
2. 执行工作流 → GenericNodeExecutor → PluginRegistry.getInstance()
3. 管理员启用/禁用插件 → PluginManager.enable/disable()
3.2 核心组件
1. @NodePlugin 注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component // 继承Spring注解,自动注册为Bean
public @interface NodePlugin {
String id(); // 节点唯一ID
String name(); // 节点名称
String displayName(); // 显示名称
NodeCategory category(); // 分类
String version() default "1.0.0"; // 版本
String author() default ""; // 作者
String description() default ""; // 描述
boolean enabled() default true; // 默认是否启用
String[] dependencies() default {}; // 依赖的其他插件
}
2. PluginDescriptor(插件描述符)
@Data
@Builder
public class PluginDescriptor {
private String id;
private String name;
private String displayName;
private String version;
private String author;
private String description;
private NodeCategory category;
private boolean enabled;
private PluginSource source; // INTERNAL, EXTERNAL
// 外部插件特有
private String jarPath;
private ClassLoader classLoader;
private List<String> dependencies;
// 节点元数据
private NodeType metadata;
// 运行时信息
private LocalDateTime loadedAt;
private PluginStatus status; // LOADED, ACTIVE, DISABLED, ERROR
private String errorMessage;
}
public enum PluginSource {
INTERNAL, // 内置插件(classpath)
EXTERNAL // 外部插件(JAR文件)
}
public enum PluginStatus {
LOADED, // 已加载
ACTIVE, // 活跃
DISABLED, // 禁用
ERROR // 错误
}
3. PluginManager(插件管理器)
职责:
- 扫描并加载所有插件
- 管理插件生命周期(加载/启用/禁用/卸载)
- 插件依赖解析
- 错误处理和日志记录
@Service
public class PluginManager {
private final Map<String, PluginDescriptor> plugins = new ConcurrentHashMap<>();
@PostConstruct
public void initialize() {
log.info("开始初始化插件系统...");
// 1. 扫描内置插件(classpath)
loadInternalPlugins();
// 2. 扫描外部插件(plugins目录)
loadExternalPlugins();
// 3. 解析依赖关系
resolveDependencies();
// 4. 注册到NodeTypeRegistry
registerAllPlugins();
log.info("插件系统初始化完成,共加载 {} 个插件", plugins.size());
}
private void loadInternalPlugins() { ... }
private void loadExternalPlugins() { ... }
private void resolveDependencies() { ... }
// 插件生命周期管理
public void enablePlugin(String pluginId) { ... }
public void disablePlugin(String pluginId) { ... }
public void reloadPlugin(String pluginId) { ... }
// 查询
public List<PluginDescriptor> getAllPlugins() { ... }
public PluginDescriptor getPlugin(String pluginId) { ... }
}
4. PluginLoader(插件加载器)
职责:
- 从JAR文件加载插件类
- 创建独立的ClassLoader(第二期)
- 验证插件合法性
@Component
public class PluginLoader {
/**
* 从JAR文件加载插件
*/
public PluginDescriptor loadFromJar(File jarFile) {
// 1. 读取 plugin.json
PluginManifest manifest = readManifest(jarFile);
// 2. 创建类加载器(第二期实现隔离)
ClassLoader classLoader = createClassLoader(jarFile);
// 3. 加载主类
Class<?> pluginClass = classLoader.loadClass(manifest.getMainClass());
// 4. 实例化
WorkflowNode instance = instantiatePlugin(pluginClass);
// 5. 构建描述符
return buildDescriptor(instance, manifest, jarFile, classLoader);
}
/**
* 验证插件
*/
public void validatePlugin(PluginDescriptor descriptor) {
// 检查必需字段
// 检查依赖是否满足
// 检查版本兼容性
}
}
5. PluginClassLoader(独立类加载器,第二期)
public class PluginClassLoader extends URLClassLoader {
private final String pluginId;
private final Set<String> sharedPackages; // 与主应用共享的包
public PluginClassLoader(String pluginId, URL[] urls, ClassLoader parent) {
super(urls, parent);
this.pluginId = pluginId;
this.sharedPackages = Set.of(
"com.flowable.devops.workflow.node",
"com.flowable.devops.workflow.model",
"org.springframework",
"org.flowable"
);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1. 共享包从父类加载器加载
if (isSharedPackage(name)) {
return super.loadClass(name, resolve);
}
// 2. 插件自己的类优先从自己加载
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
c = findClass(name);
} catch (ClassNotFoundException e) {
c = super.loadClass(name, resolve);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
四、插件开发规范
4.1 插件项目结构
my-custom-node-plugin/
├── pom.xml # Maven配置
├── plugin.json # 插件清单
├── src/
│ └── main/
│ ├── java/
│ │ └── com/example/
│ │ └── CustomNode.java # 节点实现
│ └── resources/
│ └── icon.svg # 节点图标(可选)
└── README.md # 插件文档
4.2 插件开发步骤
Step 1: 创建Maven项目
<!-- pom.xml -->
<project>
<parent>
<groupId>com.flowable</groupId>
<artifactId>flowable-plugin-parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>my-custom-node</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<dependencies>
<!-- 只需要依赖插件SDK -->
<dependency>
<groupId>com.flowable</groupId>
<artifactId>flowable-plugin-sdk</artifactId>
<version>1.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Step 2: 实现节点接口
package com.example;
import com.flowable.devops.workflow.node.*;
import com.flowable.devops.workflow.model.*;
@NodePlugin(
id = "my_custom_node",
name = "myCustomNode",
displayName = "My Custom Node",
category = NodeCategory.OTHER,
version = "1.0.0",
author = "Your Name",
description = "这是我的自定义节点"
)
public class CustomNode implements WorkflowNode {
@Override
public NodeType getMetadata() {
return NodeType.builder()
.id("my_custom_node")
.displayName("My Custom Node")
.fields(List.of(
FieldDefinition.builder()
.name("input")
.label("输入")
.type(FieldType.TEXT)
.required(true)
.build()
))
.outputSchema(Map.of(
"type", "object",
"properties", Map.of(
"result", Map.of("type", "string")
)
))
.build();
}
@Override
public NodeExecutionResult execute(NodeInput input, NodeExecutionContext context) {
LocalDateTime startTime = LocalDateTime.now();
try {
// 获取输入
String inputValue = input.getStringRequired("input");
// 执行逻辑
String result = processInput(inputValue);
// 构建输出
Map<String, Object> output = Map.of("result", result);
LocalDateTime endTime = LocalDateTime.now();
return NodeExecutionResult.success(output, startTime, endTime);
} catch (Exception e) {
LocalDateTime endTime = LocalDateTime.now();
return NodeExecutionResult.failed(e.getMessage(), e.getClass().getSimpleName(), startTime, endTime);
}
}
private String processInput(String input) {
// 你的业务逻辑
return "Processed: " + input;
}
}
Step 3: 创建插件清单
// plugin.json
{
"id": "my_custom_node",
"name": "myCustomNode",
"displayName": "My Custom Node",
"version": "1.0.0",
"author": "Your Name",
"description": "这是我的自定义节点",
"mainClass": "com.example.CustomNode",
"category": "OTHER",
"enabled": true,
"dependencies": [],
"minPlatformVersion": "1.0.0"
}
Step 4: 打包和部署
# 打包
mvn clean package
# 部署(复制到插件目录)
cp target/my-custom-node-1.0.0.jar /path/to/flowable-devops/backend/plugins/
# 重启应用(或触发热加载)
# 插件会自动被扫描和注册
4.3 最佳实践
✅ 推荐做法:
- 节点应该是无状态的
- 所有配置通过
NodeInput传递 - 使用
NodeExecutionResult.success/failed返回结果 - 充分的错误处理和日志记录
- 提供完整的元数据(字段定义、输出结构)
❌ 禁止做法:
- 不要在节点中保存状态
- 不要直接访问数据库(通过Service层)
- 不要使用阻塞式IO(使用异步API)
- 不要抛出未处理的异常
- 不要访问其他插件的私有API
五、插件管理API
5.1 REST API
@RestController
@RequestMapping("/api/plugins")
public class PluginController {
@Autowired
private PluginManager pluginManager;
/**
* 获取所有插件
*/
@GetMapping
public ResponseEntity<List<PluginDescriptor>> getAllPlugins() {
return ResponseEntity.ok(pluginManager.getAllPlugins());
}
/**
* 获取单个插件
*/
@GetMapping("/{pluginId}")
public ResponseEntity<PluginDescriptor> getPlugin(@PathVariable String pluginId) {
return ResponseEntity.ok(pluginManager.getPlugin(pluginId));
}
/**
* 启用插件
*/
@PostMapping("/{pluginId}/enable")
public ResponseEntity<Void> enablePlugin(@PathVariable String pluginId) {
pluginManager.enablePlugin(pluginId);
return ResponseEntity.ok().build();
}
/**
* 禁用插件
*/
@PostMapping("/{pluginId}/disable")
public ResponseEntity<Void> disablePlugin(@PathVariable String pluginId) {
pluginManager.disablePlugin(pluginId);
return ResponseEntity.ok().build();
}
/**
* 上传插件
*/
@PostMapping("/upload")
public ResponseEntity<PluginDescriptor> uploadPlugin(@RequestParam("file") MultipartFile file) {
PluginDescriptor descriptor = pluginManager.installPlugin(file);
return ResponseEntity.ok(descriptor);
}
/**
* 卸载插件
*/
@DeleteMapping("/{pluginId}")
public ResponseEntity<Void> uninstallPlugin(@PathVariable String pluginId) {
pluginManager.uninstallPlugin(pluginId);
return ResponseEntity.noContent().build();
}
/**
* 重新加载插件(热加载)
*/
@PostMapping("/{pluginId}/reload")
public ResponseEntity<Void> reloadPlugin(@PathVariable String pluginId) {
pluginManager.reloadPlugin(pluginId);
return ResponseEntity.ok().build();
}
}
六、数据库设计
6.1 插件信息表
CREATE TABLE plugin_registry (
id VARCHAR(64) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
display_name VARCHAR(255) NOT NULL,
version VARCHAR(20) NOT NULL,
author VARCHAR(100),
description TEXT,
category VARCHAR(50),
-- 插件来源
source VARCHAR(20) NOT NULL, -- INTERNAL, EXTERNAL
jar_path VARCHAR(500),
main_class VARCHAR(500) NOT NULL,
-- 状态
enabled BOOLEAN DEFAULT true,
status VARCHAR(20) DEFAULT 'LOADED', -- LOADED, ACTIVE, DISABLED, ERROR
error_message TEXT,
-- 依赖
dependencies JSON, -- ["plugin1:1.0.0", "plugin2:2.0.0"]
min_platform_version VARCHAR(20),
-- 时间
installed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
loaded_at TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 索引
INDEX idx_status (status),
INDEX idx_source (source),
INDEX idx_enabled (enabled)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
七、实施计划
阶段1:基础插件化(Week 1-2)
目标:实现注解驱动的插件开发,支持内置插件自动注册
任务:
- 设计插件接口和注解
- 实现 PluginManager(内置插件扫描)
- 改造现有节点为插件形式
- 单元测试和集成测试
验收标准:
- 新增节点只需添加 @NodePlugin 注解
- 应用启动时自动扫描并注册所有插件
- 前端可以看到所有插件节点
阶段2:外部插件支持(Week 3-4)
目标:支持从外部JAR加载插件,实现热加载
任务:
- 实现 PluginLoader(JAR加载)
- 实现 PluginClassLoader(类隔离,可选)
- 插件生命周期管理
- 插件管理API
验收标准:
- 可以上传JAR文件部署插件
- 插件可以启用/禁用/卸载
- 插件间相互隔离,互不影响
阶段3:插件生态(Week 5-6)
目标:完善插件开发工具和文档
任务:
- 创建插件SDK(parent POM)
- 插件开发脚手架工具
- 插件开发文档和示例
- 插件打包和发布工具
验收标准:
- 第三方开发者可以轻松开发插件
- 插件开发文档完整
- 至少5个示例插件
八、风险和应对
8.1 技术风险
| 风险 | 影响 | 应对方案 |
|---|---|---|
| ClassLoader隔离复杂 | 高 | 第一期不做隔离,第二期逐步引入 |
| 热加载可能导致内存泄漏 | 中 | 限制热加载频率,提供监控 |
| 插件依赖冲突 | 中 | 使用独立ClassLoader隔离 |
| 第三方插件安全风险 | 高 | 代码审核 + 沙箱执行(长期) |
8.2 业务风险
| 风险 | 影响 | 应对方案 |
|---|---|---|
| 现有节点改造成本 | 中 | 提供自动化迁移工具 |
| 插件质量参差不齐 | 中 | 插件审核机制 + 评分系统 |
| 用户学习成本 | 低 | 完善文档 + 示例插件 |
九、后续演进
9.1 第二期功能
- 插件市场(发布、浏览、下载)
- 插件评分和评论系统
- 插件依赖自动下载
- 插件版本管理
9.2 第三期功能
- 插件沙箱执行(安全隔离)
- 插件收费机制
- 插件开发者认证
- 插件CI/CD集成
十、参考资料
- Spring Plugin Framework: https://github.com/spring-projects/spring-plugin
- PF4J (Plugin Framework for Java): https://github.com/pf4j/pf4j
- Jenkins Plugin Architecture: https://www.jenkins.io/doc/developer/plugin-development/
下一步:查看具体实现代码和示例插件。