Start and stop the API debug proxy safely
This commit is contained in:
+110
@@ -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", "*");
|
||||
|
||||
+79
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user