代码优化提交

This commit is contained in:
张家训
2026-06-09 14:38:36 +08:00
parent b964fef4dd
commit c5773063d7
16 changed files with 159 additions and 182 deletions
@@ -41,19 +41,18 @@ public class WorkUtilsApplication implements CommandLineRunner {
} }
/** /**
* 命令行模式:通过 PluginManager 按命令名分发到对应 Plugin * 命令行模式:通过 PluginManager 按命令名分发到对应 Tool
* 启动类不依赖任何具体业务 Service * 启动类不依赖任何具体业务 Service
*/ */
private void processCommand(String... args) throws Exception { private void processCommand(String... args) throws Exception {
String command = args[0].toLowerCase(); String command = args[0].toLowerCase();
Optional<Plugin> pluginOpt = pluginManager.getPluginByCommand(command); Optional<Plugin> toolOpt = pluginManager.getPluginByCommand(command);
if (pluginOpt.isPresent()) { if (toolOpt.isPresent()) {
Plugin plugin = pluginOpt.get(); Plugin plugin = toolOpt.get();
try { try {
// args[0]是命令名,从args[1]开始传给plugin String[] toolArgs = args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0];
String[] pluginArgs = args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]; plugin.executeCommand(toolArgs);
plugin.executeCommand(pluginArgs);
} catch (Exception e) { } catch (Exception e) {
log.error("命令执行失败: {} [{}]", plugin.getName(), plugin.getId(), e); log.error("命令执行失败: {} [{}]", plugin.getName(), plugin.getId(), e);
System.err.println("命令执行失败: " + e.getMessage()); System.err.println("命令执行失败: " + e.getMessage());
@@ -13,13 +13,14 @@ import java.util.List;
import java.util.Scanner; import java.util.Scanner;
/** /**
* 批量处理插件(实现 Plugin 接口) * 批量处理插件(实现 Tool 接口)
* 仅负责菜单展示和用户交互,业务逻辑委托给各Service * 仅负责菜单展示和用户交互,业务逻辑委托给各Service
*/ */
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class BatchTool implements Plugin { public class BatchPlugin implements Plugin {
@FunctionalInterface @FunctionalInterface
public interface ExceptionRunnable { public interface ExceptionRunnable {
@@ -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.GitService;
import com.example.workutils.biz.git.service.internal.GitCommandExecutor; 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.PluginDescriptor;
import com.example.workutils.system.plugin.Plugin;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -14,12 +14,12 @@ import java.util.Scanner;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* Git仓库健康检测插件(实现 Plugin 接口) * Git仓库健康检测插件(实现 Tool 接口)
*/ */
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class GitTool implements Plugin { public class GitPlugin implements Plugin {
private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder() private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder()
.id("git-health") .id("git-health")
@@ -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.GitCommandExecutor;
import com.example.workutils.biz.git.service.internal.strategy.GitStrategyFactory; import com.example.workutils.biz.git.service.internal.strategy.GitStrategyFactory;
import com.example.workutils.biz.git.service.model.GitCommand; 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -26,7 +26,7 @@ public class GitServiceImpl implements GitService {
private final GitStrategyFactory strategyFactory; private final GitStrategyFactory strategyFactory;
@Autowired @Autowired
private FileOperationService fileOperationService; private FileOperationHelper fileOperationHelper;
private static final String DEFAULT_BRANCH = "master"; private static final String DEFAULT_BRANCH = "master";
@@ -52,10 +52,10 @@ public class GitServiceImpl implements GitService {
@Override @Override
public List<GitCommandExecutor.RepoHealthResult> checkCorruptedRepos(Scanner scanner) { public List<GitCommandExecutor.RepoHealthResult> checkCorruptedRepos(Scanner scanner) {
List<GitCommandExecutor.RepoHealthResult> corruptedRepos = new ArrayList<>(); List<GitCommandExecutor.RepoHealthResult> corruptedRepos = new ArrayList<>();
List<FileOperationService.ProjectPathInfo> projects; List<FileOperationHelper.ProjectPathInfo> projects;
try { try {
projects = fileOperationService.getProjectPathsWithStatus(); projects = fileOperationHelper.getProjectPathsWithStatus();
} catch (IOException e) { } catch (IOException e) {
log.error("读取项目路径配置文件失败", e); log.error("读取项目路径配置文件失败", e);
return corruptedRepos; return corruptedRepos;
@@ -68,7 +68,7 @@ public class GitServiceImpl implements GitService {
int totalCount = projects.size(); int totalCount = projects.size();
boolean isAborted = false; boolean isAborted = false;
for (FileOperationService.ProjectPathInfo project : projects) { for (FileOperationHelper.ProjectPathInfo project : projects) {
String repoPath = project.getPath(); String repoPath = project.getPath();
String repoName = extractProjectName(repoPath); String repoName = extractProjectName(repoPath);
@@ -80,7 +80,7 @@ public class GitServiceImpl implements GitService {
if (healthResult.isHealthy()) { if (healthResult.isHealthy()) {
try { try {
fileOperationService.updateProjectStatus(repoPath, FileOperationService.ProjectStatus.NORMAL); fileOperationHelper.updateProjectStatus(repoPath, FileOperationHelper.ProjectStatus.NORMAL);
System.out.printf("[%d/%d] %s - 正常%n", checkedCount, totalCount, repoName); System.out.printf("[%d/%d] %s - 正常%n", checkedCount, totalCount, repoName);
} catch (IOException e) { } catch (IOException e) {
log.warn("更新配置文件失败: {}", repoPath, e); log.warn("更新配置文件失败: {}", repoPath, e);
@@ -88,7 +88,7 @@ public class GitServiceImpl implements GitService {
} else { } else {
corruptedRepos.add(healthResult); corruptedRepos.add(healthResult);
try { 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()); System.out.printf("[%d/%d] %s - 损坏: %s%n", checkedCount, totalCount, repoName, healthResult.getCorruptionReason());
} catch (IOException e) { } catch (IOException e) {
log.warn("更新配置文件失败: {}", repoPath, e); log.warn("更新配置文件失败: {}", repoPath, e);
@@ -143,7 +143,7 @@ public class GitServiceImpl implements GitService {
if (result.isSuccess()) { if (result.isSuccess()) {
log.info("仓库修复成功: {}", repoPath); log.info("仓库修复成功: {}", repoPath);
try { try {
fileOperationService.updateProjectStatus(repoPath, FileOperationService.ProjectStatus.NORMAL); fileOperationHelper.updateProjectStatus(repoPath, FileOperationHelper.ProjectStatus.NORMAL);
} catch (IOException e) { } catch (IOException e) {
log.warn("更新配置文件失败: {}", repoPath, e); log.warn("更新配置文件失败: {}", repoPath, e);
} }
@@ -157,9 +157,9 @@ public class GitServiceImpl implements GitService {
@Override @Override
public List<String> listCorruptedRepos() { public List<String> listCorruptedRepos() {
try { try {
return fileOperationService.getProjectPathsWithStatus().stream() return fileOperationHelper.getProjectPathsWithStatus().stream()
.filter(p -> p.getStatus() == FileOperationService.ProjectStatus.CORRUPTED) .filter(p -> p.getStatus() == FileOperationHelper.ProjectStatus.CORRUPTED)
.map(FileOperationService.ProjectPathInfo::getPath) .map(FileOperationHelper.ProjectPathInfo::getPath)
.collect(Collectors.toList()); .collect(Collectors.toList());
} catch (IOException e) { } catch (IOException e) {
log.error("读取损坏仓库列表失败", e); log.error("读取损坏仓库列表失败", e);
@@ -1,8 +1,8 @@
package com.example.workutils.biz.mybatisexport; package com.example.workutils.biz.mybatisexport;
import com.example.workutils.biz.mybatisexport.service.MyBatisExportService; 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.PluginDescriptor;
import com.example.workutils.system.plugin.Plugin;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -11,12 +11,12 @@ import java.util.Arrays;
import java.util.Scanner; import java.util.Scanner;
/** /**
* MyBatis SQL导出插件(实现 Plugin 接口) * MyBatis SQL导出插件(实现 Tool 接口)
*/ */
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class MyBatisExportTool implements Plugin { public class MyBatisExportPlugin implements Plugin {
private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder() private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder()
.id("mybatisexport") .id("mybatisexport")
@@ -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.mybatisexport.service.model.MybatisTag;
import com.example.workutils.biz.scanner.service.config.ScanConfig; 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 com.example.workutils.system.support.OutputService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -25,7 +25,7 @@ import java.util.regex.Pattern;
public class MyBatisExportService { public class MyBatisExportService {
@Autowired @Autowired
private FileOperationService fileOperationService; private FileOperationHelper fileOperationHelper;
@Autowired @Autowired
private OutputService outputService; private OutputService outputService;
@@ -78,7 +78,7 @@ public class MyBatisExportService {
private void exportWithFilter(String fileNamePrefix, SqlFilter filter, private void exportWithFilter(String fileNamePrefix, SqlFilter filter,
ProcessStrategy processStrategy) throws Exception { ProcessStrategy processStrategy) throws Exception {
List<String> projectPaths = fileOperationService.getProjectPaths(); List<String> projectPaths = fileOperationHelper.getProjectPaths();
if (projectPaths.isEmpty()) { if (projectPaths.isEmpty()) {
log.warn("未找到项目路径"); log.warn("未找到项目路径");
return; return;
@@ -108,10 +108,10 @@ public class MyBatisExportService {
private List<String[]> processProject(String projectPath, SqlFilter filter, private List<String[]> processProject(String projectPath, SqlFilter filter,
ProcessStrategy processStrategy) throws Exception { ProcessStrategy processStrategy) throws Exception {
List<String[]> records = new ArrayList<>(); List<String[]> records = new ArrayList<>();
String projectName = fileOperationService.extractProjectName(projectPath); String projectName = fileOperationHelper.extractProjectName(projectPath);
ScanConfig config = ScanConfig.forXmlFiles(); ScanConfig config = ScanConfig.forXmlFiles();
List<Path> xmlFiles = fileOperationService.scanFiles(projectPath, config); List<Path> xmlFiles = fileOperationHelper.scanFiles(projectPath, config);
for (Path xmlFile : xmlFiles) { for (Path xmlFile : xmlFiles) {
try { try {
@@ -134,7 +134,7 @@ public class MyBatisExportService {
private List<MybatisTag> parseXmlFile(Path xmlFile) throws Exception { private List<MybatisTag> parseXmlFile(Path xmlFile) throws Exception {
List<MybatisTag> tags = new ArrayList<>(); List<MybatisTag> tags = new ArrayList<>();
String content = fileOperationService.readFileContent(xmlFile); String content = fileOperationHelper.readFileContent(xmlFile);
for (String tagType : SQL_TAG_TYPES) { for (String tagType : SQL_TAG_TYPES) {
tags.addAll(parseSqlTags(content, tagType)); tags.addAll(parseSqlTags(content, tagType));
@@ -12,12 +12,12 @@ import java.util.List;
import java.util.Scanner; import java.util.Scanner;
/** /**
* MyBatis逆向工程插件(实现 Plugin 接口) * MyBatis逆向工程插件(实现 Tool 接口)
*/ */
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class MyBatisGeneratorTool implements Plugin { public class MyBatisGeneratorPlugin implements Plugin {
private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder() private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder()
.id("mybatisgenerator") .id("mybatisgenerator")
@@ -1,8 +1,8 @@
package com.example.workutils.biz.scanner; package com.example.workutils.biz.scanner;
import com.example.workutils.biz.scanner.service.ScannerService; 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.PluginDescriptor;
import com.example.workutils.system.plugin.Plugin;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -10,12 +10,12 @@ import org.springframework.stereotype.Component;
import java.util.Scanner; import java.util.Scanner;
/** /**
* 扫描工具插件(实现 Plugin 接口) * 扫描工具插件(实现 Tool 接口)
*/ */
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class ScannerTool implements Plugin { public class ScannerPlugin implements Plugin {
private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder() private static final PluginDescriptor DESCRIPTOR = PluginDescriptor.builder()
.id("scan") .id("scan")
@@ -2,7 +2,7 @@ package com.example.workutils.biz.scanner.service;
import com.example.workutils.biz.scanner.service.config.ScanConfig; import com.example.workutils.biz.scanner.service.config.ScanConfig;
import com.example.workutils.system.config.WorkUtilsProperties; 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 com.example.workutils.system.support.OutputService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -24,14 +24,14 @@ public class ScannerService {
private final ExecutorService executorService = Executors.newFixedThreadPool(10); private final ExecutorService executorService = Executors.newFixedThreadPool(10);
@Autowired @Autowired
private FileOperationService fileOperationService; private FileOperationHelper fileOperationHelper;
@Autowired @Autowired
private OutputService outputService; private OutputService outputService;
@Autowired @Autowired
private WorkUtilsProperties properties; private WorkUtilsProperties properties;
public ScanResult scanAllProjects(ScanType scanType, String pattern) throws Exception { public ScanResult scanAllProjects(ScanType scanType, String pattern) throws Exception {
List<String> projectPaths = fileOperationService.getProjectPaths(); List<String> projectPaths = fileOperationHelper.getProjectPaths();
if (projectPaths.isEmpty()) { if (projectPaths.isEmpty()) {
log.warn("No project paths found"); log.warn("No project paths found");
return new ScanResult(); return new ScanResult();
@@ -63,10 +63,10 @@ public class ScannerService {
private ProjectScanResult scanProject(String projectPath, ScanType scanType, String pattern) throws Exception { private ProjectScanResult scanProject(String projectPath, ScanType scanType, String pattern) throws Exception {
ProjectScanResult projectResult = new ProjectScanResult(projectPath, new ArrayList<>()); ProjectScanResult projectResult = new ProjectScanResult(projectPath, new ArrayList<>());
String projectName = fileOperationService.extractProjectName(projectPath); String projectName = fileOperationHelper.extractProjectName(projectPath);
ScanConfig config = getConfigForScanType(scanType); ScanConfig config = getConfigForScanType(scanType);
List<Path> files = fileOperationService.scanFiles(projectPath, config); List<Path> files = fileOperationHelper.scanFiles(projectPath, config);
for (Path file : files) { for (Path file : files) {
try { try {
@@ -90,7 +90,7 @@ public class ScannerService {
List<MatchResult> matches = new ArrayList<>(); List<MatchResult> matches = new ArrayList<>();
List<String> lines; List<String> lines;
try { try {
lines = fileOperationService.readLinesFromFile(file); lines = fileOperationHelper.readLinesFromFile(file);
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to read file: {}, skipping this file", file, e); log.warn("Failed to read file: {}, skipping this file", file, e);
return matches; return matches;
@@ -185,7 +185,7 @@ public class ScannerService {
} }
} }
fileOperationService.ensureDirectoryExists(properties.getTodayExportPath()); fileOperationHelper.ensureDirectoryExists(properties.getTodayExportPath());
outputService.writeRecordsToCsv(records, outputFileName); outputService.writeRecordsToCsv(records, outputFileName);
log.info("Scan results exported to: {}", outputFileName); log.info("Scan results exported to: {}", outputFileName);
} }
@@ -1,24 +1,77 @@
package com.example.workutils.system.plugin; 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 模块的顶级契约
* <p> * <p>
* 继承 {@link Tool},在其基础上增加插件生命周期管理: * 提供:
* <ul> * <ul>
* <li>{@link #getDescriptor()} — 提供插件元数据(id/name/version/description/dependencies)</li> * <li>插件元数据 — {@link #getDescriptor()} 声明 id/name/version/description</li>
* <li>{@link #onLoad(PluginContext)} — 插件加载时回调,注入运行时上下文</li> * <li>生命周期 — {@link #onLoad(PluginContext)} / {@link #onUnload()}</li>
* <li>{@link #onUnload()} — 插件卸载时回调,清理资源</li> * <li>交互模式 — {@link #execute(Scanner)} 模板方法驱动,子类实现 {@link #handleChoice(int, Scanner)}</li>
* <li>命令行模式 — {@link #getCommandName()} + {@link #executeCommand(String...)}</li>
* </ul> * </ul>
* <p>
* getId()/getName() 默认委托给 PluginDescriptor,实现类只需提供 getDescriptor()。
*/ */
public interface Plugin extends Tool { public interface Plugin {
/** 插件描述符(元数据) */ /** 插件描述符(元数据) */
PluginDescriptor getDescriptor(); 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 在注册时调用 */ /** 插件加载回调,PluginManager 在注册时调用 */
default void onLoad(PluginContext context) { default void onLoad(PluginContext context) {
} }
@@ -27,15 +80,25 @@ public interface Plugin extends Tool {
default void onUnload() { default void onUnload() {
} }
// ==================== 默认委托 ==================== // ==================== 控制台输出辅助方法 ====================
@Override default void printMenuTitle(String title) {
default String getId() { ConsoleHelper.printMenuTitle(title);
return getDescriptor().getId();
} }
@Override default void displayMenu(String[] options) {
default String getName() { ConsoleHelper.displayMenu(options);
return getDescriptor().getName(); }
default void printSuccess(String operation) {
ConsoleHelper.printSuccess(operation);
}
default void printError(String message) {
ConsoleHelper.printError(message);
}
default void printInvalidOption() {
ConsoleHelper.printInvalidOption();
} }
} }
@@ -6,7 +6,7 @@ import java.util.List;
/** /**
* 插件描述符(不可变元数据) * 插件描述符(不可变元数据)
* <p> * <p>
* 每个 Plugin 通过 {@link Plugin#getDescriptor()} 返回此描述符, * 每个插件通过 {@link Plugin#getDescriptor()} 返回此描述符,
* PluginManager 据此识别、管理、展示插件。 * PluginManager 据此识别、管理、展示插件。
*/ */
public class PluginDescriptor { public class PluginDescriptor {
@@ -11,7 +11,7 @@ import java.util.*;
/** /**
* 插件管理器 —— 插件生命周期管理 + 注册中心 + 主菜单 * 插件管理器 —— 插件生命周期管理 + 注册中心 + 主菜单
* <p> * <p>
* 替换旧版 {@code ToolRegistry},统一管理所有 {@link Plugin} 的: * 统一管理所有 {@link Plugin} 实例的:
* <ul> * <ul>
* <li>生命周期 — load / unload / enable / disable</li> * <li>生命周期 — load / unload / enable / disable</li>
* <li>注册发现 — 按 id / command 查找</li> * <li>注册发现 — 按 id / command 查找</li>
@@ -21,7 +21,7 @@ import java.util.stream.Stream;
*/ */
@Service @Service
@Slf4j @Slf4j
public class FileOperationService { public class FileOperationHelper {
@Autowired @Autowired
private WorkUtilsProperties properties; private WorkUtilsProperties properties;
@@ -22,10 +22,10 @@ import java.util.List;
*/ */
@Service @Service
@Slf4j @Slf4j
public class OutputService { public class OutputHelper {
@Autowired @Autowired
private FileOperationService fileOperationService; private FileOperationHelper fileOperationHelper;
@Autowired @Autowired
private WorkUtilsProperties properties; private WorkUtilsProperties properties;
@@ -39,7 +39,7 @@ public class OutputService {
} else { } else {
filePath = properties.resolveExportPath(outputPath); filePath = properties.resolveExportPath(outputPath);
} }
fileOperationService.ensureDirectoryExists(filePath.getParent()); fileOperationHelper.ensureDirectoryExists(filePath.getParent());
try (CSVWriter writer = new CSVWriter(new FileWriter(filePath.toFile()))) { try (CSVWriter writer = new CSVWriter(new FileWriter(filePath.toFile()))) {
if (headers.length > 0) { if (headers.length > 0) {
@@ -62,9 +62,9 @@ public class OutputService {
public void appendToCsv(String[] record, String outputPath, String... headers) throws IOException { public void appendToCsv(String[] record, String outputPath, String... headers) throws IOException {
Path filePath = properties.resolveExportPath(outputPath); 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; boolean shouldWriteHeaders = !fileExists && headers.length > 0;
try (CSVWriter writer = new CSVWriter(new FileWriter(filePath.toFile(), true))) { try (CSVWriter writer = new CSVWriter(new FileWriter(filePath.toFile(), true))) {
@@ -82,21 +82,21 @@ public class OutputService {
public <T> void writeToExcel(List<T> data, String outputPath, Class<T> modelClass) throws IOException { public <T> void writeToExcel(List<T> data, String outputPath, Class<T> modelClass) throws IOException {
Path filePath = properties.resolveExportPath(outputPath); Path filePath = properties.resolveExportPath(outputPath);
fileOperationService.ensureDirectoryExists(filePath.getParent()); fileOperationHelper.ensureDirectoryExists(filePath.getParent());
EasyExcel.write(filePath.toFile(), modelClass).sheet().doWrite(data); EasyExcel.write(filePath.toFile(), modelClass).sheet().doWrite(data);
log.info("Excel文件写入成功: {}", filePath); log.info("Excel文件写入成功: {}", filePath);
} }
public <T> void writeToExcel(List<T> data, String outputPath, String sheetName, Class<T> modelClass) throws IOException { public <T> void writeToExcel(List<T> data, String outputPath, String sheetName, Class<T> modelClass) throws IOException {
Path filePath = properties.resolveExportPath(outputPath); Path filePath = properties.resolveExportPath(outputPath);
fileOperationService.ensureDirectoryExists(filePath.getParent()); fileOperationHelper.ensureDirectoryExists(filePath.getParent());
EasyExcel.write(filePath.toFile(), modelClass).sheet(sheetName).doWrite(data); EasyExcel.write(filePath.toFile(), modelClass).sheet(sheetName).doWrite(data);
log.info("Excel文件写入成功: {}", filePath); log.info("Excel文件写入成功: {}", filePath);
} }
public <T> void writeToExcelWithMultipleSheets(List<ExcelSheetData<T>> sheetsData, String outputPath) throws IOException { public <T> void writeToExcelWithMultipleSheets(List<ExcelSheetData<T>> sheetsData, String outputPath) throws IOException {
Path filePath = properties.resolveExportPath(outputPath); Path filePath = properties.resolveExportPath(outputPath);
fileOperationService.ensureDirectoryExists(filePath.getParent()); fileOperationHelper.ensureDirectoryExists(filePath.getParent());
try (ExcelWriter excelWriter = EasyExcel.write(filePath.toFile()).build()) { try (ExcelWriter excelWriter = EasyExcel.write(filePath.toFile()).build()) {
for (ExcelSheetData<T> sheetData : sheetsData) { for (ExcelSheetData<T> sheetData : sheetsData) {
@@ -113,7 +113,7 @@ public class OutputService {
// ==================== 文件名生成相关方法 ==================== // ==================== 文件名生成相关方法 ====================
public String generateOutputFileName(String prefix, String extension) { public String generateOutputFileName(String prefix, String extension) {
return fileOperationService.generateFileName(prefix, extension); return fileOperationHelper.generateFileName(prefix, extension);
} }
// ==================== 内部数据类 ==================== // ==================== 内部数据类 ====================
@@ -1,83 +0,0 @@
package com.example.workutils.system.tool;
import com.example.workutils.system.support.ConsoleHelper;
import java.util.Scanner;
/**
* 工具策略接口
* 所有可插拔工具实现此接口,Spring 自动发现 Tool Bean
* <p>
* 提供交互式菜单(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();
}
}
+24 -27
View File
@@ -45,19 +45,18 @@ src/main/java/com/example/workutils/
│ │ ├── ConsoleHelper.java # 控制台交互工具类 │ │ ├── ConsoleHelper.java # 控制台交互工具类
│ │ ├── FileOperationService.java# 文件操作服务(读取/扫描/写入/路径管理) │ │ ├── FileOperationService.java# 文件操作服务(读取/扫描/写入/路径管理)
│ │ └── OutputService.java # 输出服务(CSV/Excel) │ │ └── OutputService.java # 输出服务(CSV/Excel)
│ ├── tool/ # Tool 基础契约 │ ├── plugin/ # Tool 基础契约
│ │ └── Tool.java # 工具基础接口(Plugin 继承此接口) │ │ └── Tool.java # 工具接口(合并了原 Plugin,各 biz 模块直接实现)
│ └── plugin/ # 插件化框架 │ └── plugin/ # 插件化框架
│ ├── Plugin.java # 插件接口(扩展 Tool,增加生命周期)
│ ├── PluginDescriptor.java # 插件描述符(id/name/version/description/dependencies) │ ├── PluginDescriptor.java # 插件描述符(id/name/version/description/dependencies)
│ ├── PluginContext.java # 插件运行时上下文 │ ├── PluginContext.java # 插件运行时上下文
│ ├── PluginManager.java # 插件管理器(生命周期 + 注册 + 主菜单 + 命令分发) │ ├── PluginManager.java # 插件管理器(生命周期 + 注册 + 主菜单 + 命令分发)
│ └── PluginState.java # 插件状态枚举(UNLOADED/LOADED/ACTIVE/DISABLED/ERROR) │ └── PluginState.java # 插件状态枚举(UNLOADED/LOADED/ACTIVE/DISABLED/ERROR)
└── biz/ # 业务插件层(各模块是独立 Plugin) └── biz/ # 业务插件层(各模块实现 Tool 接口)
├── batch/ # 批量处理插件 ├── batch/ # 批量处理插件
│ └── BatchTool.java # 批量检查入口(实现 Plugin) │ └── BatchTool.java # 批量检查入口(实现 Tool)
├── git/ # Git 操作插件 ├── git/ # Git 操作插件
│ ├── GitTool.java # Git 插件入口(实现 Plugin) │ ├── GitTool.java # Git 插件入口(实现 Tool)
│ └── service/ # 服务实现层 │ └── service/ # 服务实现层
│ ├── GitService.java # Git 服务接口 │ ├── GitService.java # Git 服务接口
│ ├── GitServiceImpl.java # Git 服务实现 │ ├── GitServiceImpl.java # Git 服务实现
@@ -76,20 +75,20 @@ src/main/java/com/example/workutils/
│ ├── CheckBranchChangesStrategy.java │ ├── CheckBranchChangesStrategy.java
│ └── CheckoutAndPullStrategy.java │ └── CheckoutAndPullStrategy.java
├── mybatisexport/ # MyBatis SQL导出插件 ├── mybatisexport/ # MyBatis SQL导出插件
│ ├── MyBatisExportTool.java # SQL 导出入口(实现 Plugin) │ ├── MyBatisExportTool.java # SQL 导出入口(实现 Tool)
│ └── service/ │ └── service/
│ ├── MyBatisExportService.java # SQL 导出服务 │ ├── MyBatisExportService.java # SQL 导出服务
│ └── model/ │ └── model/
│ └── MybatisTag.java # MyBatis 标签数据对象 │ └── MybatisTag.java # MyBatis 标签数据对象
├── mybatisgenerator/ # MyBatis逆向工程插件 ├── mybatisgenerator/ # MyBatis逆向工程插件
│ ├── MyBatisGeneratorTool.java # 逆向工程入口(实现 Plugin) │ ├── MyBatisGeneratorTool.java # 逆向工程入口(实现 Tool)
│ └── service/generator/ │ └── service/generator/
│ ├── MyBatisGeneratorService.java # 逆向工程服务 │ ├── MyBatisGeneratorService.java # 逆向工程服务
│ ├── CustomCommentGenerator.java # 自定义注释生成器 │ ├── CustomCommentGenerator.java # 自定义注释生成器
│ ├── LombokPlugin.java # Lombok 插件(@Data) │ ├── LombokPlugin.java # Lombok 插件(@Data)
│ └── SelectiveOnlyPlugin.java # 只保留 Selective 方法插件 │ └── SelectiveOnlyPlugin.java # 只保留 Selective 方法插件
└── scanner/ # 扫描插件 └── scanner/ # 扫描插件
├── ScannerTool.java # 扫描工具入口(实现 Plugin) ├── ScannerTool.java # 扫描工具入口(实现 Tool)
└── service/ # 服务实现层 └── service/ # 服务实现层
├── ScannerService.java # 扫描服务 ├── ScannerService.java # 扫描服务
└── config/ └── 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 模式构建 | | `PluginDescriptor.java` | 插件描述符(不可变),包含 `id`、`name`、`version`、`description`、`dependencies`,通过 Builder 模式构建 |
| `PluginContext.java` | 插件运行时上下文,提供 `PluginManager` 引用和跨插件共享属性 | | `PluginContext.java` | 插件运行时上下文,提供 `PluginManager` 引用和跨插件共享属性 |
| `PluginManager.java` | **插件管理器**(`@Component`),负责:生命周期管理(load/enable/disable)、注册发现(按 id/command 查找)、启用过滤(白名单/黑名单)、主菜单交互循环、命令行分发 | | `PluginManager.java` | **插件管理器**(`@Component`),负责:生命周期管理(load/enable/disable)、注册发现(按 id/command 查找)、启用过滤(白名单/黑名单)、主菜单交互循环、命令行分发 |
| `PluginState.java` | 插件状态枚举:`UNLOADED` → `LOADED` → `ACTIVE` / `DISABLED` / `ERROR` | | `PluginState.java` | 插件状态枚举:`UNLOADED` → `LOADED` → `ACTIVE` / `DISABLED` / `ERROR` |
#### 3.2.2 Tool 基础契约(`system/tool/`) #### 3.2.2 Tool 基础契约(`system/plugin/`)
#### `Tool.java` #### `Tool.java`
- **作用**: 工具基础接口,定义 `getId()`、`getName()`、`getMenuOptions()`、`execute()`、`handleChoice()`、`getCommandName()`、`executeCommand()` 等方法 - **作用**: 工具接口,定义 `getDescriptor()`、`getId()`、`getName()`、`getMenuOptions()`、`execute()`、`handleChoice()`、`getCommandName()`、`executeCommand()`、`onLoad(PluginContext)`、`onUnload()` 等方法
- **关系**: `Plugin` 继承 `Tool`,biz 模块直接实现 `Plugin` - **关系**: biz 模块直接实现 `Tool`
#### 3.2.3 共享基础设施(`system/support/`) #### 3.2.3 共享基础设施(`system/support/`)
@@ -147,13 +146,13 @@ src/main/java/com/example/workutils/
### 3.3 业务插件层(`biz/`) ### 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/`) #### 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/ScannerService.java` | 扫描服务,支持关键词扫描、Redis Key 扫描、Dubbo 接口扫描三种类型,支持并发多项目扫描 |
| `service/config/ScanConfig.java` | 扫描配置,定义文件扩展名、排除目录、最大文件大小等参数 | | `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/MyBatisExportService.java` | SQL 导出核心服务,从 XML 文件中提取 select/insert/update/delete 标签 |
| `service/model/MybatisTag.java` | MyBatis 标签数据对象 | | `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/MyBatisGeneratorService.java` | MyBatis 逆向工程服务 |
| `service/generator/CustomCommentGenerator.java` | 自定义注释生成器 | | `service/generator/CustomCommentGenerator.java` | 自定义注释生成器 |
| `service/generator/LombokPlugin.java` | Lombok 插件(@Data) | | `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/GitService.java` | Git 服务接口 |
| `service/GitServiceImpl.java` | Git 服务实现 | | `service/GitServiceImpl.java` | Git 服务实现 |
| `service/config/GitOptimizationConfig.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` + `PluginManager` + `PluginDescriptor` | 每个 biz 模块作为独立 Tool,声明版本、描述,PluginManager 统一管理生命周期(load/enable/disable),可插拔扩展 |
| **策略模式** | `Tool` / `Plugin` 接口 + 各 biz 实现 | 各插件通过实现 Plugin 接口注册到 PluginManager,支持交互和命令行双模式 | | **策略模式** | `Tool` 接口 + 各 biz 实现 | 各插件通过实现 Tool 接口注册到 PluginManager,支持交互和命令行双模式 |
| **策略模式** | `GitCommandStrategy` + 各 Strategy 实现 | Git 命令执行策略,`GitStrategyFactory` 管理注册 | | **策略模式** | `GitCommandStrategy` + 各 Strategy 实现 | Git 命令执行策略,`GitStrategyFactory` 管理注册 |
| **工厂模式** | `PluginManager`、`GitStrategyFactory` | Spring 自动发现和注入 Bean | | **工厂模式** | `PluginManager`、`GitStrategyFactory` | Spring 自动发现和注入 Bean |
| **命令模式** | `GitCommand` + `GitCommandController` | 封装 Git 命令的异步执行、中断、跳过 | | **命令模式** | `GitCommand` + `GitCommandController` | 封装 Git 命令的异步执行、中断、跳过 |
@@ -235,7 +234,7 @@ src/main/resources/
| **模板回调** | `FileOperationService.scanFilesRecursively` | 使用 `SimpleFileVisitor` 实现文件遍历回调 | | **模板回调** | `FileOperationService.scanFilesRecursively` | 使用 `SimpleFileVisitor` 实现文件遍历回调 |
| **分层架构** | `system/` vs `biz/` | system 定义平台契约与共享基础设施,biz 实现具体插件 | | **分层架构** | `system/` vs `biz/` | system 定义平台契约与共享基础设施,biz 实现具体插件 |
| **内部隔离** | `biz/*/service/internal/` | 内部实现类用 `internal/` 隔离,不对外暴露 | | **内部隔离** | `biz/*/service/internal/` | 内部实现类用 `internal/` 隔离,不对外暴露 |
| **生命周期管理** | `Plugin.onLoad()` / `Plugin.onUnload()` | 插件加载/卸载回调,PluginContext 传递运行时上下文 | | **生命周期管理** | `Tool.onLoad()` / `Tool.onUnload()` | 插件加载/卸载回调,PluginContext 传递运行时上下文 |
--- ---
@@ -260,7 +259,7 @@ WorkUtilsApplication (入口)
插件加载链路: 插件加载链路:
PluginManager.init() PluginManager.init()
├── 遍历 Spring 中所有 Plugin Bean ├── 遍历 Spring 中所有 Tool Bean
├── 依次调用 plugin.onLoad(PluginContext) ├── 依次调用 plugin.onLoad(PluginContext)
├── 按 ToolsConfig 过滤启用/禁用 ├── 按 ToolsConfig 过滤启用/禁用
└── 构建 pluginByIdMap / pluginByCommandMap / pluginStates └── 构建 pluginByIdMap / pluginByCommandMap / pluginStates
@@ -318,9 +317,7 @@ WorkUtilsApplication (入口)
### 8.2 接口层次 ### 8.2 接口层次
``` ```
Tool (system/tool/) ← 基础契约:getId/getName/getMenuOptions/execute/... Tool (system/plugin/) ← 统一契约:getDescriptor/onLoad/onUnload/getMenuOptions/execute/...
↑
Plugin (system/plugin/) ← 扩展契约:+getDescriptor/onLoad/onUnload
↑ ↑
各 biz 模块 ← 具体实现:@Component + PluginDescriptor 各 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` 动态加载 2. **独立打包** — 每个 biz 模块打成独立 jar,`PluginManager` 动态加载
3. **热加载** — 基于自定义 ClassLoader 实现插件的动态加载/卸载/更新 3. **热加载** — 基于自定义 ClassLoader 实现插件的动态加载/卸载/更新
4. **依赖注入隔离** — 每个插件使用独立 Spring 子容器(`ApplicationContext`),避免 Bean 冲突 4. **依赖注入隔离** — 每个插件使用独立 Spring 子容器(`ApplicationContext`),避免 Bean 冲突