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.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 本地 HTTP 代理,解决浏览器直连内网网关不通的问题。
|
* 本地 HTTP 代理,解决浏览器直连内网网关不通的问题。
|
||||||
@@ -31,13 +32,16 @@ public class DubboDebugProxy {
|
|||||||
|
|
||||||
private final int port;
|
private final int port;
|
||||||
private final HttpServer server;
|
private final HttpServer server;
|
||||||
|
private final ExecutorService executor;
|
||||||
|
private boolean stopped;
|
||||||
|
|
||||||
private DubboDebugProxy() {
|
private DubboDebugProxy() {
|
||||||
try {
|
try {
|
||||||
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||||
this.port = server.getAddress().getPort();
|
this.port = server.getAddress().getPort();
|
||||||
server.createContext("/dubbo/invoke", this::handleInvoke);
|
server.createContext("/dubbo/invoke", this::handleInvoke);
|
||||||
server.setExecutor(Executors.newFixedThreadPool(4));
|
executor = Executors.newFixedThreadPool(4);
|
||||||
|
server.setExecutor(executor);
|
||||||
server.start();
|
server.start();
|
||||||
log.info("DubboDebugProxy 已启动: http://127.0.0.1:{}/dubbo/invoke", port);
|
log.info("DubboDebugProxy 已启动: http://127.0.0.1:{}/dubbo/invoke", port);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -64,6 +68,16 @@ public class DubboDebugProxy {
|
|||||||
return "http://127.0.0.1:" + port + "/dubbo/invoke";
|
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 {
|
private void handleInvoke(HttpExchange exchange) throws IOException {
|
||||||
// CORS 头
|
// CORS 头
|
||||||
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
|
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