From b964fef4dd6b57440f5ec0dc13201bd399c1b6ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=B6=E8=AE=AD?= Date: Tue, 9 Jun 2026 09:41:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=92=E4=BB=B6=E7=89=88=E6=9C=AC=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workutils/WorkUtilsApplication.java | 30 +-- .../workutils/biz/batch/BatchTool.java | 23 +- .../example/workutils/biz/git/GitTool.java | 23 +- .../biz/mybatisexport/MyBatisExportTool.java | 23 +- .../MyBatisGeneratorTool.java | 23 +- .../workutils/biz/scanner/ScannerTool.java | 23 +- .../workutils/system/plugin/Plugin.java | 41 +++ .../system/plugin/PluginContext.java | 42 +++ .../system/plugin/PluginDescriptor.java | 68 +++++ .../system/plugin/PluginManager.java | 255 ++++++++++++++++++ .../workutils/system/plugin/PluginState.java | 22 ++ .../workutils/system/tool/ToolRegistry.java | 142 ---------- 项目目录结构说明.md | 222 ++++++++++----- 13 files changed, 661 insertions(+), 276 deletions(-) create mode 100644 src/main/java/com/example/workutils/system/plugin/Plugin.java create mode 100644 src/main/java/com/example/workutils/system/plugin/PluginContext.java create mode 100644 src/main/java/com/example/workutils/system/plugin/PluginDescriptor.java create mode 100644 src/main/java/com/example/workutils/system/plugin/PluginManager.java create mode 100644 src/main/java/com/example/workutils/system/plugin/PluginState.java delete mode 100644 src/main/java/com/example/workutils/system/tool/ToolRegistry.java diff --git a/src/main/java/com/example/workutils/WorkUtilsApplication.java b/src/main/java/com/example/workutils/WorkUtilsApplication.java index 8e85e68..c057943 100644 --- a/src/main/java/com/example/workutils/WorkUtilsApplication.java +++ b/src/main/java/com/example/workutils/WorkUtilsApplication.java @@ -1,7 +1,7 @@ package com.example.workutils; -import com.example.workutils.system.tool.Tool; -import com.example.workutils.system.tool.ToolRegistry; +import com.example.workutils.system.plugin.Plugin; +import com.example.workutils.system.plugin.PluginManager; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; @@ -16,10 +16,10 @@ import java.util.Scanner; @Slf4j public class WorkUtilsApplication implements CommandLineRunner { - private final ToolRegistry toolRegistry; + private final PluginManager pluginManager; - public WorkUtilsApplication(ToolRegistry toolRegistry) { - this.toolRegistry = toolRegistry; + public WorkUtilsApplication(PluginManager pluginManager) { + this.pluginManager = pluginManager; } public static void main(String[] args) { @@ -37,25 +37,25 @@ public class WorkUtilsApplication implements CommandLineRunner { private void showInteractiveMenu() { Scanner scanner = new Scanner(System.in); - toolRegistry.runMainMenu(scanner); + pluginManager.runMainMenu(scanner); } /** - * 命令行模式:通过ToolRegistry按命令名分发到对应Tool - * 启动类不依赖任何具体业务Service + * 命令行模式:通过 PluginManager 按命令名分发到对应 Plugin + * 启动类不依赖任何具体业务 Service */ private void processCommand(String... args) throws Exception { String command = args[0].toLowerCase(); - Optional toolOpt = toolRegistry.getToolByCommand(command); - if (toolOpt.isPresent()) { - Tool tool = toolOpt.get(); + Optional pluginOpt = pluginManager.getPluginByCommand(command); + if (pluginOpt.isPresent()) { + Plugin plugin = pluginOpt.get(); try { - // args[0]是命令名,从args[1]开始传给tool - String[] toolArgs = args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]; - tool.executeCommand(toolArgs); + // args[0]是命令名,从args[1]开始传给plugin + String[] pluginArgs = args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]; + plugin.executeCommand(pluginArgs); } catch (Exception e) { - log.error("命令执行失败: {} [{}]", tool.getName(), tool.getId(), e); + log.error("命令执行失败: {} [{}]", plugin.getName(), plugin.getId(), e); System.err.println("命令执行失败: " + e.getMessage()); } } else { diff --git a/src/main/java/com/example/workutils/biz/batch/BatchTool.java b/src/main/java/com/example/workutils/biz/batch/BatchTool.java index 1599566..2270145 100644 --- a/src/main/java/com/example/workutils/biz/batch/BatchTool.java +++ b/src/main/java/com/example/workutils/biz/batch/BatchTool.java @@ -2,7 +2,8 @@ package com.example.workutils.biz.batch; import com.example.workutils.biz.mybatisexport.service.MyBatisExportService; import com.example.workutils.biz.scanner.service.ScannerService; -import com.example.workutils.system.tool.Tool; +import com.example.workutils.system.plugin.Plugin; +import com.example.workutils.system.plugin.PluginDescriptor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -12,19 +13,26 @@ import java.util.List; import java.util.Scanner; /** - * 批量处理工具 + * 批量处理插件(实现 Plugin 接口) * 仅负责菜单展示和用户交互,业务逻辑委托给各Service */ @Slf4j @Component @RequiredArgsConstructor -public class BatchTool implements Tool { +public class BatchTool implements Plugin { @FunctionalInterface public interface ExceptionRunnable { void run() throws Exception; } + private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder() + .id("batch") + .name("批量处理") + .version("1.0.0") + .description("一键执行多种检查任务") + .build(); + private static final String[] MENU_OPTIONS = { "1. 执行所有检查", "2. 自定义组合检查", @@ -42,13 +50,8 @@ public class BatchTool implements Tool { private final MyBatisExportService myBatisExportService; @Override - public String getId() { - return "batch"; - } - - @Override - public String getName() { - return "批量处理"; + public PluginDescriptor getDescriptor() { + return DESCRIPTOR; } @Override diff --git a/src/main/java/com/example/workutils/biz/git/GitTool.java b/src/main/java/com/example/workutils/biz/git/GitTool.java index a799be0..4032721 100644 --- a/src/main/java/com/example/workutils/biz/git/GitTool.java +++ b/src/main/java/com/example/workutils/biz/git/GitTool.java @@ -2,7 +2,8 @@ package com.example.workutils.biz.git; import com.example.workutils.biz.git.service.GitService; import com.example.workutils.biz.git.service.internal.GitCommandExecutor; -import com.example.workutils.system.tool.Tool; +import com.example.workutils.system.plugin.Plugin; +import com.example.workutils.system.plugin.PluginDescriptor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -13,12 +14,19 @@ import java.util.Scanner; import java.util.stream.Collectors; /** - * Git仓库健康检测工具入口(符合 Tool 规范) + * Git仓库健康检测插件(实现 Plugin 接口) */ @Slf4j @Component @RequiredArgsConstructor -public class GitTool implements Tool { +public class GitTool implements Plugin { + + private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder() + .id("git-health") + .name("Git仓库健康检测") + .version("1.0.0") + .description("检测和修复Git仓库健康状态") + .build(); private static final String[] MENU_OPTIONS = { "1. 检测所有仓库健康状态", @@ -29,13 +37,8 @@ public class GitTool implements Tool { private final GitService gitService; @Override - public String getId() { - return "git-health"; - } - - @Override - public String getName() { - return "Git仓库健康检测"; + public PluginDescriptor getDescriptor() { + return DESCRIPTOR; } @Override diff --git a/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportTool.java b/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportTool.java index aed4ff4..c8ffef2 100644 --- a/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportTool.java +++ b/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportTool.java @@ -1,7 +1,8 @@ package com.example.workutils.biz.mybatisexport; import com.example.workutils.biz.mybatisexport.service.MyBatisExportService; -import com.example.workutils.system.tool.Tool; +import com.example.workutils.system.plugin.Plugin; +import com.example.workutils.system.plugin.PluginDescriptor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -10,12 +11,19 @@ import java.util.Arrays; import java.util.Scanner; /** - * MyBatis SQL导出工具入口(符合 Tool 规范) + * MyBatis SQL导出插件(实现 Plugin 接口) */ @Slf4j @Component @RequiredArgsConstructor -public class MyBatisExportTool implements Tool { +public class MyBatisExportTool implements Plugin { + + private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder() + .id("mybatisexport") + .name("MyBatis SQL导出") + .version("1.0.0") + .description("从MyBatis XML中提取SQL并导出为CSV") + .build(); private static final String[] MENU_OPTIONS = { "1. 导出所有SQL", @@ -29,13 +37,8 @@ public class MyBatisExportTool implements Tool { private final MyBatisExportService myBatisExportService; @Override - public String getId() { - return "mybatisexport"; - } - - @Override - public String getName() { - return "MyBatis SQL导出"; + public PluginDescriptor getDescriptor() { + return DESCRIPTOR; } @Override diff --git a/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorTool.java b/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorTool.java index 78518e7..40e30d9 100644 --- a/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorTool.java +++ b/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorTool.java @@ -1,7 +1,8 @@ package com.example.workutils.biz.mybatisgenerator; import com.example.workutils.biz.mybatisgenerator.service.generator.MyBatisGeneratorService; -import com.example.workutils.system.tool.Tool; +import com.example.workutils.system.plugin.Plugin; +import com.example.workutils.system.plugin.PluginDescriptor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -11,12 +12,19 @@ import java.util.List; import java.util.Scanner; /** - * MyBatis逆向工程工具入口(符合 Tool 规范) + * MyBatis逆向工程插件(实现 Plugin 接口) */ @Slf4j @Component @RequiredArgsConstructor -public class MyBatisGeneratorTool implements Tool { +public class MyBatisGeneratorTool implements Plugin { + + private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder() + .id("mybatisgenerator") + .name("MyBatis逆向工程") + .version("1.0.0") + .description("根据数据库表结构生成PO/Mapper/XML") + .build(); private static final String[] MENU_OPTIONS = { "1. 根据表名生成(单表)", @@ -30,13 +38,8 @@ public class MyBatisGeneratorTool implements Tool { private final MyBatisGeneratorService generatorService; @Override - public String getId() { - return "mybatisgenerator"; - } - - @Override - public String getName() { - return "MyBatis逆向工程"; + public PluginDescriptor getDescriptor() { + return DESCRIPTOR; } @Override diff --git a/src/main/java/com/example/workutils/biz/scanner/ScannerTool.java b/src/main/java/com/example/workutils/biz/scanner/ScannerTool.java index 4588b24..7372625 100644 --- a/src/main/java/com/example/workutils/biz/scanner/ScannerTool.java +++ b/src/main/java/com/example/workutils/biz/scanner/ScannerTool.java @@ -1,7 +1,8 @@ package com.example.workutils.biz.scanner; import com.example.workutils.biz.scanner.service.ScannerService; -import com.example.workutils.system.tool.Tool; +import com.example.workutils.system.plugin.Plugin; +import com.example.workutils.system.plugin.PluginDescriptor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -9,12 +10,19 @@ import org.springframework.stereotype.Component; import java.util.Scanner; /** - * 扫描工具入口(符合 Tool 规范) + * 扫描工具插件(实现 Plugin 接口) */ @Slf4j @Component @RequiredArgsConstructor -public class ScannerTool implements Tool { +public class ScannerTool implements Plugin { + + private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder() + .id("scan") + .name("扫描功能") + .version("1.0.0") + .description("关键词/Redis/Dubbo扫描工具") + .build(); private static final String[] MENU_OPTIONS = { "1. 关键词扫描", @@ -24,13 +32,8 @@ public class ScannerTool implements Tool { private final ScannerService scannerService; @Override - public String getId() { - return "scan"; - } - - @Override - public String getName() { - return "扫描功能"; + public PluginDescriptor getDescriptor() { + return DESCRIPTOR; } @Override diff --git a/src/main/java/com/example/workutils/system/plugin/Plugin.java b/src/main/java/com/example/workutils/system/plugin/Plugin.java new file mode 100644 index 0000000..17e2f4b --- /dev/null +++ b/src/main/java/com/example/workutils/system/plugin/Plugin.java @@ -0,0 +1,41 @@ +package com.example.workutils.system.plugin; + +import com.example.workutils.system.tool.Tool; + +/** + * 插件接口 —— 所有 biz 模块的顶级契约 + *

+ * 继承 {@link Tool},在其基础上增加插件生命周期管理: + *

    + *
  • {@link #getDescriptor()} — 提供插件元数据(id/name/version/description/dependencies)
  • + *
  • {@link #onLoad(PluginContext)} — 插件加载时回调,注入运行时上下文
  • + *
  • {@link #onUnload()} — 插件卸载时回调,清理资源
  • + *
+ *

+ * getId()/getName() 默认委托给 PluginDescriptor,实现类只需提供 getDescriptor()。 + */ +public interface Plugin extends Tool { + + /** 插件描述符(元数据) */ + PluginDescriptor getDescriptor(); + + /** 插件加载回调,PluginManager 在注册时调用 */ + default void onLoad(PluginContext context) { + } + + /** 插件卸载回调,清理资源 */ + default void onUnload() { + } + + // ==================== 默认委托 ==================== + + @Override + default String getId() { + return getDescriptor().getId(); + } + + @Override + default String getName() { + return getDescriptor().getName(); + } +} diff --git a/src/main/java/com/example/workutils/system/plugin/PluginContext.java b/src/main/java/com/example/workutils/system/plugin/PluginContext.java new file mode 100644 index 0000000..9abd358 --- /dev/null +++ b/src/main/java/com/example/workutils/system/plugin/PluginContext.java @@ -0,0 +1,42 @@ +package com.example.workutils.system.plugin; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 插件运行时上下文 + *

+ * 通过 {@link Plugin#onLoad(PluginContext)} 注入给每个插件, + * 提供访问 PluginManager 和跨插件共享属性的能力。 + */ +public class PluginContext { + + private final PluginManager pluginManager; + private final Map attributes = new ConcurrentHashMap<>(); + + public PluginContext(PluginManager pluginManager) { + this.pluginManager = pluginManager; + } + + /** 获取 PluginManager,用于插件间通信 */ + public PluginManager getPluginManager() { + return pluginManager; + } + + /** 设置共享属性 */ + public void setAttribute(String key, Object value) { + attributes.put(key, value); + } + + /** 获取共享属性 */ + @SuppressWarnings("unchecked") + public T getAttribute(String key) { + return (T) attributes.get(key); + } + + /** 获取共享属性,带默认值 */ + @SuppressWarnings("unchecked") + public T getAttribute(String key, T defaultValue) { + return (T) attributes.getOrDefault(key, defaultValue); + } +} diff --git a/src/main/java/com/example/workutils/system/plugin/PluginDescriptor.java b/src/main/java/com/example/workutils/system/plugin/PluginDescriptor.java new file mode 100644 index 0000000..b27c7d6 --- /dev/null +++ b/src/main/java/com/example/workutils/system/plugin/PluginDescriptor.java @@ -0,0 +1,68 @@ +package com.example.workutils.system.plugin; + +import java.util.Collections; +import java.util.List; + +/** + * 插件描述符(不可变元数据) + *

+ * 每个 Plugin 通过 {@link Plugin#getDescriptor()} 返回此描述符, + * PluginManager 据此识别、管理、展示插件。 + */ +public class PluginDescriptor { + + private final String id; + private final String name; + private final String version; + private final String description; + private final List dependencies; + + private PluginDescriptor(Builder builder) { + this.id = builder.id; + this.name = builder.name; + this.version = builder.version; + this.description = builder.description; + this.dependencies = Collections.unmodifiableList(builder.dependencies); + } + + public static Builder builder() { + return new Builder(); + } + + // ==================== Getters ==================== + + public String getId() { return id; } + public String getName() { return name; } + public String getVersion() { return version; } + public String getDescription() { return description; } + + /** 依赖的插件 ID 列表(暂未强制校验,预留设计) */ + public List getDependencies() { return dependencies; } + + @Override + public String toString() { + return name + " [v" + version + "]"; + } + + // ==================== Builder ==================== + + public static class Builder { + private String id; + private String name; + private String version = "1.0.0"; + private String description = ""; + private List dependencies = Collections.emptyList(); + + public Builder id(String id) { this.id = id; return this; } + public Builder name(String name) { this.name = name; return this; } + public Builder version(String version) { this.version = version;return this; } + public Builder description(String desc) { this.description = desc; return this; } + public Builder dependencies(List d) { this.dependencies = d; return this; } + + public PluginDescriptor build() { + if (id == null || id.isEmpty()) throw new IllegalArgumentException("plugin id is required"); + if (name == null || name.isEmpty()) throw new IllegalArgumentException("plugin name is required"); + return new PluginDescriptor(this); + } + } +} diff --git a/src/main/java/com/example/workutils/system/plugin/PluginManager.java b/src/main/java/com/example/workutils/system/plugin/PluginManager.java new file mode 100644 index 0000000..1cc0d45 --- /dev/null +++ b/src/main/java/com/example/workutils/system/plugin/PluginManager.java @@ -0,0 +1,255 @@ +package com.example.workutils.system.plugin; + +import com.example.workutils.system.config.WorkUtilsProperties; +import com.example.workutils.system.support.ConsoleHelper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.*; + +/** + * 插件管理器 —— 插件生命周期管理 + 注册中心 + 主菜单 + *

+ * 替换旧版 {@code ToolRegistry},统一管理所有 {@link Plugin} 的: + *

    + *
  • 生命周期 — load / unload / enable / disable
  • + *
  • 注册发现 — 按 id / command 查找
  • + *
  • 启用过滤 — 白名单 / 黑名单
  • + *
  • 主菜单 — 交互式菜单循环
  • + *
  • 命令行分发 — 按 command 名路由
  • + *
+ */ +@Slf4j +@Component +public class PluginManager { + + private static final int EXIT_CODE = 0; + + private final List allPlugins; + private final WorkUtilsProperties properties; + private PluginContext pluginContext; + + private List enabledPlugins; + private Map pluginByIdMap; + private Map pluginByCommandMap; + private Map pluginStates; + + public PluginManager(List plugins, WorkUtilsProperties properties) { + this.allPlugins = plugins; + this.properties = properties; + } + + @PostConstruct + public void init() { + this.pluginContext = new PluginContext(this); + this.enabledPlugins = new ArrayList<>(); + this.pluginByIdMap = new LinkedHashMap<>(); + this.pluginByCommandMap = new LinkedHashMap<>(); + this.pluginStates = new LinkedHashMap<>(); + + Set enabledIds = resolveEnabledIds(); + + for (Plugin plugin : allPlugins) { + try { + // 生命周期:加载 + plugin.onLoad(pluginContext); + PluginDescriptor desc = plugin.getDescriptor(); + pluginStates.put(desc.getId(), PluginState.LOADED); + + if (enabledIds == null || enabledIds.contains(desc.getId())) { + enabledPlugins.add(plugin); + pluginByIdMap.put(desc.getId(), plugin); + pluginStates.put(desc.getId(), PluginState.ACTIVE); + + String cmd = plugin.getCommandName(); + if (cmd != null && !cmd.isEmpty()) { + pluginByCommandMap.put(cmd, plugin); + } + log.info("加载插件: {}", desc); + } else { + pluginStates.put(desc.getId(), PluginState.DISABLED); + log.info("跳过已禁用插件: {}", desc); + } + } catch (Exception e) { + String pluginId = safeGetId(plugin); + pluginStates.put(pluginId, PluginState.ERROR); + log.error("插件加载失败: " + pluginId, e); + } + } + + log.info("插件加载完成: {}/{} 已激活", enabledPlugins.size(), allPlugins.size()); + } + + // ==================== 主菜单 ==================== + + /** 启动主菜单交互循环 */ + public void runMainMenu(Scanner scanner) { + log.info("启动主菜单"); + while (true) { + ConsoleHelper.printMenuTitle("主菜单"); + displayMainMenu(); + + int choice = ConsoleHelper.readChoice(scanner); + scanner.nextLine(); + + if (choice == EXIT_CODE) { + log.info("用户退出系统"); + System.out.println("感谢使用,再见!"); + shutdown(); + break; + } + + dispatchPlugin(choice, scanner); + } + } + + private void displayMainMenu() { + System.out.println("请选择操作:"); + for (int i = 0; i < enabledPlugins.size(); i++) { + PluginDescriptor desc = enabledPlugins.get(i).getDescriptor(); + System.out.println((i + 1) + ". " + desc.getName() + " [v" + desc.getVersion() + "]"); + } + System.out.println(EXIT_CODE + ". 退出系统"); + } + + private void dispatchPlugin(int choice, Scanner scanner) { + int index = choice - 1; + if (index >= 0 && index < enabledPlugins.size()) { + Plugin plugin = enabledPlugins.get(index); + PluginDescriptor desc = plugin.getDescriptor(); + try { + plugin.execute(scanner); + } catch (Exception e) { + log.error("插件执行失败: " + desc, e); + System.out.println("插件执行失败: " + e.getMessage()); + } + } else { + ConsoleHelper.printInvalidOption(); + } + } + + // ==================== 查找方法 ==================== + + /** 获取所有已启用插件(只读) */ + public List getEnabledPlugins() { + return Collections.unmodifiableList(enabledPlugins); + } + + /** 按 ID 查找插件 */ + public Optional getPluginById(String id) { + return Optional.ofNullable(pluginByIdMap.get(id)); + } + + /** 按命令行命令名查找插件 */ + public Optional getPluginByCommand(String command) { + return Optional.ofNullable(pluginByCommandMap.get(command)); + } + + /** 获取插件状态 */ + public PluginState getPluginState(String pluginId) { + return pluginStates.getOrDefault(pluginId, PluginState.UNLOADED); + } + + /** 列出所有插件(含已禁用) */ + public List listAllPluginDescriptors() { + List result = new ArrayList<>(); + for (Plugin plugin : allPlugins) { + result.add(plugin.getDescriptor()); + } + return result; + } + + // ==================== 生命周期管理(预留) ==================== + + /** 手动启用插件 */ + public boolean enablePlugin(String pluginId) { + Plugin plugin = pluginByIdMap.get(pluginId); + if (plugin == null) { + Plugin p = findPluginById(pluginId); + if (p == null) { + log.warn("未找到插件: {}", pluginId); + return false; + } + enabledPlugins.add(p); + pluginByIdMap.put(pluginId, p); + String cmd = p.getCommandName(); + if (cmd != null && !cmd.isEmpty()) { + pluginByCommandMap.put(cmd, p); + } + pluginStates.put(pluginId, PluginState.ACTIVE); + log.info("启用插件: {}", p.getDescriptor()); + } else { + pluginStates.put(pluginId, PluginState.ACTIVE); + } + return true; + } + + /** 手动禁用插件 */ + public boolean disablePlugin(String pluginId) { + Plugin plugin = pluginByIdMap.remove(pluginId); + if (plugin != null) { + enabledPlugins.remove(plugin); + String cmd = plugin.getCommandName(); + if (cmd != null && !cmd.isEmpty()) { + pluginByCommandMap.remove(cmd, plugin); + } + pluginStates.put(pluginId, PluginState.DISABLED); + log.info("禁用插件: {}", plugin.getDescriptor()); + } + return plugin != null; + } + + // ==================== 内部方法 ==================== + + private void shutdown() { + for (Plugin plugin : allPlugins) { + try { + plugin.onUnload(); + } catch (Exception e) { + log.warn("插件卸载异常: " + plugin.getDescriptor(), e); + } + } + } + + private Plugin findPluginById(String id) { + for (Plugin p : allPlugins) { + if (p.getId().equals(id)) { + return p; + } + } + return null; + } + + private String safeGetId(Plugin plugin) { + try { + return plugin.getId(); + } catch (Exception e) { + return "unknown"; + } + } + + // ==================== 启用/禁用解析 ==================== + + private Set resolveEnabledIds() { + WorkUtilsProperties.ToolsConfig toolsConfig = properties.getTools(); + if (toolsConfig == null) { + return null; + } + + if (toolsConfig.getEnabled() != null && !toolsConfig.getEnabled().isEmpty()) { + return new HashSet<>(toolsConfig.getEnabled()); + } + + if (toolsConfig.getDisabled() != null && !toolsConfig.getDisabled().isEmpty()) { + Set allIds = new HashSet<>(); + for (Plugin plugin : allPlugins) { + allIds.add(plugin.getId()); + } + allIds.removeAll(toolsConfig.getDisabled()); + return allIds; + } + + return null; + } +} diff --git a/src/main/java/com/example/workutils/system/plugin/PluginState.java b/src/main/java/com/example/workutils/system/plugin/PluginState.java new file mode 100644 index 0000000..f57425a --- /dev/null +++ b/src/main/java/com/example/workutils/system/plugin/PluginState.java @@ -0,0 +1,22 @@ +package com.example.workutils.system.plugin; + +/** + * 插件生命周期状态 + */ +public enum PluginState { + + /** 未加载 */ + UNLOADED, + + /** 已加载(onLoad 完成) */ + LOADED, + + /** 已激活(已启用,可交互) */ + ACTIVE, + + /** 已禁用 */ + DISABLED, + + /** 加载/执行异常 */ + ERROR +} diff --git a/src/main/java/com/example/workutils/system/tool/ToolRegistry.java b/src/main/java/com/example/workutils/system/tool/ToolRegistry.java deleted file mode 100644 index 0ff74fc..0000000 --- a/src/main/java/com/example/workutils/system/tool/ToolRegistry.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.example.workutils.system.tool; - -import com.example.workutils.system.support.ConsoleHelper; -import com.example.workutils.system.config.WorkUtilsProperties; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import javax.annotation.PostConstruct; -import java.util.*; -import java.util.stream.Collectors; - -/** - * 工具注册中心 - * 自动发现 Spring 容器中的 Tool Bean,按配置过滤启用/禁用 - * 同时负责主菜单循环 - */ -@Slf4j -@Component -public class ToolRegistry { - - private static final int EXIT_CODE = 0; - - private final List allTools; - private final WorkUtilsProperties properties; - private List enabledTools; - private Map toolByIdMap; - private Map toolByCommandMap; - - public ToolRegistry(List tools, WorkUtilsProperties properties) { - this.allTools = tools; - this.properties = properties; - } - - @PostConstruct - public void init() { - Set enabledIds = resolveEnabledIds(); - this.enabledTools = new ArrayList<>(); - this.toolByIdMap = new LinkedHashMap<>(); - this.toolByCommandMap = new LinkedHashMap<>(); - - for (Tool tool : allTools) { - try { - if (enabledIds == null || enabledIds.contains(tool.getId())) { - enabledTools.add(tool); - toolByIdMap.put(tool.getId(), tool); - String cmd = tool.getCommandName(); - if (cmd != null && !cmd.isEmpty()) { - toolByCommandMap.put(cmd, tool); - } - log.info("注册工具: {} [{}]", tool.getName(), tool.getId()); - } else { - log.info("跳过已禁用工具: {} [{}]", tool.getName(), tool.getId()); - } - } catch (Exception e) { - log.error("工具初始化失败: {} [{}]", tool.getName(), tool.getId(), e); - } - } - - log.info("工具注册完成: {}/{} 已启用", enabledTools.size(), allTools.size()); - } - - // ==================== 主菜单 ==================== - - public void runMainMenu(Scanner scanner) { - log.info("启动主菜单"); - while (true) { - ConsoleHelper.printMenuTitle("主菜单"); - displayMainMenu(); - - int choice = ConsoleHelper.readChoice(scanner); - scanner.nextLine(); - - if (choice == EXIT_CODE) { - log.info("用户退出系统"); - System.out.println("感谢使用,再见!"); - break; - } - - dispatchTool(choice, scanner); - } - } - - private void displayMainMenu() { - System.out.println("请选择操作:"); - for (int i = 0; i < enabledTools.size(); i++) { - System.out.println((i + 1) + ". " + enabledTools.get(i).getName()); - } - System.out.println(EXIT_CODE + ". 退出系统"); - } - - private void dispatchTool(int choice, Scanner scanner) { - int index = choice - 1; - if (index >= 0 && index < enabledTools.size()) { - Tool tool = enabledTools.get(index); - try { - tool.execute(scanner); - } catch (Exception e) { - log.error("工具执行失败: {} [{}]", tool.getName(), tool.getId(), e); - System.out.println("工具执行失败: " + e.getMessage()); - } - } else { - ConsoleHelper.printInvalidOption(); - } - } - - // ==================== 查找方法 ==================== - - public List getEnabledTools() { - return Collections.unmodifiableList(enabledTools); - } - - public Optional getToolById(String id) { - return Optional.ofNullable(toolByIdMap.get(id)); - } - - public Optional getToolByCommand(String command) { - return Optional.ofNullable(toolByCommandMap.get(command)); - } - - // ==================== 启用/禁用解析 ==================== - - private Set resolveEnabledIds() { - WorkUtilsProperties.ToolsConfig toolsConfig = properties.getTools(); - if (toolsConfig == null) { - return null; - } - - if (toolsConfig.getEnabled() != null && !toolsConfig.getEnabled().isEmpty()) { - return new HashSet<>(toolsConfig.getEnabled()); - } - - if (toolsConfig.getDisabled() != null && !toolsConfig.getDisabled().isEmpty()) { - Set allIds = allTools.stream() - .map(Tool::getId) - .collect(Collectors.toSet()); - allIds.removeAll(toolsConfig.getDisabled()); - return allIds; - } - - return null; - } -} diff --git a/项目目录结构说明.md b/项目目录结构说明.md index 0c4f92d..8f9b219 100644 --- a/项目目录结构说明.md +++ b/项目目录结构说明.md @@ -45,14 +45,19 @@ src/main/java/com/example/workutils/ │ │ ├── ConsoleHelper.java # 控制台交互工具类 │ │ ├── FileOperationService.java# 文件操作服务(读取/扫描/写入/路径管理) │ │ └── OutputService.java # 输出服务(CSV/Excel) -│ └── tool/ # Tool 契约与注册中心 -│ ├── Tool.java # 工具契约接口(所有 biz 模块必须实现) -│ └── ToolRegistry.java # 工具注册中心(主菜单循环 + 按命令分发) -└── biz/ # 业务工具层(各模块是独立 Tool) - ├── batch/ # 批量处理工具 - │ └── BatchTool.java # 批量检查入口(实现 Tool) - ├── git/ # Git 操作工具 - │ ├── GitTool.java # Git 工具入口(实现 Tool) +│ ├── tool/ # Tool 基础契约 +│ │ └── Tool.java # 工具基础接口(Plugin 继承此接口) +│ └── plugin/ # 插件化框架 +│ ├── Plugin.java # 插件接口(扩展 Tool,增加生命周期) +│ ├── PluginDescriptor.java # 插件描述符(id/name/version/description/dependencies) +│ ├── PluginContext.java # 插件运行时上下文 +│ ├── PluginManager.java # 插件管理器(生命周期 + 注册 + 主菜单 + 命令分发) +│ └── PluginState.java # 插件状态枚举(UNLOADED/LOADED/ACTIVE/DISABLED/ERROR) +└── biz/ # 业务插件层(各模块是独立 Plugin) + ├── batch/ # 批量处理插件 + │ └── BatchTool.java # 批量检查入口(实现 Plugin) + ├── git/ # Git 操作插件 + │ ├── GitTool.java # Git 插件入口(实现 Plugin) │ └── service/ # 服务实现层 │ ├── GitService.java # Git 服务接口 │ ├── GitServiceImpl.java # Git 服务实现 @@ -70,21 +75,21 @@ src/main/java/com/example/workutils/ │ ├── GitStrategyFactory.java # 策略工厂 │ ├── CheckBranchChangesStrategy.java │ └── CheckoutAndPullStrategy.java - ├── mybatisexport/ # MyBatis SQL导出工具(独立插件) - │ ├── MyBatisExportTool.java # SQL 导出入口(实现 Tool) + ├── mybatisexport/ # MyBatis SQL导出插件 + │ ├── MyBatisExportTool.java # SQL 导出入口(实现 Plugin) │ └── service/ │ ├── MyBatisExportService.java # SQL 导出服务 │ └── model/ │ └── MybatisTag.java # MyBatis 标签数据对象 - ├── mybatisgenerator/ # MyBatis逆向工程工具(独立插件) - │ ├── MyBatisGeneratorTool.java # 逆向工程入口(实现 Tool) + ├── mybatisgenerator/ # MyBatis逆向工程插件 + │ ├── MyBatisGeneratorTool.java # 逆向工程入口(实现 Plugin) │ └── service/generator/ │ ├── MyBatisGeneratorService.java # 逆向工程服务 │ ├── CustomCommentGenerator.java # 自定义注释生成器 │ ├── LombokPlugin.java # Lombok 插件(@Data) │ └── SelectiveOnlyPlugin.java # 只保留 Selective 方法插件 - └── scanner/ # 扫描工具 - ├── ScannerTool.java # 扫描工具入口(实现 Tool) + └── scanner/ # 扫描插件 + ├── ScannerTool.java # 扫描工具入口(实现 Plugin) └── service/ # 服务实现层 ├── ScannerService.java # 扫描服务 └── config/ @@ -100,92 +105,96 @@ src/main/java/com/example/workutils/ #### `WorkUtilsApplication.java` - **作用**: Spring Boot 应用启动类,实现 `CommandLineRunner` - **功能**: - - 支持交互式菜单模式(无参数启动),委托 `ToolRegistry.runMainMenu()` - - 支持命令行模式,通过 `ToolRegistry.getToolByCommand()` 按命令名分发 + - 支持交互式菜单模式(无参数启动),委托 `PluginManager.runMainMenu()` + - 支持命令行模式,通过 `PluginManager.getPluginByCommand()` 按命令名分发 - 不依赖任何具体业务 Service,完全解耦 --- ### 3.2 系统框架层(`system/`) -#### `system/tool/Tool.java` -- **作用**: 工具契约接口,所有 biz 工具模块必须实现此接口 -- **核心方法**: `getId()`、`getName()`、`getMenuOptions()`、`execute()`(模板方法,含默认实现)、`getCommandName()`(命令行模式)、`executeCommand()`(命令行模式执行) +#### 3.2.1 插件化框架(`system/plugin/`) -#### `system/tool/ToolRegistry.java` -- **作用**: 工具注册中心,Spring 自动发现 `Tool` Bean,提供主菜单循环 -- **功能**: 根据配置过滤启用/禁用工具,支持白名单(enabled)和黑名单(disabled)模式,维护按命令名的索引,支持命令行模式按命令名分发 +| 文件 | 作用 | +|------|------| +| `Plugin.java` | **插件接口**,继承 `Tool`,增加生命周期方法 `onLoad(PluginContext)` / `onUnload()`,`getId()`/`getName()` 默认委托给 `PluginDescriptor` | +| `PluginDescriptor.java` | 插件描述符(不可变),包含 `id`、`name`、`version`、`description`、`dependencies`,通过 Builder 模式构建 | +| `PluginContext.java` | 插件运行时上下文,提供 `PluginManager` 引用和跨插件共享属性 | +| `PluginManager.java` | **插件管理器**(`@Component`),负责:生命周期管理(load/enable/disable)、注册发现(按 id/command 查找)、启用过滤(白名单/黑名单)、主菜单交互循环、命令行分发 | +| `PluginState.java` | 插件状态枚举:`UNLOADED` → `LOADED` → `ACTIVE` / `DISABLED` / `ERROR` | -#### `system/support/ConsoleHelper.java` -- **作用**: 控制台交互工具类(静态方法),统一控制台 I/O +#### 3.2.2 Tool 基础契约(`system/tool/`) -#### `system/support/FileOperationService.java` -- **作用**: 文件操作服务,提供文件读取、扫描、写入、项目路径管理等基础能力 +#### `Tool.java` +- **作用**: 工具基础接口,定义 `getId()`、`getName()`、`getMenuOptions()`、`execute()`、`handleChoice()`、`getCommandName()`、`executeCommand()` 等方法 +- **关系**: `Plugin` 继承 `Tool`,biz 模块直接实现 `Plugin` -#### `system/support/OutputService.java` -- **作用**: 输出服务,负责将数据输出为 CSV(OpenCSV)或 Excel(EasyExcel)格式,支持单Sheet/多Sheet、追加写入等 +#### 3.2.3 共享基础设施(`system/support/`) -#### `system/config/WorkUtilsProperties.java` -- **作用**: 全局配置属性类,绑定 `application.yml` 中 `workutils` 前缀的配置 -- **功能**: 管理基础目录、配置目录、导出目录路径,提供带日期时间戳的导出文件路径生成,管理工具启用/禁用白名单和黑名单 +| 文件 | 作用 | +|------|------| +| `ConsoleHelper.java` | 控制台交互工具类(静态方法),统一控制台 I/O | +| `FileOperationService.java` | 文件操作服务,提供文件读取、扫描、写入、项目路径管理等基础能力 | +| `OutputService.java` | 输出服务,负责将数据输出为 CSV(OpenCSV)或 Excel(EasyExcel)格式 | + +#### 3.2.4 全局配置(`system/config/`) + +| 文件 | 作用 | +|------|------| +| `WorkUtilsProperties.java` | 全局配置属性类(`@ConfigurationProperties`),管理路径、工具启用/禁用(`ToolsConfig`) | --- -### 3.3 业务工具层(`biz/`) +### 3.3 业务插件层(`biz/`) -> **架构约定**: 每个 biz 模块的 Tool 类(如 `GitTool.java`)放在模块根目录作为入口,所有服务实现及附属文件(config/model/internal/generator)收在 `service/` 子包下,内部细节用 `internal/` 隔离。 +> **架构约定**: 每个 biz 模块的 Tool 类放在模块根目录,实现 `Plugin` 接口(继承自 `Tool`)。通过静态 `PluginDescriptor` 声明插件元数据(id/name/version/description)。所有服务实现及附属文件收在 `service/` 子包下,内部细节用 `internal/` 隔离。 -#### 3.3.1 扫描工具(`biz/scanner/`) +#### 3.3.1 扫描插件(`biz/scanner/`) | 文件 | 作用 | |------|------| -| `ScannerTool.java` | 扫描工具入口(实现 Tool),负责菜单交互,委托 ScannerService 执行业务逻辑 | +| `ScannerTool.java` | 扫描插件入口(实现 Plugin),版本 `1.0.0`,负责菜单交互,委托 ScannerService 执行业务逻辑 | | `service/ScannerService.java` | 扫描服务,支持关键词扫描、Redis Key 扫描、Dubbo 接口扫描三种类型,支持并发多项目扫描 | | `service/config/ScanConfig.java` | 扫描配置,定义文件扩展名、排除目录、最大文件大小等参数 | -#### 3.3.2a MyBatis SQL导出工具(`biz/mybatisexport/`) +#### 3.3.2a MyBatis SQL导出插件(`biz/mybatisexport/`) | 文件 | 作用 | |------|------| -| `MyBatisExportTool.java` | SQL 导出工具入口(实现 Tool),负责菜单交互,委托 MyBatisExportService 执行 | -| `service/MyBatisExportService.java` | SQL 导出核心服务,从 XML 文件中提取 select/insert/update/delete 标签,支持按表名/关键字/标签ID 过滤导出为 CSV | -| `service/model/MybatisTag.java` | MyBatis 标签数据对象,封装 tagId、tagName、parameterType、resultMap、namespace | +| `MyBatisExportTool.java` | SQL 导出插件入口(实现 Plugin),版本 `1.0.0`,负责菜单交互 | +| `service/MyBatisExportService.java` | SQL 导出核心服务,从 XML 文件中提取 select/insert/update/delete 标签 | +| `service/model/MybatisTag.java` | MyBatis 标签数据对象 | -#### 3.3.2b MyBatis逆向工程工具(`biz/mybatisgenerator/`) +#### 3.3.2b MyBatis逆向工程插件(`biz/mybatisgenerator/`) | 文件 | 作用 | |------|------| -| `MyBatisGeneratorTool.java` | 逆向工程工具入口(实现 Tool),负责菜单交互,委托 MyBatisGeneratorService 执行 | -| `service/generator/MyBatisGeneratorService.java` | MyBatis 逆向工程服务,根据数据库表生成 PO、Mapper 接口、XML 映射文件 | -| `service/generator/CustomCommentGenerator.java` | 自定义注释生成器,生成简洁中文注释 | -| `service/generator/LombokPlugin.java` | Lombok 插件,为 PO 类添加 `@Data` 注解 | -| `service/generator/SelectiveOnlyPlugin.java` | 自定义插件,只保留 `insertSelective` 和 `updateByPrimaryKeySelective` | +| `MyBatisGeneratorTool.java` | 逆向工程插件入口(实现 Plugin),版本 `1.0.0` | +| `service/generator/MyBatisGeneratorService.java` | MyBatis 逆向工程服务 | +| `service/generator/CustomCommentGenerator.java` | 自定义注释生成器 | +| `service/generator/LombokPlugin.java` | Lombok 插件(@Data) | +| `service/generator/SelectiveOnlyPlugin.java` | 只保留 Selective 方法插件 | -> **设计说明**: 原 `biz/mybatis/` 已按工具维度拆分为 `mybatisexport` 和 `mybatisgenerator` 两个独立包,为后续**插件化设计**做准备——每个工具将独立打包、独立加载。 - -#### 3.3.3 Git 工具(`biz/git/`) +#### 3.3.3 Git 插件(`biz/git/`) | 文件 | 作用 | |------|------| -| `GitTool.java` | Git 工具入口(实现 Tool),负责菜单交互,委托 GitService 执行 | -| `service/GitService.java` | Git 服务接口,定义分支变更检查、切换拉取、健康检测、修复仓库等方法 | -| `service/GitServiceImpl.java` | Git 服务实现,协调各策略执行仓库健康检测和修复逻辑 | -| `service/config/GitOptimizationConfig.java` | Git 性能优化配置,管理超时、线程池、缓存、批处理、Fetch、网络等参数 | -| `service/model/GitCommand.java` | Git 命令传输对象,定义命令类型枚举(CHECK_BRANCH_CHANGES、CHECKOUT_AND_PULL 等6种) | -| `service/internal/GitCommandExecutor.java` | **核心执行器**,封装原生 git 命令执行(跨平台),提供超时控制、并发线程池、命令批处理 | -| `service/internal/GitCommandController.java` | Git 命令控制器,管理命令的异步执行、跳过、中断 | -| `service/internal/GitRepositoryManager.java` | Git 仓库管理器,提供仓库发现、分支操作、批量操作等高级封装 | -| `service/internal/GitResultCache.java` | Git 结果缓存管理器,缓存仓库信息/分支信息/提交ID,减少重复 git 命令 | -| `service/internal/strategy/GitCommandStrategy.java` | Git 命令策略接口 | -| `service/internal/strategy/GitStrategyFactory.java` | Git 策略工厂,注册和管理所有策略实现 | -| `service/internal/strategy/CheckBranchChangesStrategy.java` | 分支变更检查策略,比较两个分支的差异 | -| `service/internal/strategy/CheckoutAndPullStrategy.java` | 切换并拉取策略,批量切换分支并拉取最新代码 | +| `GitTool.java` | Git 插件入口(实现 Plugin),版本 `1.0.0` | +| `service/GitService.java` | Git 服务接口 | +| `service/GitServiceImpl.java` | Git 服务实现 | +| `service/config/GitOptimizationConfig.java` | Git 性能优化配置 | +| `service/model/GitCommand.java` | Git 命令传输对象 | +| `service/internal/GitCommandExecutor.java` | **核心执行器**,跨平台 git 命令执行 | +| `service/internal/GitCommandController.java` | Git 命令控制器(异步/中断) | +| `service/internal/GitRepositoryManager.java` | Git 仓库管理器 | +| `service/internal/GitResultCache.java` | Git 结果缓存管理器 | +| `service/internal/strategy/*` | 策略模式实现 | -#### 3.3.4 批量工具(`biz/batch/`) +#### 3.3.4 批量插件(`biz/batch/`) | 文件 | 作用 | |------|------| -| `BatchTool.java` | 批量处理工具入口(实现 Tool),显示批量检查选项,委托各 Service 执行 | +| `BatchTool.java` | 批量处理插件入口(实现 Plugin),版本 `1.0.0`,一键执行多种检查 | --- @@ -215,16 +224,18 @@ src/main/resources/ | 模式/特点 | 应用位置 | 说明 | |-----------|---------|------| -| **策略模式** | `Tool` 接口 + 各 biz Tool 实现 | 各工具通过实现 `Tool` 接口注册到 `ToolRegistry`,可插拔扩展,支持交互和命令行双模式 | +| **插件架构** | `Plugin` + `PluginManager` + `PluginDescriptor` | 每个 biz 模块作为独立 Plugin,声明版本、描述,PluginManager 统一管理生命周期(load/enable/disable),可插拔扩展 | +| **策略模式** | `Tool` / `Plugin` 接口 + 各 biz 实现 | 各插件通过实现 Plugin 接口注册到 PluginManager,支持交互和命令行双模式 | | **策略模式** | `GitCommandStrategy` + 各 Strategy 实现 | Git 命令执行策略,`GitStrategyFactory` 管理注册 | -| **工厂模式** | `ToolRegistry`、`GitStrategyFactory` | Spring 自动发现和注入 Bean | +| **工厂模式** | `PluginManager`、`GitStrategyFactory` | Spring 自动发现和注入 Bean | | **命令模式** | `GitCommand` + `GitCommandController` | 封装 Git 命令的异步执行、中断、跳过 | | **缓存模式** | `GitResultCache` | 缓存 Git 查询结果,减少重复命令执行 | | **并发处理** | `GitCommandExecutor`、`ScannerService` | 线程池并发执行 Git 命令和多项目扫描 | | **过滤器模式** | `MyBatisExportService` 内部类 | `SqlFilter` 接口实现表名/关键字/标签ID 过滤 | | **模板回调** | `FileOperationService.scanFilesRecursively` | 使用 `SimpleFileVisitor` 实现文件遍历回调 | -| **分层架构** | `system/` vs `biz/` | system 定义平台契约与共享基础设施,biz 实现具体工具 | +| **分层架构** | `system/` vs `biz/` | system 定义平台契约与共享基础设施,biz 实现具体插件 | | **内部隔离** | `biz/*/service/internal/` | 内部实现类用 `internal/` 隔离,不对外暴露 | +| **生命周期管理** | `Plugin.onLoad()` / `Plugin.onUnload()` | 插件加载/卸载回调,PluginContext 传递运行时上下文 | --- @@ -232,21 +243,28 @@ src/main/resources/ ``` WorkUtilsApplication (入口) - ├── 交互模式 → ToolRegistry.runMainMenu() - │ └── ToolRegistry.getEnabledTools() + ├── 交互模式 → PluginManager.runMainMenu() + │ └── PluginManager.getEnabledPlugins() │ ├── ScannerTool ────────────→ ScannerService │ ├── MyBatisExportTool ──────→ MyBatisExportService │ ├── MyBatisGeneratorTool ───→ MyBatisGeneratorService │ ├── GitTool ────────────────→ GitService → GitStrategyFactory │ └── BatchTool ──────────────→ ScannerService + MyBatisExportService │ - └── 命令行模式 → ToolRegistry.getToolByCommand(command) + └── 命令行模式 → PluginManager.getPluginByCommand(command) ├── "scan" → ScannerTool.executeCommand(args) ├── "sql" → MyBatisExportTool.executeCommand(args) ├── "batch" → BatchTool.executeCommand(args) └── "git" → GitTool.executeCommand(args) (启动类不依赖任何具体业务 Service) +插件加载链路: + PluginManager.init() + ├── 遍历 Spring 中所有 Plugin Bean + ├── 依次调用 plugin.onLoad(PluginContext) + ├── 按 ToolsConfig 过滤启用/禁用 + └── 构建 pluginByIdMap / pluginByCommandMap / pluginStates + 核心依赖链: ScannerService → FileOperationService + OutputService MyBatisExportService → FileOperationService + OutputService @@ -273,3 +291,69 @@ WorkUtilsApplication (入口) 2. **将数据库密码移到外部配置文件**或使用环境变量 3. **更新 `项目说明.md`** 使其与实际技术栈一致 4. **增加单元测试**,目前无测试代码 + +--- + +## 八、插件化架构设计 + +### 8.1 核心概念 + +``` +┌──────────────────────────────────────────────────────┐ +│ PluginManager │ +│ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │ +│ │ 生命周期管理 │ │ 注册与发现 │ │ 菜单/命令 │ │ +│ │ load/unload │ │ id/command │ │ 分发 │ │ +│ └─────────────┘ └──────────────┘ └────────────┘ │ +│ │ +│ pluginByIdMap pluginByCommandMap pluginStates │ +└──────────────────────┬───────────────────────────────┘ + │ onLoad(PluginContext) + ┌──────────────┼──────────────┬──────────────┐ + ▼ ▼ ▼ ▼ + ScannerTool MyBatisExport MyBatisGen GitTool BatchTool + [v1.0.0] [v1.0.0] [v1.0.0] [v1.0.0] [v1.0.0] +``` + +### 8.2 接口层次 + +``` +Tool (system/tool/) ← 基础契约:getId/getName/getMenuOptions/execute/... + ↑ +Plugin (system/plugin/) ← 扩展契约:+getDescriptor/onLoad/onUnload + ↑ +各 biz 模块 ← 具体实现:@Component + PluginDescriptor +``` + +### 8.3 插件描述符(PluginDescriptor) + +每个插件通过静态 `PluginDescriptor` 声明自身元数据: + +| 字段 | 说明 | 示例 | +|------|------|------| +| `id` | 全局唯一标识 | `"scan"`, `"mybatisexport"` | +| `name` | 菜单显示名称 | `"扫描功能"` | +| `version` | 语义化版本 | `"1.0.0"` | +| `description` | 功能简述 | `"关键词/Redis/Dubbo扫描工具"` | +| `dependencies` | 依赖的插件 ID(预留) | `[]` | + +### 8.4 插件状态机 + +``` +UNLOADED ──load──▶ LOADED ──enable──▶ ACTIVE ──disable──▶ DISABLED + ▲ │ │ │ + │ │ error │ error │ + │ ▼ ▼ │ + └──────────────── ERROR ◀──────────── ERROR ◀─────────────┘ + unload ↻ UNLOADED +``` + +### 8.5 后续插件化方向 + +当前已完成插件化基础框架搭建,后续可进一步实现: + +1. **SPI 自动发现** — 通过 `META-INF/services` 或 Spring Factories 自动发现外部 jar 中的 Plugin 实现 +2. **独立打包** — 每个 biz 模块打成独立 jar,`PluginManager` 动态加载 +3. **热加载** — 基于自定义 ClassLoader 实现插件的动态加载/卸载/更新 +4. **依赖注入隔离** — 每个插件使用独立 Spring 子容器(`ApplicationContext`),避免 Bean 冲突 +5. **插件市场** — 插件仓库 + 版本管理 + 远程下载