diff --git a/src/main/java/com/example/workutils/WorkUtilsApplication.java b/src/main/java/com/example/workutils/WorkUtilsApplication.java index c057943..eb7e85d 100644 --- a/src/main/java/com/example/workutils/WorkUtilsApplication.java +++ b/src/main/java/com/example/workutils/WorkUtilsApplication.java @@ -41,19 +41,18 @@ public class WorkUtilsApplication implements CommandLineRunner { } /** - * 命令行模式:通过 PluginManager 按命令名分发到对应 Plugin + * 命令行模式:通过 PluginManager 按命令名分发到对应 Tool * 启动类不依赖任何具体业务 Service */ private void processCommand(String... args) throws Exception { String command = args[0].toLowerCase(); - Optional pluginOpt = pluginManager.getPluginByCommand(command); - if (pluginOpt.isPresent()) { - Plugin plugin = pluginOpt.get(); + Optional toolOpt = pluginManager.getPluginByCommand(command); + if (toolOpt.isPresent()) { + Plugin plugin = toolOpt.get(); try { - // args[0]是命令名,从args[1]开始传给plugin - String[] pluginArgs = args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]; - plugin.executeCommand(pluginArgs); + String[] toolArgs = args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]; + plugin.executeCommand(toolArgs); } catch (Exception e) { log.error("命令执行失败: {} [{}]", plugin.getName(), plugin.getId(), e); System.err.println("命令执行失败: " + e.getMessage()); diff --git a/src/main/java/com/example/workutils/biz/batch/BatchTool.java b/src/main/java/com/example/workutils/biz/batch/BatchPlugin.java similarity index 98% rename from src/main/java/com/example/workutils/biz/batch/BatchTool.java rename to src/main/java/com/example/workutils/biz/batch/BatchPlugin.java index 2270145..d297141 100644 --- a/src/main/java/com/example/workutils/biz/batch/BatchTool.java +++ b/src/main/java/com/example/workutils/biz/batch/BatchPlugin.java @@ -13,13 +13,14 @@ import java.util.List; import java.util.Scanner; /** - * 批量处理插件(实现 Plugin 接口) + * 批量处理插件(实现 Tool 接口) * 仅负责菜单展示和用户交互,业务逻辑委托给各Service + */ @Slf4j @Component @RequiredArgsConstructor -public class BatchTool implements Plugin { +public class BatchPlugin implements Plugin { @FunctionalInterface public interface ExceptionRunnable { diff --git a/src/main/java/com/example/workutils/biz/git/GitTool.java b/src/main/java/com/example/workutils/biz/git/GitPlugin.java similarity index 98% rename from src/main/java/com/example/workutils/biz/git/GitTool.java rename to src/main/java/com/example/workutils/biz/git/GitPlugin.java index 4032721..ab47aae 100644 --- a/src/main/java/com/example/workutils/biz/git/GitTool.java +++ b/src/main/java/com/example/workutils/biz/git/GitPlugin.java @@ -2,8 +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.plugin.Plugin; import com.example.workutils.system.plugin.PluginDescriptor; +import com.example.workutils.system.plugin.Plugin; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -14,12 +14,12 @@ import java.util.Scanner; import java.util.stream.Collectors; /** - * Git仓库健康检测插件(实现 Plugin 接口) + * Git仓库健康检测插件(实现 Tool 接口) */ @Slf4j @Component @RequiredArgsConstructor -public class GitTool implements Plugin { +public class GitPlugin implements Plugin { private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder() .id("git-health") diff --git a/src/main/java/com/example/workutils/biz/git/service/GitServiceImpl.java b/src/main/java/com/example/workutils/biz/git/service/GitServiceImpl.java index 1b5f578..b660f9a 100644 --- a/src/main/java/com/example/workutils/biz/git/service/GitServiceImpl.java +++ b/src/main/java/com/example/workutils/biz/git/service/GitServiceImpl.java @@ -3,7 +3,7 @@ package com.example.workutils.biz.git.service; import com.example.workutils.biz.git.service.internal.GitCommandExecutor; import com.example.workutils.biz.git.service.internal.strategy.GitStrategyFactory; import com.example.workutils.biz.git.service.model.GitCommand; -import com.example.workutils.system.support.FileOperationService; +import com.example.workutils.system.support.FileOperationHelper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -26,7 +26,7 @@ public class GitServiceImpl implements GitService { private final GitStrategyFactory strategyFactory; @Autowired - private FileOperationService fileOperationService; + private FileOperationHelper fileOperationHelper; private static final String DEFAULT_BRANCH = "master"; @@ -52,10 +52,10 @@ public class GitServiceImpl implements GitService { @Override public List checkCorruptedRepos(Scanner scanner) { List corruptedRepos = new ArrayList<>(); - List projects; + List projects; try { - projects = fileOperationService.getProjectPathsWithStatus(); + projects = fileOperationHelper.getProjectPathsWithStatus(); } catch (IOException e) { log.error("读取项目路径配置文件失败", e); return corruptedRepos; @@ -68,7 +68,7 @@ public class GitServiceImpl implements GitService { int totalCount = projects.size(); boolean isAborted = false; - for (FileOperationService.ProjectPathInfo project : projects) { + for (FileOperationHelper.ProjectPathInfo project : projects) { String repoPath = project.getPath(); String repoName = extractProjectName(repoPath); @@ -80,7 +80,7 @@ public class GitServiceImpl implements GitService { if (healthResult.isHealthy()) { try { - fileOperationService.updateProjectStatus(repoPath, FileOperationService.ProjectStatus.NORMAL); + fileOperationHelper.updateProjectStatus(repoPath, FileOperationHelper.ProjectStatus.NORMAL); System.out.printf("[%d/%d] %s - 正常%n", checkedCount, totalCount, repoName); } catch (IOException e) { log.warn("更新配置文件失败: {}", repoPath, e); @@ -88,7 +88,7 @@ public class GitServiceImpl implements GitService { } else { corruptedRepos.add(healthResult); try { - fileOperationService.updateProjectStatus(repoPath, FileOperationService.ProjectStatus.CORRUPTED); + fileOperationHelper.updateProjectStatus(repoPath, FileOperationHelper.ProjectStatus.CORRUPTED); System.out.printf("[%d/%d] %s - 损坏: %s%n", checkedCount, totalCount, repoName, healthResult.getCorruptionReason()); } catch (IOException e) { log.warn("更新配置文件失败: {}", repoPath, e); @@ -143,7 +143,7 @@ public class GitServiceImpl implements GitService { if (result.isSuccess()) { log.info("仓库修复成功: {}", repoPath); try { - fileOperationService.updateProjectStatus(repoPath, FileOperationService.ProjectStatus.NORMAL); + fileOperationHelper.updateProjectStatus(repoPath, FileOperationHelper.ProjectStatus.NORMAL); } catch (IOException e) { log.warn("更新配置文件失败: {}", repoPath, e); } @@ -157,9 +157,9 @@ public class GitServiceImpl implements GitService { @Override public List listCorruptedRepos() { try { - return fileOperationService.getProjectPathsWithStatus().stream() - .filter(p -> p.getStatus() == FileOperationService.ProjectStatus.CORRUPTED) - .map(FileOperationService.ProjectPathInfo::getPath) + return fileOperationHelper.getProjectPathsWithStatus().stream() + .filter(p -> p.getStatus() == FileOperationHelper.ProjectStatus.CORRUPTED) + .map(FileOperationHelper.ProjectPathInfo::getPath) .collect(Collectors.toList()); } catch (IOException e) { log.error("读取损坏仓库列表失败", e); diff --git a/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportTool.java b/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportPlugin.java similarity index 98% rename from src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportTool.java rename to src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportPlugin.java index c8ffef2..3c201e4 100644 --- a/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportTool.java +++ b/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportPlugin.java @@ -1,8 +1,8 @@ package com.example.workutils.biz.mybatisexport; import com.example.workutils.biz.mybatisexport.service.MyBatisExportService; -import com.example.workutils.system.plugin.Plugin; import com.example.workutils.system.plugin.PluginDescriptor; +import com.example.workutils.system.plugin.Plugin; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -11,12 +11,12 @@ import java.util.Arrays; import java.util.Scanner; /** - * MyBatis SQL导出插件(实现 Plugin 接口) + * MyBatis SQL导出插件(实现 Tool 接口) */ @Slf4j @Component @RequiredArgsConstructor -public class MyBatisExportTool implements Plugin { +public class MyBatisExportPlugin implements Plugin { private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder() .id("mybatisexport") diff --git a/src/main/java/com/example/workutils/biz/mybatisexport/service/MyBatisExportService.java b/src/main/java/com/example/workutils/biz/mybatisexport/service/MyBatisExportService.java index b589ca8..843cc90 100644 --- a/src/main/java/com/example/workutils/biz/mybatisexport/service/MyBatisExportService.java +++ b/src/main/java/com/example/workutils/biz/mybatisexport/service/MyBatisExportService.java @@ -2,7 +2,7 @@ package com.example.workutils.biz.mybatisexport.service; import com.example.workutils.biz.mybatisexport.service.model.MybatisTag; import com.example.workutils.biz.scanner.service.config.ScanConfig; -import com.example.workutils.system.support.FileOperationService; +import com.example.workutils.system.support.FileOperationHelper; import com.example.workutils.system.support.OutputService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -25,7 +25,7 @@ import java.util.regex.Pattern; public class MyBatisExportService { @Autowired - private FileOperationService fileOperationService; + private FileOperationHelper fileOperationHelper; @Autowired private OutputService outputService; @@ -78,7 +78,7 @@ public class MyBatisExportService { private void exportWithFilter(String fileNamePrefix, SqlFilter filter, ProcessStrategy processStrategy) throws Exception { - List projectPaths = fileOperationService.getProjectPaths(); + List projectPaths = fileOperationHelper.getProjectPaths(); if (projectPaths.isEmpty()) { log.warn("未找到项目路径"); return; @@ -108,10 +108,10 @@ public class MyBatisExportService { private List processProject(String projectPath, SqlFilter filter, ProcessStrategy processStrategy) throws Exception { List records = new ArrayList<>(); - String projectName = fileOperationService.extractProjectName(projectPath); + String projectName = fileOperationHelper.extractProjectName(projectPath); ScanConfig config = ScanConfig.forXmlFiles(); - List xmlFiles = fileOperationService.scanFiles(projectPath, config); + List xmlFiles = fileOperationHelper.scanFiles(projectPath, config); for (Path xmlFile : xmlFiles) { try { @@ -134,7 +134,7 @@ public class MyBatisExportService { private List parseXmlFile(Path xmlFile) throws Exception { List tags = new ArrayList<>(); - String content = fileOperationService.readFileContent(xmlFile); + String content = fileOperationHelper.readFileContent(xmlFile); for (String tagType : SQL_TAG_TYPES) { tags.addAll(parseSqlTags(content, tagType)); diff --git a/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorTool.java b/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorPlugin.java similarity index 98% rename from src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorTool.java rename to src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorPlugin.java index 40e30d9..00da0ca 100644 --- a/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorTool.java +++ b/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorPlugin.java @@ -12,12 +12,12 @@ import java.util.List; import java.util.Scanner; /** - * MyBatis逆向工程插件(实现 Plugin 接口) + * MyBatis逆向工程插件(实现 Tool 接口) */ @Slf4j @Component @RequiredArgsConstructor -public class MyBatisGeneratorTool implements Plugin { +public class MyBatisGeneratorPlugin implements Plugin { private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder() .id("mybatisgenerator") diff --git a/src/main/java/com/example/workutils/biz/scanner/ScannerTool.java b/src/main/java/com/example/workutils/biz/scanner/ScannerPlugin.java similarity index 96% rename from src/main/java/com/example/workutils/biz/scanner/ScannerTool.java rename to src/main/java/com/example/workutils/biz/scanner/ScannerPlugin.java index 7372625..19f51e5 100644 --- a/src/main/java/com/example/workutils/biz/scanner/ScannerTool.java +++ b/src/main/java/com/example/workutils/biz/scanner/ScannerPlugin.java @@ -1,8 +1,8 @@ package com.example.workutils.biz.scanner; import com.example.workutils.biz.scanner.service.ScannerService; -import com.example.workutils.system.plugin.Plugin; import com.example.workutils.system.plugin.PluginDescriptor; +import com.example.workutils.system.plugin.Plugin; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -10,12 +10,12 @@ import org.springframework.stereotype.Component; import java.util.Scanner; /** - * 扫描工具插件(实现 Plugin 接口) + * 扫描工具插件(实现 Tool 接口) */ @Slf4j @Component @RequiredArgsConstructor -public class ScannerTool implements Plugin { +public class ScannerPlugin implements Plugin { private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder() .id("scan") diff --git a/src/main/java/com/example/workutils/biz/scanner/service/ScannerService.java b/src/main/java/com/example/workutils/biz/scanner/service/ScannerService.java index 93e4951..35efe22 100644 --- a/src/main/java/com/example/workutils/biz/scanner/service/ScannerService.java +++ b/src/main/java/com/example/workutils/biz/scanner/service/ScannerService.java @@ -2,7 +2,7 @@ package com.example.workutils.biz.scanner.service; import com.example.workutils.biz.scanner.service.config.ScanConfig; import com.example.workutils.system.config.WorkUtilsProperties; -import com.example.workutils.system.support.FileOperationService; +import com.example.workutils.system.support.FileOperationHelper; import com.example.workutils.system.support.OutputService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -24,14 +24,14 @@ public class ScannerService { private final ExecutorService executorService = Executors.newFixedThreadPool(10); @Autowired - private FileOperationService fileOperationService; + private FileOperationHelper fileOperationHelper; @Autowired private OutputService outputService; @Autowired private WorkUtilsProperties properties; public ScanResult scanAllProjects(ScanType scanType, String pattern) throws Exception { - List projectPaths = fileOperationService.getProjectPaths(); + List projectPaths = fileOperationHelper.getProjectPaths(); if (projectPaths.isEmpty()) { log.warn("No project paths found"); return new ScanResult(); @@ -63,10 +63,10 @@ public class ScannerService { private ProjectScanResult scanProject(String projectPath, ScanType scanType, String pattern) throws Exception { ProjectScanResult projectResult = new ProjectScanResult(projectPath, new ArrayList<>()); - String projectName = fileOperationService.extractProjectName(projectPath); + String projectName = fileOperationHelper.extractProjectName(projectPath); ScanConfig config = getConfigForScanType(scanType); - List files = fileOperationService.scanFiles(projectPath, config); + List files = fileOperationHelper.scanFiles(projectPath, config); for (Path file : files) { try { @@ -90,7 +90,7 @@ public class ScannerService { List matches = new ArrayList<>(); List lines; try { - lines = fileOperationService.readLinesFromFile(file); + lines = fileOperationHelper.readLinesFromFile(file); } catch (Exception e) { log.warn("Failed to read file: {}, skipping this file", file, e); return matches; @@ -185,7 +185,7 @@ public class ScannerService { } } - fileOperationService.ensureDirectoryExists(properties.getTodayExportPath()); + fileOperationHelper.ensureDirectoryExists(properties.getTodayExportPath()); outputService.writeRecordsToCsv(records, outputFileName); log.info("Scan results exported to: {}", outputFileName); } diff --git a/src/main/java/com/example/workutils/system/plugin/Plugin.java b/src/main/java/com/example/workutils/system/plugin/Plugin.java index 17e2f4b..02048bb 100644 --- a/src/main/java/com/example/workutils/system/plugin/Plugin.java +++ b/src/main/java/com/example/workutils/system/plugin/Plugin.java @@ -1,24 +1,77 @@ package com.example.workutils.system.plugin; -import com.example.workutils.system.tool.Tool; +import com.example.workutils.system.support.ConsoleHelper; + +import java.util.Scanner; /** - * 插件接口 —— 所有 biz 模块的顶级契约 + * 工具接口 —— 所有 biz 模块的顶级契约 *

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

    - *
  • {@link #getDescriptor()} — 提供插件元数据(id/name/version/description/dependencies)
  • - *
  • {@link #onLoad(PluginContext)} — 插件加载时回调,注入运行时上下文
  • - *
  • {@link #onUnload()} — 插件卸载时回调,清理资源
  • + *
  • 插件元数据 — {@link #getDescriptor()} 声明 id/name/version/description
  • + *
  • 生命周期 — {@link #onLoad(PluginContext)} / {@link #onUnload()}
  • + *
  • 交互模式 — {@link #execute(Scanner)} 模板方法驱动,子类实现 {@link #handleChoice(int, Scanner)}
  • + *
  • 命令行模式 — {@link #getCommandName()} + {@link #executeCommand(String...)}
  • *
- *

- * getId()/getName() 默认委托给 PluginDescriptor,实现类只需提供 getDescriptor()。 */ -public interface Plugin extends Tool { +public interface Plugin { /** 插件描述符(元数据) */ PluginDescriptor getDescriptor(); + /** 工具唯一标识,默认委托给 PluginDescriptor */ + default String getId() { + return getDescriptor().getId(); + } + + /** 菜单显示名称,默认委托给 PluginDescriptor */ + default String getName() { + return getDescriptor().getName(); + } + + /** 子菜单选项,返回空数组表示无子菜单 */ + String[] getMenuOptions(); + + /** 命令行命令名(如 "sql", "scan"),返回空字符串表示不支持命令行模式 */ + default String getCommandName() { + return ""; + } + + // ==================== 交互模式(模板方法) ==================== + + default void execute(Scanner scanner) { + while (true) { + ConsoleHelper.printMenuTitle(getName()); + ConsoleHelper.displayMenu(getMenuOptions()); + + int choice = ConsoleHelper.readChoice(scanner); + scanner.nextLine(); + + if (choice == 0) { + return; + } + + try { + handleChoice(choice, scanner); + } catch (Exception e) { + ConsoleHelper.printError(e.getMessage()); + } + } + } + + /** 处理子菜单选择,子类实现具体分发逻辑 */ + default void handleChoice(int choice, Scanner scanner) throws Exception { + } + + // ==================== 命令行模式 ==================== + + default void executeCommand(String... args) throws Exception { + System.out.println("工具 [" + getName() + "] 不支持命令行模式"); + } + + // ==================== 生命周期回调 ==================== + /** 插件加载回调,PluginManager 在注册时调用 */ default void onLoad(PluginContext context) { } @@ -27,15 +80,25 @@ public interface Plugin extends Tool { default void onUnload() { } - // ==================== 默认委托 ==================== + // ==================== 控制台输出辅助方法 ==================== - @Override - default String getId() { - return getDescriptor().getId(); + default void printMenuTitle(String title) { + ConsoleHelper.printMenuTitle(title); } - @Override - default String getName() { - return getDescriptor().getName(); + default void displayMenu(String[] options) { + ConsoleHelper.displayMenu(options); + } + + default void printSuccess(String operation) { + ConsoleHelper.printSuccess(operation); + } + + default void printError(String message) { + ConsoleHelper.printError(message); + } + + default void printInvalidOption() { + ConsoleHelper.printInvalidOption(); } } diff --git a/src/main/java/com/example/workutils/system/plugin/PluginDescriptor.java b/src/main/java/com/example/workutils/system/plugin/PluginDescriptor.java index b27c7d6..b80ed2b 100644 --- a/src/main/java/com/example/workutils/system/plugin/PluginDescriptor.java +++ b/src/main/java/com/example/workutils/system/plugin/PluginDescriptor.java @@ -6,7 +6,7 @@ import java.util.List; /** * 插件描述符(不可变元数据) *

- * 每个 Plugin 通过 {@link Plugin#getDescriptor()} 返回此描述符, + * 每个插件通过 {@link Plugin#getDescriptor()} 返回此描述符, * PluginManager 据此识别、管理、展示插件。 */ public class PluginDescriptor { diff --git a/src/main/java/com/example/workutils/system/plugin/PluginManager.java b/src/main/java/com/example/workutils/system/plugin/PluginManager.java index 1cc0d45..861480a 100644 --- a/src/main/java/com/example/workutils/system/plugin/PluginManager.java +++ b/src/main/java/com/example/workutils/system/plugin/PluginManager.java @@ -11,7 +11,7 @@ import java.util.*; /** * 插件管理器 —— 插件生命周期管理 + 注册中心 + 主菜单 *

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

    *
  • 生命周期 — load / unload / enable / disable
  • *
  • 注册发现 — 按 id / command 查找
  • diff --git a/src/main/java/com/example/workutils/system/support/FileOperationService.java b/src/main/java/com/example/workutils/system/support/FileOperationHelper.java similarity index 99% rename from src/main/java/com/example/workutils/system/support/FileOperationService.java rename to src/main/java/com/example/workutils/system/support/FileOperationHelper.java index fe62257..c38efdb 100644 --- a/src/main/java/com/example/workutils/system/support/FileOperationService.java +++ b/src/main/java/com/example/workutils/system/support/FileOperationHelper.java @@ -21,7 +21,7 @@ import java.util.stream.Stream; */ @Service @Slf4j -public class FileOperationService { +public class FileOperationHelper { @Autowired private WorkUtilsProperties properties; diff --git a/src/main/java/com/example/workutils/system/support/OutputService.java b/src/main/java/com/example/workutils/system/support/OutputHelper.java similarity index 88% rename from src/main/java/com/example/workutils/system/support/OutputService.java rename to src/main/java/com/example/workutils/system/support/OutputHelper.java index e37f0d4..681666c 100644 --- a/src/main/java/com/example/workutils/system/support/OutputService.java +++ b/src/main/java/com/example/workutils/system/support/OutputHelper.java @@ -22,10 +22,10 @@ import java.util.List; */ @Service @Slf4j -public class OutputService { +public class OutputHelper { @Autowired - private FileOperationService fileOperationService; + private FileOperationHelper fileOperationHelper; @Autowired private WorkUtilsProperties properties; @@ -39,7 +39,7 @@ public class OutputService { } else { filePath = properties.resolveExportPath(outputPath); } - fileOperationService.ensureDirectoryExists(filePath.getParent()); + fileOperationHelper.ensureDirectoryExists(filePath.getParent()); try (CSVWriter writer = new CSVWriter(new FileWriter(filePath.toFile()))) { if (headers.length > 0) { @@ -62,9 +62,9 @@ public class OutputService { public void appendToCsv(String[] record, String outputPath, String... headers) throws IOException { Path filePath = properties.resolveExportPath(outputPath); - fileOperationService.ensureDirectoryExists(filePath.getParent()); + fileOperationHelper.ensureDirectoryExists(filePath.getParent()); - boolean fileExists = fileOperationService.fileExists(filePath); + boolean fileExists = fileOperationHelper.fileExists(filePath); boolean shouldWriteHeaders = !fileExists && headers.length > 0; try (CSVWriter writer = new CSVWriter(new FileWriter(filePath.toFile(), true))) { @@ -82,21 +82,21 @@ public class OutputService { public void writeToExcel(List data, String outputPath, Class modelClass) throws IOException { Path filePath = properties.resolveExportPath(outputPath); - fileOperationService.ensureDirectoryExists(filePath.getParent()); + fileOperationHelper.ensureDirectoryExists(filePath.getParent()); EasyExcel.write(filePath.toFile(), modelClass).sheet().doWrite(data); log.info("Excel文件写入成功: {}", filePath); } public void writeToExcel(List data, String outputPath, String sheetName, Class modelClass) throws IOException { Path filePath = properties.resolveExportPath(outputPath); - fileOperationService.ensureDirectoryExists(filePath.getParent()); + fileOperationHelper.ensureDirectoryExists(filePath.getParent()); EasyExcel.write(filePath.toFile(), modelClass).sheet(sheetName).doWrite(data); log.info("Excel文件写入成功: {}", filePath); } public void writeToExcelWithMultipleSheets(List> sheetsData, String outputPath) throws IOException { Path filePath = properties.resolveExportPath(outputPath); - fileOperationService.ensureDirectoryExists(filePath.getParent()); + fileOperationHelper.ensureDirectoryExists(filePath.getParent()); try (ExcelWriter excelWriter = EasyExcel.write(filePath.toFile()).build()) { for (ExcelSheetData sheetData : sheetsData) { @@ -113,7 +113,7 @@ public class OutputService { // ==================== 文件名生成相关方法 ==================== public String generateOutputFileName(String prefix, String extension) { - return fileOperationService.generateFileName(prefix, extension); + return fileOperationHelper.generateFileName(prefix, extension); } // ==================== 内部数据类 ==================== diff --git a/src/main/java/com/example/workutils/system/tool/Tool.java b/src/main/java/com/example/workutils/system/tool/Tool.java deleted file mode 100644 index e89e8d9..0000000 --- a/src/main/java/com/example/workutils/system/tool/Tool.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.example.workutils.system.tool; - -import com.example.workutils.system.support.ConsoleHelper; - -import java.util.Scanner; - -/** - * 工具策略接口 - * 所有可插拔工具实现此接口,Spring 自动发现 Tool Bean - *

    - * 提供交互式菜单(execute)和命令行(executeCommand)两种执行模式。 - * 交互模式由默认 execute() 模板方法驱动,子类实现 handleChoice() 定义选择逻辑。 - */ -public interface Tool { - - /** 工具唯一标识(如 "scan", "mybatis") */ - String getId(); - - /** 菜单显示名称(如 "关键词扫描", "MyBatis SQL导出") */ - String getName(); - - /** 子菜单选项,返回空数组表示无子菜单 */ - String[] getMenuOptions(); - - /** 命令行命令名(如 "sql", "scan"),返回空字符串表示不支持命令行模式 */ - default String getCommandName() { - return ""; - } - - // ==================== 交互模式(模板方法) ==================== - - default void execute(Scanner scanner) { - while (true) { - ConsoleHelper.printMenuTitle(getName()); - ConsoleHelper.displayMenu(getMenuOptions()); - - int choice = ConsoleHelper.readChoice(scanner); - scanner.nextLine(); - - if (choice == 0) { - return; - } - - try { - handleChoice(choice, scanner); - } catch (Exception e) { - ConsoleHelper.printError(e.getMessage()); - } - } - } - - /** 处理子菜单选择,子类实现具体分发逻辑 */ - default void handleChoice(int choice, Scanner scanner) throws Exception { - } - - // ==================== 命令行模式 ==================== - - default void executeCommand(String... args) throws Exception { - System.out.println("工具 [" + getName() + "] 不支持命令行模式"); - } - - // ==================== 控制台输出辅助方法 ==================== - - default void printMenuTitle(String title) { - ConsoleHelper.printMenuTitle(title); - } - - default void displayMenu(String[] options) { - ConsoleHelper.displayMenu(options); - } - - default void printSuccess(String operation) { - ConsoleHelper.printSuccess(operation); - } - - default void printError(String message) { - ConsoleHelper.printError(message); - } - - default void printInvalidOption() { - ConsoleHelper.printInvalidOption(); - } -} diff --git a/项目目录结构说明.md b/项目目录结构说明.md index 8f9b219..238a017 100644 --- a/项目目录结构说明.md +++ b/项目目录结构说明.md @@ -45,19 +45,18 @@ src/main/java/com/example/workutils/ │ │ ├── ConsoleHelper.java # 控制台交互工具类 │ │ ├── FileOperationService.java# 文件操作服务(读取/扫描/写入/路径管理) │ │ └── OutputService.java # 输出服务(CSV/Excel) -│ ├── tool/ # Tool 基础契约 -│ │ └── Tool.java # 工具基础接口(Plugin 继承此接口) +│ ├── plugin/ # Tool 基础契约 +│ │ └── Tool.java # 工具接口(合并了原 Plugin,各 biz 模块直接实现) │ └── plugin/ # 插件化框架 -│ ├── Plugin.java # 插件接口(扩展 Tool,增加生命周期) │ ├── PluginDescriptor.java # 插件描述符(id/name/version/description/dependencies) │ ├── PluginContext.java # 插件运行时上下文 │ ├── PluginManager.java # 插件管理器(生命周期 + 注册 + 主菜单 + 命令分发) │ └── PluginState.java # 插件状态枚举(UNLOADED/LOADED/ACTIVE/DISABLED/ERROR) -└── biz/ # 业务插件层(各模块是独立 Plugin) +└── biz/ # 业务插件层(各模块实现 Tool 接口) ├── batch/ # 批量处理插件 - │ └── BatchTool.java # 批量检查入口(实现 Plugin) + │ └── BatchTool.java # 批量检查入口(实现 Tool) ├── git/ # Git 操作插件 - │ ├── GitTool.java # Git 插件入口(实现 Plugin) + │ ├── GitTool.java # Git 插件入口(实现 Tool) │ └── service/ # 服务实现层 │ ├── GitService.java # Git 服务接口 │ ├── GitServiceImpl.java # Git 服务实现 @@ -76,20 +75,20 @@ src/main/java/com/example/workutils/ │ ├── CheckBranchChangesStrategy.java │ └── CheckoutAndPullStrategy.java ├── mybatisexport/ # MyBatis SQL导出插件 - │ ├── MyBatisExportTool.java # SQL 导出入口(实现 Plugin) + │ ├── MyBatisExportTool.java # SQL 导出入口(实现 Tool) │ └── service/ │ ├── MyBatisExportService.java # SQL 导出服务 │ └── model/ │ └── MybatisTag.java # MyBatis 标签数据对象 ├── mybatisgenerator/ # MyBatis逆向工程插件 - │ ├── MyBatisGeneratorTool.java # 逆向工程入口(实现 Plugin) + │ ├── MyBatisGeneratorTool.java # 逆向工程入口(实现 Tool) │ └── service/generator/ │ ├── MyBatisGeneratorService.java # 逆向工程服务 │ ├── CustomCommentGenerator.java # 自定义注释生成器 │ ├── LombokPlugin.java # Lombok 插件(@Data) │ └── SelectiveOnlyPlugin.java # 只保留 Selective 方法插件 └── scanner/ # 扫描插件 - ├── ScannerTool.java # 扫描工具入口(实现 Plugin) + ├── ScannerTool.java # 扫描工具入口(实现 Tool) └── service/ # 服务实现层 ├── ScannerService.java # 扫描服务 └── config/ @@ -117,17 +116,17 @@ src/main/java/com/example/workutils/ | 文件 | 作用 | |------|------| -| `Plugin.java` | **插件接口**,继承 `Tool`,增加生命周期方法 `onLoad(PluginContext)` / `onUnload()`,`getId()`/`getName()` 默认委托给 `PluginDescriptor` | +| `Tool.java` | **工具接口**,合并了原 Plugin,包含生命周期方法 `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` | -#### 3.2.2 Tool 基础契约(`system/tool/`) +#### 3.2.2 Tool 基础契约(`system/plugin/`) #### `Tool.java` -- **作用**: 工具基础接口,定义 `getId()`、`getName()`、`getMenuOptions()`、`execute()`、`handleChoice()`、`getCommandName()`、`executeCommand()` 等方法 -- **关系**: `Plugin` 继承 `Tool`,biz 模块直接实现 `Plugin` +- **作用**: 工具接口,定义 `getDescriptor()`、`getId()`、`getName()`、`getMenuOptions()`、`execute()`、`handleChoice()`、`getCommandName()`、`executeCommand()`、`onLoad(PluginContext)`、`onUnload()` 等方法 +- **关系**: biz 模块直接实现 `Tool` #### 3.2.3 共享基础设施(`system/support/`) @@ -147,13 +146,13 @@ src/main/java/com/example/workutils/ ### 3.3 业务插件层(`biz/`) -> **架构约定**: 每个 biz 模块的 Tool 类放在模块根目录,实现 `Plugin` 接口(继承自 `Tool`)。通过静态 `PluginDescriptor` 声明插件元数据(id/name/version/description)。所有服务实现及附属文件收在 `service/` 子包下,内部细节用 `internal/` 隔离。 +> **架构约定**: 每个 biz 模块的 Tool 类放在模块根目录,实现 `Tool` 接口。通过静态 `PluginDescriptor` 声明插件元数据(id/name/version/description)。所有服务实现及附属文件收在 `service/` 子包下,内部细节用 `internal/` 隔离。 #### 3.3.1 扫描插件(`biz/scanner/`) | 文件 | 作用 | |------|------| -| `ScannerTool.java` | 扫描插件入口(实现 Plugin),版本 `1.0.0`,负责菜单交互,委托 ScannerService 执行业务逻辑 | +| `ScannerTool.java` | 扫描插件入口(实现 Tool),版本 `1.0.0`,负责菜单交互,委托 ScannerService 执行业务逻辑 | | `service/ScannerService.java` | 扫描服务,支持关键词扫描、Redis Key 扫描、Dubbo 接口扫描三种类型,支持并发多项目扫描 | | `service/config/ScanConfig.java` | 扫描配置,定义文件扩展名、排除目录、最大文件大小等参数 | @@ -161,7 +160,7 @@ src/main/java/com/example/workutils/ | 文件 | 作用 | |------|------| -| `MyBatisExportTool.java` | SQL 导出插件入口(实现 Plugin),版本 `1.0.0`,负责菜单交互 | +| `MyBatisExportTool.java` | SQL 导出插件入口(实现 Tool),版本 `1.0.0`,负责菜单交互 | | `service/MyBatisExportService.java` | SQL 导出核心服务,从 XML 文件中提取 select/insert/update/delete 标签 | | `service/model/MybatisTag.java` | MyBatis 标签数据对象 | @@ -169,7 +168,7 @@ src/main/java/com/example/workutils/ | 文件 | 作用 | |------|------| -| `MyBatisGeneratorTool.java` | 逆向工程插件入口(实现 Plugin),版本 `1.0.0` | +| `MyBatisGeneratorTool.java` | 逆向工程插件入口(实现 Tool),版本 `1.0.0` | | `service/generator/MyBatisGeneratorService.java` | MyBatis 逆向工程服务 | | `service/generator/CustomCommentGenerator.java` | 自定义注释生成器 | | `service/generator/LombokPlugin.java` | Lombok 插件(@Data) | @@ -179,7 +178,7 @@ src/main/java/com/example/workutils/ | 文件 | 作用 | |------|------| -| `GitTool.java` | Git 插件入口(实现 Plugin),版本 `1.0.0` | +| `GitTool.java` | Git 插件入口(实现 Tool),版本 `1.0.0` | | `service/GitService.java` | Git 服务接口 | | `service/GitServiceImpl.java` | Git 服务实现 | | `service/config/GitOptimizationConfig.java` | Git 性能优化配置 | @@ -194,7 +193,7 @@ src/main/java/com/example/workutils/ | 文件 | 作用 | |------|------| -| `BatchTool.java` | 批量处理插件入口(实现 Plugin),版本 `1.0.0`,一键执行多种检查 | +| `BatchTool.java` | 批量处理插件入口(实现 Tool),版本 `1.0.0`,一键执行多种检查 | --- @@ -224,8 +223,8 @@ src/main/resources/ | 模式/特点 | 应用位置 | 说明 | |-----------|---------|------| -| **插件架构** | `Plugin` + `PluginManager` + `PluginDescriptor` | 每个 biz 模块作为独立 Plugin,声明版本、描述,PluginManager 统一管理生命周期(load/enable/disable),可插拔扩展 | -| **策略模式** | `Tool` / `Plugin` 接口 + 各 biz 实现 | 各插件通过实现 Plugin 接口注册到 PluginManager,支持交互和命令行双模式 | +| **插件架构** | `Tool` + `PluginManager` + `PluginDescriptor` | 每个 biz 模块作为独立 Tool,声明版本、描述,PluginManager 统一管理生命周期(load/enable/disable),可插拔扩展 | +| **策略模式** | `Tool` 接口 + 各 biz 实现 | 各插件通过实现 Tool 接口注册到 PluginManager,支持交互和命令行双模式 | | **策略模式** | `GitCommandStrategy` + 各 Strategy 实现 | Git 命令执行策略,`GitStrategyFactory` 管理注册 | | **工厂模式** | `PluginManager`、`GitStrategyFactory` | Spring 自动发现和注入 Bean | | **命令模式** | `GitCommand` + `GitCommandController` | 封装 Git 命令的异步执行、中断、跳过 | @@ -235,7 +234,7 @@ src/main/resources/ | **模板回调** | `FileOperationService.scanFilesRecursively` | 使用 `SimpleFileVisitor` 实现文件遍历回调 | | **分层架构** | `system/` vs `biz/` | system 定义平台契约与共享基础设施,biz 实现具体插件 | | **内部隔离** | `biz/*/service/internal/` | 内部实现类用 `internal/` 隔离,不对外暴露 | -| **生命周期管理** | `Plugin.onLoad()` / `Plugin.onUnload()` | 插件加载/卸载回调,PluginContext 传递运行时上下文 | +| **生命周期管理** | `Tool.onLoad()` / `Tool.onUnload()` | 插件加载/卸载回调,PluginContext 传递运行时上下文 | --- @@ -260,7 +259,7 @@ WorkUtilsApplication (入口) 插件加载链路: PluginManager.init() - ├── 遍历 Spring 中所有 Plugin Bean + ├── 遍历 Spring 中所有 Tool Bean ├── 依次调用 plugin.onLoad(PluginContext) ├── 按 ToolsConfig 过滤启用/禁用 └── 构建 pluginByIdMap / pluginByCommandMap / pluginStates @@ -318,9 +317,7 @@ WorkUtilsApplication (入口) ### 8.2 接口层次 ``` -Tool (system/tool/) ← 基础契约:getId/getName/getMenuOptions/execute/... - ↑ -Plugin (system/plugin/) ← 扩展契约:+getDescriptor/onLoad/onUnload +Tool (system/plugin/) ← 统一契约:getDescriptor/onLoad/onUnload/getMenuOptions/execute/... ↑ 各 biz 模块 ← 具体实现:@Component + PluginDescriptor ``` @@ -352,7 +349,7 @@ UNLOADED ──load──▶ LOADED ──enable──▶ ACTIVE ──disable 当前已完成插件化基础框架搭建,后续可进一步实现: -1. **SPI 自动发现** — 通过 `META-INF/services` 或 Spring Factories 自动发现外部 jar 中的 Plugin 实现 +1. **SPI 自动发现** — 通过 `META-INF/services` 或 Spring Factories 自动发现外部 jar 中的 Tool 实现 2. **独立打包** — 每个 biz 模块打成独立 jar,`PluginManager` 动态加载 3. **热加载** — 基于自定义 ClassLoader 实现插件的动态加载/卸载/更新 4. **依赖注入隔离** — 每个插件使用独立 Spring 子容器(`ApplicationContext`),避免 Bean 冲突