Start API proxy with WorkUtils
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user