flowable-devops/backend/docs/06-插件化架构设计.md
dengqichen ab88ff72f2 提交
2025-10-13 23:01:56 +08:00

19 KiB
Raw Permalink Blame History

插件化架构设计文档

版本: 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 最佳实践

推荐做法

  1. 节点应该是无状态
  2. 所有配置通过 NodeInput 传递
  3. 使用 NodeExecutionResult.success/failed 返回结果
  4. 充分的错误处理和日志记录
  5. 提供完整的元数据(字段定义、输出结构)

禁止做法

  1. 不要在节点中保存状态
  2. 不要直接访问数据库通过Service层
  3. 不要使用阻塞式IO使用异步API
  4. 不要抛出未处理的异常
  5. 不要访问其他插件的私有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加载插件实现热加载

任务

  • 实现 PluginLoaderJAR加载
  • 实现 PluginClassLoader类隔离可选
  • 插件生命周期管理
  • 插件管理API

验收标准

  • 可以上传JAR文件部署插件
  • 插件可以启用/禁用/卸载
  • 插件间相互隔离,互不影响

阶段3插件生态Week 5-6

目标:完善插件开发工具和文档

任务

  • 创建插件SDKparent POM
  • 插件开发脚手架工具
  • 插件开发文档和示例
  • 插件打包和发布工具

验收标准

  • 第三方开发者可以轻松开发插件
  • 插件开发文档完整
  • 至少5个示例插件

八、风险和应对

8.1 技术风险

风险 影响 应对方案
ClassLoader隔离复杂 第一期不做隔离,第二期逐步引入
热加载可能导致内存泄漏 限制热加载频率,提供监控
插件依赖冲突 使用独立ClassLoader隔离
第三方插件安全风险 代码审核 + 沙箱执行(长期)

8.2 业务风险

风险 影响 应对方案
现有节点改造成本 提供自动化迁移工具
插件质量参差不齐 插件审核机制 + 评分系统
用户学习成本 完善文档 + 示例插件

九、后续演进

9.1 第二期功能

  • 插件市场(发布、浏览、下载)
  • 插件评分和评论系统
  • 插件依赖自动下载
  • 插件版本管理

9.2 第三期功能

  • 插件沙箱执行(安全隔离)
  • 插件收费机制
  • 插件开发者认证
  • 插件CI/CD集成

十、参考资料


下一步:查看具体实现代码和示例插件。