Start and stop the API debug proxy safely

This commit is contained in:
张家训
2026-08-03 10:49:13 +08:00
parent 88b0d3888c
commit bbddc74c9d
3 changed files with 204 additions and 1 deletions
@@ -0,0 +1,110 @@
package com.example.workutils.biz.apidoc.application;
import com.example.workutils.biz.apidoc.generator.DubboDebugProxy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Objects;
import java.util.function.Supplier;
@Slf4j
@Component
public final class ApiDebugProxyLifecycle {
public enum State {
NOT_STARTED, RUNNING, FAILED
}
public record Status(State state, String endpoint, String errorMessage) {
public Status {
Objects.requireNonNull(state, "state");
endpoint = Objects.requireNonNull(endpoint, "endpoint");
errorMessage = Objects.requireNonNull(errorMessage, "errorMessage");
}
}
interface ProxyHandle {
String endpoint();
void stop();
}
private final Supplier<ProxyHandle> proxyFactory;
private volatile Status status = new Status(State.NOT_STARTED, "", "");
private ProxyHandle proxy;
public ApiDebugProxyLifecycle() {
this(() -> {
DubboDebugProxy debugProxy = DubboDebugProxy.getInstance();
return new ProxyHandle() {
@Override
public String endpoint() {
return debugProxy.getInvokeUrl();
}
@Override
public void stop() {
debugProxy.stop();
}
};
});
}
ApiDebugProxyLifecycle(Supplier<ProxyHandle> proxyFactory) {
this.proxyFactory = Objects.requireNonNull(proxyFactory, "proxyFactory");
}
public synchronized Status start() {
if (status.state() != State.NOT_STARTED) {
return status;
}
try {
ProxyHandle startedProxy = Objects.requireNonNull(
proxyFactory.get(), "proxyFactory result");
String endpoint = Objects.requireNonNull(
startedProxy.endpoint(), "proxy endpoint");
proxy = startedProxy;
status = new Status(State.RUNNING, endpoint, "");
log.info("API 本地调试代理已就绪: {}", endpoint);
} catch (RuntimeException error) {
status = new Status(State.FAILED, "", readableMessage(error));
log.error("API 本地调试代理启动失败", error);
}
return status;
}
public synchronized void stop() {
ProxyHandle activeProxy = proxy;
proxy = null;
if (activeProxy == null) {
return;
}
try {
activeProxy.stop();
} catch (RuntimeException error) {
log.warn("API 本地调试代理关闭失败", error);
}
}
public Status status() {
return status;
}
public String endpoint() {
return status.endpoint();
}
private static String readableMessage(RuntimeException error) {
String message = error.getMessage();
Throwable cause = error.getCause();
if (cause != null && cause.getMessage() != null
&& !cause.getMessage().isBlank()) {
return (message == null || message.isBlank())
? cause.getMessage()
: message + ": " + cause.getMessage();
}
return message == null || message.isBlank()
? error.getClass().getSimpleName()
: message;
}
}
@@ -17,6 +17,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
/**
* 本地 HTTP 代理,解决浏览器直连内网网关不通的问题。
@@ -31,13 +32,16 @@ public class DubboDebugProxy {
private final int port;
private final HttpServer server;
private final ExecutorService executor;
private boolean stopped;
private DubboDebugProxy() {
try {
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
this.port = server.getAddress().getPort();
server.createContext("/dubbo/invoke", this::handleInvoke);
server.setExecutor(Executors.newFixedThreadPool(4));
executor = Executors.newFixedThreadPool(4);
server.setExecutor(executor);
server.start();
log.info("DubboDebugProxy 已启动: http://127.0.0.1:{}/dubbo/invoke", port);
} catch (IOException e) {
@@ -64,6 +68,16 @@ public class DubboDebugProxy {
return "http://127.0.0.1:" + port + "/dubbo/invoke";
}
public synchronized void stop() {
if (stopped) {
return;
}
stopped = true;
server.stop(0);
executor.shutdownNow();
log.info("DubboDebugProxy 已停止");
}
private void handleInvoke(HttpExchange exchange) throws IOException {
// CORS 头
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
@@ -0,0 +1,79 @@
package com.example.workutils.biz.apidoc.application;
import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ApiDebugProxyLifecycleTest {
@Test
void startsAndStopsTheOwnedProxyOnlyOnce() {
AtomicInteger starts = new AtomicInteger();
AtomicInteger stops = new AtomicInteger();
ApiDebugProxyLifecycle lifecycle = new ApiDebugProxyLifecycle(() -> {
starts.incrementAndGet();
return new ApiDebugProxyLifecycle.ProxyHandle() {
@Override
public String endpoint() {
return "http://127.0.0.1:1234/dubbo/invoke";
}
@Override
public void stop() {
stops.incrementAndGet();
}
};
});
ApiDebugProxyLifecycle.Status first = lifecycle.start();
ApiDebugProxyLifecycle.Status second = lifecycle.start();
lifecycle.stop();
lifecycle.stop();
assertEquals(1, starts.get());
assertEquals(1, stops.get());
assertEquals(ApiDebugProxyLifecycle.State.RUNNING, first.state());
assertEquals(first, second);
assertEquals("http://127.0.0.1:1234/dubbo/invoke", lifecycle.endpoint());
}
@Test
void recordsStartupFailureWithoutPreventingTheApplicationFromContinuing() {
ApiDebugProxyLifecycle lifecycle = new ApiDebugProxyLifecycle(
() -> { throw new IllegalStateException("端口不可用"); });
ApiDebugProxyLifecycle.Status status = assertDoesNotThrow(lifecycle::start);
assertEquals(ApiDebugProxyLifecycle.State.FAILED, status.state());
assertEquals("", lifecycle.endpoint());
assertTrue(status.errorMessage().contains("端口不可用"));
assertDoesNotThrow(lifecycle::stop);
}
@Test
void shutdownFailureDoesNotEscapeOrRetryTheSameHandle() {
AtomicInteger stops = new AtomicInteger();
ApiDebugProxyLifecycle lifecycle = new ApiDebugProxyLifecycle(() ->
new ApiDebugProxyLifecycle.ProxyHandle() {
@Override
public String endpoint() {
return "http://127.0.0.1:1234/dubbo/invoke";
}
@Override
public void stop() {
stops.incrementAndGet();
throw new IllegalStateException("关闭失败");
}
});
lifecycle.start();
assertDoesNotThrow(lifecycle::stop);
assertDoesNotThrow(lifecycle::stop);
assertEquals(1, stops.get());
}
}