# 插件化架构设计文档

**版本**: 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 注解

```java
@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（插件描述符）

```java
@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（插件管理器）

职责：
- 扫描并加载所有插件
- 管理插件生命周期（加载/启用/禁用/卸载）
- 插件依赖解析
- 错误处理和日志记录

```java
@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（第二期）
- 验证插件合法性

```java
@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（独立类加载器，第二期）

```java
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项目**

```xml
<!-- 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: 实现节点接口**

```java
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: 创建插件清单**

```json
// 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: 打包和部署**

```bash
# 打包
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

```java
@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 插件信息表

```sql
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）

**目标**：实现注解驱动的插件开发，支持内置插件自动注册

**任务**：
- [x] 设计插件接口和注解
- [ ] 实现 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/

---

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