diff --git a/src/main/java/com/example/workutils/WorkUtilsApplication.java b/src/main/java/com/example/workutils/WorkUtilsApplication.java index 3c48deb..0d6f4f7 100644 --- a/src/main/java/com/example/workutils/WorkUtilsApplication.java +++ b/src/main/java/com/example/workutils/WorkUtilsApplication.java @@ -1,5 +1,6 @@ package com.example.workutils; +import com.example.workutils.biz.apidoc.application.ApiDebugProxyLifecycle; import com.example.workutils.system.config.*; import com.example.workutils.system.plugin.Plugin; import com.example.workutils.system.plugin.PluginDescriptor; @@ -38,6 +39,7 @@ public class WorkUtilsApplication implements CommandLineRunner { private final PluginManager pluginManager; private final WorkUtilsProperties properties; private final SettingsService settingsService; + private final ApiDebugProxyLifecycle apiDebugProxyLifecycle; /** 主窗口引用,供内嵌插件面板使用 */ private MainWindow mainWindow; @@ -45,10 +47,12 @@ public class WorkUtilsApplication implements CommandLineRunner { private SettingsWindow settingsWindow; public WorkUtilsApplication(PluginManager pluginManager, WorkUtilsProperties properties, - SettingsService settingsService) { + SettingsService settingsService, + ApiDebugProxyLifecycle apiDebugProxyLifecycle) { this.pluginManager = pluginManager; this.properties = properties; this.settingsService = settingsService; + this.apiDebugProxyLifecycle = apiDebugProxyLifecycle; } // ==================== 启动诊断 ==================== @@ -91,10 +95,15 @@ public class WorkUtilsApplication implements CommandLineRunner { @Override public void run(String... args) throws Exception { - if (args.length == 0) { - showMainWindow(); - } else { - processCommand(args); + apiDebugProxyLifecycle.start(); + try { + if (args.length == 0) { + showMainWindow(); + } else { + processCommand(args); + } + } finally { + apiDebugProxyLifecycle.stop(); } } diff --git a/src/main/java/com/example/workutils/biz/apidoc/ApiDocPlugin.java b/src/main/java/com/example/workutils/biz/apidoc/ApiDocPlugin.java index 4aff6a2..47a0af1 100644 --- a/src/main/java/com/example/workutils/biz/apidoc/ApiDocPlugin.java +++ b/src/main/java/com/example/workutils/biz/apidoc/ApiDocPlugin.java @@ -1,5 +1,6 @@ package com.example.workutils.biz.apidoc; +import com.example.workutils.biz.apidoc.application.ApiDebugProxyLifecycle; import com.example.workutils.biz.apidoc.config.ApiBaseClassManager; import com.example.workutils.system.config.WorkUtilsProperties; import com.example.workutils.system.plugin.Plugin; @@ -40,6 +41,7 @@ public class ApiDocPlugin implements Plugin { private final WorkUtilsProperties properties; private final FileOperationHelper fileOperationHelper; private final ApiBaseClassManager apiBaseClassManager; + private final ApiDebugProxyLifecycle apiDebugProxyLifecycle; @Override public PluginDescriptor getDescriptor() { return DESCRIPTOR; } @@ -55,7 +57,8 @@ public class ApiDocPlugin implements Plugin { public Parent getEmbeddedPane() { if (embeddedRoot != null) return embeddedRoot; latch = new CountDownLatch(1); - window = new ApiDocWindow(fileOperationHelper, properties, latch, apiBaseClassManager); + window = new ApiDocWindow(fileOperationHelper, properties, latch, + apiBaseClassManager, apiDebugProxyLifecycle); embeddedRoot = window.getContentPane(); window.getStage().getScene().setRoot(new Pane()); return embeddedRoot; @@ -65,7 +68,8 @@ public class ApiDocPlugin implements Plugin { public void open(Runnable onClosed) { if (window == null) { latch = new CountDownLatch(1); - window = new ApiDocWindow(fileOperationHelper, properties, latch, apiBaseClassManager); + window = new ApiDocWindow(fileOperationHelper, properties, latch, + apiBaseClassManager, apiDebugProxyLifecycle); } if (embeddedRoot != null && window.getStage().getScene().getRoot() != embeddedRoot) { window.getStage().getScene().setRoot(embeddedRoot); diff --git a/src/main/java/com/example/workutils/biz/apidoc/ApiDocWindow.java b/src/main/java/com/example/workutils/biz/apidoc/ApiDocWindow.java index 6f98856..23955c7 100644 --- a/src/main/java/com/example/workutils/biz/apidoc/ApiDocWindow.java +++ b/src/main/java/com/example/workutils/biz/apidoc/ApiDocWindow.java @@ -1,10 +1,12 @@ package com.example.workutils.biz.apidoc; +import com.example.workutils.biz.apidoc.application.ApiDebugProxyLifecycle; +import com.example.workutils.biz.apidoc.application.ApiDebugProxyLifecycle.State; +import com.example.workutils.biz.apidoc.application.ApiDebugProxyLifecycle.Status; import com.example.workutils.biz.apidoc.application.ApiDocExportRequest; import com.example.workutils.biz.apidoc.application.RunApiDocExportUseCase; import com.example.workutils.biz.apidoc.config.ApiBaseClassManager; import com.example.workutils.biz.apidoc.generator.ApiDocGenerator; -import com.example.workutils.biz.apidoc.generator.DubboDebugProxy; import com.example.workutils.biz.apidoc.model.ApiDocModel; import com.example.workutils.biz.apidoc.parser.JavaSourceParser; import com.example.workutils.system.config.WorkUtilsProperties; @@ -58,6 +60,7 @@ public class ApiDocWindow { private final WorkUtilsProperties properties; private final FileOperationHelper fileOperationHelper; private final ApiBaseClassManager apiBaseClassManager; + private final ApiDebugProxyLifecycle apiDebugProxyLifecycle; private final RunApiDocExportUseCase exportUseCase; private final AsyncOperationController exportOperation; private final HistoryFileCommands historyFileCommands; @@ -82,7 +85,6 @@ public class ApiDocWindow { // 操作区 private Button exportBtn; - private CheckBox debugProxyCb; private TextArea logArea; // 历史记录 @@ -94,13 +96,16 @@ public class ApiDocWindow { private static final String DELIMITER = "|"; public ApiDocWindow(FileOperationHelper fileOperationHelper, WorkUtilsProperties properties, - CountDownLatch closeLatch, ApiBaseClassManager apiBaseClassManager) { + CountDownLatch closeLatch, ApiBaseClassManager apiBaseClassManager, + ApiDebugProxyLifecycle apiDebugProxyLifecycle) { this.fileOperationHelper = fileOperationHelper; this.properties = properties; this.closeLatch = closeLatch; this.apiBaseClassManager = apiBaseClassManager; + this.apiDebugProxyLifecycle = Objects.requireNonNull( + apiDebugProxyLifecycle, "apiDebugProxyLifecycle"); this.exportUseCase = new RunApiDocExportUseCase( - () -> DubboDebugProxy.getInstance().getInvokeUrl(), + apiDebugProxyLifecycle::endpoint, this::executeExport); this.exportOperation = new AsyncOperationController<>( AppExecutors.io(), Platform::runLater); @@ -134,6 +139,25 @@ public class ApiDocWindow { private int fsBtn() { return properties.getFontButton(); } private int fsBody() { return properties.getFontBody(); } + static Label buildProxyStatusLabel(Status status, int fontSize) { + Objects.requireNonNull(status, "status"); + boolean running = status.state() == State.RUNNING; + String reason = status.errorMessage().isBlank() + ? "代理尚未启动" + : status.errorMessage(); + String text = running + ? "● 本地代理已启动:" + status.endpoint() + : "● 本地代理启动失败:" + reason; + Label label = new Label(text); + label.setFont(Theme.base(fontSize)); + label.setTextFill(running ? Theme.SUCCESS : Theme.DANGER); + label.setAccessibleText(text); + if (!running) { + label.setTooltip(new Tooltip("本地代理启动失败:" + reason)); + } + return label; + } + private void buildUI() { stage.setTitle("API 接口文档导出"); stage.setWidth(1080); @@ -262,14 +286,11 @@ public class ApiDocWindow { exportBtn = StyledButton.primary("导出文档", Theme.bold(fsBtn())); exportBtn.setOnAction(e -> startExport()); LoadingButtonBinder.bind(exportBtn, exportOperation, "导出文档"); - debugProxyCb = new CheckBox("启用接口调试(启动本地代理)"); - debugProxyCb.setSelected(false); - debugProxyCb.setFont(Theme.base(fsBody() - 1)); - debugProxyCb.setTooltip(new Tooltip( - "仅生成支持“发送调试请求”的文档时启用;普通导出无需启动代理。")); + Label proxyStatus = buildProxyStatusLabel( + apiDebugProxyLifecycle.status(), fsBody() - 1); Region actionSpacer = new Region(); HBox.setHgrow(actionSpacer, Priority.ALWAYS); - actionRow.getChildren().addAll(debugProxyCb, actionSpacer, exportBtn); + actionRow.getChildren().addAll(proxyStatus, actionSpacer, exportBtn); OperationStatusPane operationStatus = new OperationStatusPane( exportOperation, fsBody() - 1, false); @@ -363,6 +384,9 @@ public class ApiDocWindow { private void startExport() { try { ApiDocExportRequest request = buildRequest(); + if (apiDebugProxyLifecycle.endpoint().isBlank()) { + log("[WARN] 本地代理不可用,导出文档的接口调试功能已禁用"); + } exportOperation.start( "正在解析源码", context -> exportUseCase.execute(request, context), @@ -376,12 +400,11 @@ public class ApiDocWindow { } private ApiDocExportRequest buildRequest() throws IOException { - boolean debugEnabled = debugProxyCb.isSelected(); if (tbCustomPath.isSelected()) { - return ApiDocExportRequest.path(pathField.getText(), debugEnabled); + return ApiDocExportRequest.path(pathField.getText()); } if (tbFqn.isSelected()) { - return ApiDocExportRequest.fqn(fqnField.getText(), debugEnabled); + return ApiDocExportRequest.fqn(fqnField.getText()); } List selectedPaths = allProjectsCb.isSelected() ? fileOperationHelper.getProjectPaths() @@ -392,8 +415,7 @@ public class ApiDocWindow { selectedPaths, onlyInterfaceCb.isSelected(), facadeNameCb.isSelected(), - apiPackageOnlyCb.isSelected(), - debugEnabled); + apiPackageOnlyCb.isSelected()); } private void executeExport( diff --git a/src/main/java/com/example/workutils/biz/apidoc/application/ApiDocExportRequest.java b/src/main/java/com/example/workutils/biz/apidoc/application/ApiDocExportRequest.java index fca912d..88b2969 100644 --- a/src/main/java/com/example/workutils/biz/apidoc/application/ApiDocExportRequest.java +++ b/src/main/java/com/example/workutils/biz/apidoc/application/ApiDocExportRequest.java @@ -35,22 +35,12 @@ public record ApiDocExportRequest( false, false, false); } - @Deprecated - public static ApiDocExportRequest path(String path, boolean ignored) { - return path(path); - } - public static ApiDocExportRequest fqn(String className) { return new ApiDocExportRequest( Mode.FQN, className, List.of(), false, false, false); } - @Deprecated - public static ApiDocExportRequest fqn(String className, boolean ignored) { - return fqn(className); - } - public static ApiDocExportRequest batch( List projectPaths, boolean onlyInterfaces, @@ -60,14 +50,4 @@ public record ApiDocExportRequest( Mode.BATCH, "", projectPaths, onlyInterfaces, facadeNamesOnly, apiPackagesOnly); } - - @Deprecated - public static ApiDocExportRequest batch( - List projectPaths, - boolean onlyInterfaces, - boolean facadeNamesOnly, - boolean apiPackagesOnly, - boolean ignored) { - return batch(projectPaths, onlyInterfaces, facadeNamesOnly, apiPackagesOnly); - } } diff --git a/src/main/java/com/example/workutils/biz/apidoc/generator/DubboDebugProxy.java b/src/main/java/com/example/workutils/biz/apidoc/generator/DubboDebugProxy.java index 408a2a6..0b445b8 100644 --- a/src/main/java/com/example/workutils/biz/apidoc/generator/DubboDebugProxy.java +++ b/src/main/java/com/example/workutils/biz/apidoc/generator/DubboDebugProxy.java @@ -16,14 +16,14 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * 本地 HTTP 代理,解决浏览器直连内网网关不通的问题。 * 监听 localhost 随机端口,JS 请求本代理,由 Java 侧转发到 gw.test.bestpay.net。 * - * 单例,应用启动时创建,进程退出时自动回收。 + * 单例,由应用生命周期统一启动和停止。 */ @Slf4j public class DubboDebugProxy { diff --git a/src/test/java/com/example/workutils/biz/apidoc/ApiDebugProxyStatusLabelTest.java b/src/test/java/com/example/workutils/biz/apidoc/ApiDebugProxyStatusLabelTest.java new file mode 100644 index 0000000..d032168 --- /dev/null +++ b/src/test/java/com/example/workutils/biz/apidoc/ApiDebugProxyStatusLabelTest.java @@ -0,0 +1,42 @@ +package com.example.workutils.biz.apidoc; + +import com.example.workutils.biz.apidoc.application.ApiDebugProxyLifecycle.State; +import com.example.workutils.biz.apidoc.application.ApiDebugProxyLifecycle.Status; +import com.example.workutils.system.ui.Theme; +import javafx.scene.control.Label; +import org.junit.jupiter.api.Test; + +import static com.example.workutils.system.ui.FxTestSupport.fx; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ApiDebugProxyStatusLabelTest { + + @Test + void runningStateShowsTheLocalEndpointAsSuccessfulReadOnlyStatus() throws Exception { + fx(() -> { + Label label = ApiDocWindow.buildProxyStatusLabel( + new Status(State.RUNNING, + "http://127.0.0.1:1234/dubbo/invoke", ""), 13); + + assertTrue(label.getText().contains("本地代理已启动")); + assertTrue(label.getText().contains("127.0.0.1:1234")); + assertEquals(Theme.SUCCESS, label.getTextFill()); + assertTrue(label.getAccessibleText().contains("本地代理已启动")); + }); + } + + @Test + void failedStateExposesTheCompleteFailureReason() throws Exception { + fx(() -> { + Label label = ApiDocWindow.buildProxyStatusLabel( + new Status(State.FAILED, "", "端口不可用"), 13); + + assertTrue(label.getText().contains("本地代理启动失败")); + assertTrue(label.getText().contains("端口不可用")); + assertEquals(Theme.DANGER, label.getTextFill()); + assertTrue(label.getTooltip().getText().contains("端口不可用")); + assertTrue(label.getAccessibleText().contains("端口不可用")); + }); + } +} diff --git a/src/test/java/com/example/workutils/biz/apidoc/ApiDocWindowArchitectureTest.java b/src/test/java/com/example/workutils/biz/apidoc/ApiDocWindowArchitectureTest.java index cd70a61..5730de2 100644 --- a/src/test/java/com/example/workutils/biz/apidoc/ApiDocWindowArchitectureTest.java +++ b/src/test/java/com/example/workutils/biz/apidoc/ApiDocWindowArchitectureTest.java @@ -21,8 +21,12 @@ class ApiDocWindowArchitectureTest { assertTrue(source.contains("new Tab(\"基类配置\")")); assertTrue(source.contains("LoadingButtonBinder.bind")); assertTrue(source.contains("new ExportHistoryPane")); + assertTrue(source.contains("buildProxyStatusLabel")); assertFalse(source.contains("new Thread(")); assertFalse(source.contains("setBtnLoading(")); + assertFalse(source.contains("debugProxyCb")); + assertFalse(source.contains("new CheckBox(\"启用接口调试")); + assertFalse(source.contains("DubboDebugProxy.getInstance()")); } @Test diff --git a/src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java b/src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java index 784abbb..710c04b 100644 --- a/src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java +++ b/src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java @@ -67,6 +67,20 @@ class MainWindowArchitectureTest { < openPlugin.indexOf("factory.getEmbeddedPane()")); } + @Test + void applicationOwnsTheDebugProxyForTheWholeBusinessRun() throws Exception { + String application = source("src/main/java/com/example/workutils/WorkUtilsApplication.java"); + String runMethod = between(application, + "public void run(String... args)", + "// ==================== 主窗口入口"); + + assertTrue(runMethod.contains("apiDebugProxyLifecycle.start()")); + assertTrue(runMethod.indexOf("apiDebugProxyLifecycle.start()") + < runMethod.indexOf("if (args.length == 0)")); + assertTrue(runMethod.contains("finally")); + assertTrue(runMethod.contains("apiDebugProxyLifecycle.stop()")); + } + @Test void headerUsesDesignTokensAndFeatherSemanticsWithoutCustomWindowChrome() throws Exception { String mainWindow = source("src/main/java/com/example/workutils/system/ui/MainWindow.java");