Start API proxy with WorkUtils

This commit is contained in:
张家训
2026-08-03 10:56:00 +08:00
parent 10b7f40e65
commit b604fb5006
8 changed files with 119 additions and 44 deletions
@@ -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,11 +95,16 @@ public class WorkUtilsApplication implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
apiDebugProxyLifecycle.start();
try {
if (args.length == 0) {
showMainWindow();
} else {
processCommand(args);
}
} finally {
apiDebugProxyLifecycle.stop();
}
}
// ==================== 主窗口入口 ====================
@@ -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);
@@ -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<RunApiDocExportUseCase.Result> 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<String> 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(
@@ -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<String> projectPaths,
boolean onlyInterfaces,
@@ -60,14 +50,4 @@ public record ApiDocExportRequest(
Mode.BATCH, "", projectPaths,
onlyInterfaces, facadeNamesOnly, apiPackagesOnly);
}
@Deprecated
public static ApiDocExportRequest batch(
List<String> projectPaths,
boolean onlyInterfaces,
boolean facadeNamesOnly,
boolean apiPackagesOnly,
boolean ignored) {
return batch(projectPaths, onlyInterfaces, facadeNamesOnly, apiPackagesOnly);
}
}
@@ -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 {
@@ -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("端口不可用"));
});
}
}
@@ -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
@@ -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");