Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

package modelengine.fit.http.server.netty;

import static modelengine.fitframework.inspection.Validation.greaterThan;
import static modelengine.fitframework.inspection.Validation.greaterThanOrEquals;
import static modelengine.fitframework.inspection.Validation.isTrue;
import static modelengine.fitframework.inspection.Validation.lessThanOrEquals;
import static modelengine.fitframework.inspection.Validation.notNull;
Expand Down Expand Up @@ -52,6 +52,7 @@
import modelengine.fitframework.value.ValueFetcher;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -94,8 +95,10 @@ public class NettyHttpClassicServer implements HttpClassicServer {
log.error("Failed to start netty http server.", exception);
}));
private final ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
private volatile int httpPort;
private volatile int httpsPort;
private volatile int httpPort = 0;
private volatile int httpsPort = 0;
private volatile boolean httpBound = false;
private volatile boolean httpsBound = false;
private final boolean isGracefulExit;
private volatile boolean isStarted = false;
private final Lock lock = LockUtils.newReentrantLock();
Expand Down Expand Up @@ -131,27 +134,29 @@ public HttpClassicServer bind(int port, boolean isSecure) {
return this;
}
if (isSecure) {
this.httpsPort = greaterThan(port,
this.httpsPort = greaterThanOrEquals(port,
0,
"The port to bind to netty http server cannot be less than 1. [port={0}, isSecure={1}]",
"The port to bind to netty http server cannot be negative. [port={0}, isSecure={1}]",
port,
true);
this.httpsPort = lessThanOrEquals(port,
65535,
"The port to bind to netty http server cannot be more than 65535. [port={0}, isSecure={1}]",
port,
true);
this.httpsBound = true;
} else {
this.httpPort = greaterThan(port,
this.httpPort = greaterThanOrEquals(port,
0,
"The port to bind to netty http server cannot be less than 1. [port={0}, isSecure={1}]",
"The port to bind to netty http server cannot be negative. [port={0}, isSecure={1}]",
port,
false);
this.httpPort = lessThanOrEquals(port,
65535,
"The port to bind to netty http server cannot be more than 65535. [port={0}, isSecure={1}]",
port,
false);
this.httpBound = true;
}
return this;
}
Expand All @@ -161,7 +166,7 @@ public void start() {
if (this.isStarted) {
return;
}
isTrue(this.httpPort > 0 || this.httpsPort > 0,
isTrue(this.httpBound || this.httpsBound,
"At least 1 port should be bound to netty http server. [httpPort={0}, httpsPort={1}]",
this.httpPort,
this.httpsPort);
Expand All @@ -177,6 +182,22 @@ public boolean isStarted() {
return this.isStarted;
}

@Override
public int getActualHttpPort() {
if (!this.isStarted || !this.httpBound) {
return 0;
}
return Math.max(this.httpPort, 0);
}

@Override
public int getActualHttpsPort() {
if (!this.isStarted || !this.httpsBound) {
return 0;
}
return Math.max(this.httpsPort, 0);
}

@Override
public void stop() {
if (!this.isStarted) {
Expand All @@ -202,27 +223,32 @@ private void startServer() {
EventLoopGroup workerGroup = this.createWorkerGroup();
try {
SSLContext sslContext = null;
if (this.httpsPort > 0 && this.httpsConfig.isSslEnabled()) {
if (this.httpsBound && this.httpsConfig != null && this.httpsConfig.isSslEnabled()) {
sslContext = this.createSslContext();
}
ChannelHandler channelHandler = new ChannelInitializerHandler(this,
this.getAssemblerConfig(),
this.httpsPort,
sslContext,
this.httpsConfig);
ChannelHandler channelHandler =
new ChannelInitializerHandler(this, this.getAssemblerConfig(), sslContext, this.httpsConfig);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(channelHandler);
this.logServerStarted();
if (this.httpPort > 0) {
if (this.httpBound) {
Channel channel = serverBootstrap.bind(this.httpPort).sync().channel();
this.channelGroup.add(channel);
if (this.httpPort == 0) {
this.httpPort = ((InetSocketAddress) channel.localAddress()).getPort();
log.info("HTTP server bound to auto-assigned port: {}", this.httpPort);
}
}
if (this.httpsPort > 0) {
if (this.httpsBound) {
Channel channel = serverBootstrap.bind(this.httpsPort).sync().channel();
this.channelGroup.add(channel);
if (this.httpsPort == 0) {
this.httpsPort = ((InetSocketAddress) channel.localAddress()).getPort();
log.info("HTTPS server bound to auto-assigned port: {}", this.httpsPort);
}
}
this.logServerStarted();
ChannelGroupFuture channelFutures = this.channelGroup.newCloseFuture();
this.isStarted = true;
channelFutures.sync();
Expand Down Expand Up @@ -353,17 +379,17 @@ private static class ChannelInitializerHandler extends ChannelInitializer<Socket
"TLS_AES_128_CCM_SHA256"))
.build();

private final int httpsPort;
private final NettyHttpClassicServer server;
private final SSLContext sslContext;
private final ServerConfig.Secure httpsConfig;
private final ProtocolUpgrader upgrader;
private final ProtocolUpgrader secureUpgrader;
private final HttpClassicRequestAssembler assembler;
private final HttpClassicRequestAssembler secureAssembler;

ChannelInitializerHandler(HttpClassicServer server, HttpClassicRequestAssembler.Config assemblerConfig,
int httpsPort, SSLContext sslContext, ServerConfig.Secure httpsConfig) {
this.httpsPort = httpsPort;
ChannelInitializerHandler(NettyHttpClassicServer server, HttpClassicRequestAssembler.Config assemblerConfig,
SSLContext sslContext, ServerConfig.Secure httpsConfig) {
this.server = server;
this.sslContext = sslContext;
this.httpsConfig = httpsConfig;
this.upgrader = new ProtocolUpgrader(server,
Expand All @@ -381,7 +407,8 @@ private static class ChannelInitializerHandler extends ChannelInitializer<Socket
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
if (ch.localAddress().getPort() == this.httpsPort && this.sslContext != null
int httpsPort = this.server.httpsPort;
if (ch.localAddress().getPort() == httpsPort && this.sslContext != null && this.httpsConfig != null
&& this.httpsConfig.isSslEnabled()) {
pipeline.addLast(new SslHandler(this.buildSslEngine(this.sslContext, this.httpsConfig)));
pipeline.addLast(new HttpServerCodec());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

package modelengine.fit.server.http;

import static modelengine.fitframework.inspection.Validation.greaterThan;
import static modelengine.fitframework.inspection.Validation.greaterThanOrEquals;
import static modelengine.fitframework.inspection.Validation.notNull;

import modelengine.fit.http.server.HttpClassicServer;
Expand Down Expand Up @@ -67,7 +67,7 @@ public FitHttpServer(HttpClassicServer httpServer, FitHttpHandlerRegistry regist
if (this.httpsOpen) {
Optional<ServerConfig.Secure> secure = httpConfig.secure();
this.httpsPort = secure.flatMap(ServerConfig.Secure::port).orElse(DEFAULT_HTTPS_PORT);
greaterThan(this.httpsPort, 0, "The server https port must be positive.");
greaterThanOrEquals(this.httpsPort, 0, "The server https port must be non-negative.");
log.debug("Config 'server.https.port' is {}.", this.httpsPort);
this.toRegisterHttpsPort = secure.flatMap(ServerConfig.Secure::toRegisterPort).orElse(this.httpsPort);
log.debug("Config 'server.https.to-register-port' is {}.", this.toRegisterHttpsPort);
Expand All @@ -78,7 +78,7 @@ public FitHttpServer(HttpClassicServer httpServer, FitHttpHandlerRegistry regist
this.httpOpen = httpConfig.isProtocolEnabled() || !this.httpsOpen;
if (this.httpOpen) {
this.httpPort = httpConfig.port().orElse(DEFAULT_HTTP_PORT);
greaterThan(this.httpPort, 0, "The server http port must be positive.");
greaterThanOrEquals(this.httpPort, 0, "The server http port must be non-negative.");
log.debug("Config 'server.http.port' is {}.", this.httpPort);
this.toRegisterHttpPort = httpConfig.toRegisterPort().orElse(this.httpPort);
log.debug("Config 'server.http.to-register-port' is {}.", this.toRegisterHttpPort);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ public interface HttpClassicServer extends HttpResource {
*/
boolean isStarted();

/**
* 获取 HTTP 服务实际绑定的端口。
* <p>注意:此方法应在服务器启动后({@link #isStarted()} 返回 {@code true})调用。</p>
* <p>如果在 {@link #bind(int)} 时传入 {@code 0},此方法返回操作系统自动分配的实际端口。</p>
*
* @return 表示 HTTP 服务实际绑定的端口号,未绑定或未启动时返回 {@code 0}。
*/
int getActualHttpPort();

/**
* 获取 HTTPS 服务实际绑定的端口。
* <p>注意:此方法应在服务器启动后({@link #isStarted()} 返回 {@code true})调用。</p>
* <p>如果在 {@link #bind(int, boolean)} 时传入 {@code 0},此方法返回操作系统自动分配的实际端口。</p>
*
* @return 表示 HTTPS 服务实际绑定的端口号,未绑定或未启动时返回 {@code 0}。
*/
int getActualHttpsPort();

/**
* 停止 Http 服务器。
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import modelengine.fit.http.client.HttpClassicClientResponse;
import modelengine.fit.http.entity.TextEntity;
import modelengine.fitframework.exception.ClientException;
import modelengine.fit.http.server.HttpClassicServer;
import modelengine.fitframework.test.annotation.EnableMockMvc;
import modelengine.fitframework.test.domain.TestContext;
import modelengine.fitframework.test.domain.mvc.MockController;
Expand Down Expand Up @@ -43,16 +44,7 @@ public class MockMvcListener implements TestListener {
private static final long MIN_STARTUP_TIMEOUT = 1_000L;
private static final long MAX_STARTUP_TIMEOUT = 600_000L;

private final int port;

/**
* 通过插件端口初始化 {@link MockMvcListener} 的实例。
*
* @param port 表示插件启动端口的 {code int}。
*/
public MockMvcListener(int port) {
this.port = port;
}
public MockMvcListener() {}

@Override
public Optional<TestContextConfiguration> config(Class<?> clazz) {
Expand All @@ -73,18 +65,33 @@ public void beforeTestClass(TestContext context) {
if (AnnotationUtils.getAnnotation(testClass, EnableMockMvc.class).isEmpty()) {
return;
}
MockMvc mockMvc = new MockMvc(this.port);
context.plugin().container().registry().register(mockMvc);
long timeout = this.getStartupTimeout();
long startTime = System.currentTimeMillis();
boolean started = this.isStarted(mockMvc);
HttpClassicServer server = this.getHttpServer(context);
while (!server.isStarted()) {
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed > timeout) {
throw new IllegalStateException(this.buildTimeoutErrorMessage(elapsed, 0));
}
ThreadUtils.sleep(100);
}
int actualPort = server.getActualHttpPort();
if (actualPort <= 0) {
throw new IllegalStateException(StringUtils.format(
"Failed to resolve actual HTTP port from server. [started={0}, httpPort={1}]",
server.isStarted(),
actualPort));
}
MockMvc mockMvc = new MockMvc(actualPort);
context.plugin().container().registry().register(mockMvc);
boolean started = this.isStarted(mockMvc, actualPort);
while (!started) {
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed > timeout) {
throw new IllegalStateException(this.buildTimeoutErrorMessage(elapsed, this.port));
throw new IllegalStateException(this.buildTimeoutErrorMessage(elapsed, actualPort));
}
ThreadUtils.sleep(100);
started = this.isStarted(mockMvc);
started = this.isStarted(mockMvc, actualPort);
}
}

Expand All @@ -108,37 +115,62 @@ private long getStartupTimeout() {
}

private String buildTimeoutErrorMessage(long elapsed, int port) {
if (port > 0) {
return StringUtils.format("""
Mock MVC server failed to start within {0}ms. [port={1}]

Possible causes:
1. Port {1} is already in use by another process
2. Network configuration issues
3. Server startup is slower than expected in this environment

Troubleshooting steps:
- Check if port {1} is in use:
* macOS/Linux: lsof -i :{1}
* Windows: netstat -ano | findstr :{1}
- Check server logs for detailed error messages
- If running in a slow environment, increase timeout:
mvn test -D{2}=60000""",
elapsed,
port,
TIMEOUT_PROPERTY_KEY);
}
return StringUtils.format("""
Mock MVC server failed to start within {0}ms. [port={1}]
Mock MVC server failed to start within {0}ms. [auto-assigned port]

Possible causes:
1. Port {1} is already in use by another process
1. Port conflict with another process
2. Network configuration issues
3. Server startup is slower than expected in this environment

Troubleshooting steps:
- Check if port {1} is in use:
* macOS/Linux: lsof -i :{1}
* Windows: netstat -ano | findstr :{1}
- Check server logs for detailed error messages
- If running in a slow environment, increase timeout:
mvn test -D{2}=60000""",
mvn test -D{1}=60000""",
elapsed,
port,
TIMEOUT_PROPERTY_KEY);
}

protected boolean isStarted(MockMvc mockMvc) {
protected boolean isStarted(MockMvc mockMvc, int port) {
MockRequestBuilder builder = MockMvcRequestBuilders.get(MockController.PATH).responseType(String.class);
try (HttpClassicClientResponse<String> response = mockMvc.perform(builder)) {
String content = response.textEntity()
.map(TextEntity::content)
.orElseThrow(() -> new IllegalStateException(StringUtils.format(
"Failed to start mock http server. [port={0}]",
this.port)));
port)));
return Objects.equals(content, MockController.OK);
} catch (IOException | ClientException e) {
return false;
}
}

private HttpClassicServer getHttpServer(TestContext context) {
HttpClassicServer server = context.plugin().container().beans().get(HttpClassicServer.class);
if (server == null) {
throw new IllegalStateException("HttpClassicServer not found in container.");
}
return server;
}

}

This file was deleted.

Loading