1. 概述

在 Java 应用中启动一个 Socket 服务器时,java.net API 要求我们指定一个用于监听的端口号。这个端口号是 TCP 层识别数据目标应用的关键。

显式指定端口号并不总是最优选择,因为该端口可能已被其他应用占用,这会导致我们的 Java 应用抛出 I/O 异常。

本文将介绍如何检查某个端口是否可用,以及如何自动获取一个可用端口。我们将分别探讨使用纯 Java 和 Spring 框架的方式,并介绍一些常见的嵌入式服务器(如 Tomcat 和 Jetty)是如何处理端口分配的。

2. 检查端口状态

2.1. 检查特定端口

我们可以使用 java.net.ServerSocket 类来尝试绑定一个指定端口。如果该端口已被占用,则会抛出 IOException

try (ServerSocket serverSocket = new ServerSocket(FREE_PORT_NUMBER)) {
    assertThat(serverSocket).isNotNull();
    assertThat(serverSocket.getLocalPort()).isEqualTo(FREE_PORT_NUMBER);
} catch (IOException e) {
    fail("Port is not available");
}

如果尝试重复使用同一个端口(或该端口已被其他进程占用),构造函数会抛出异常:

try (ServerSocket serverSocket = new ServerSocket(FREE_PORT_NUMBER)) {
    new ServerSocket(FREE_PORT_NUMBER);
    fail("Same port cannot be used twice");
} catch (IOException e) {
    assertThat(e).hasMessageContaining("Address already in use");
}

2.2. 检查端口范围

我们可以通过遍历一个端口范围,捕获 IOException,来找到第一个可用的端口:

for (int port : FREE_PORT_RANGE) {
    try (ServerSocket serverSocket = new ServerSocket(port)) {
        assertThat(serverSocket).isNotNull();
        assertThat(serverSocket.getLocalPort()).isEqualTo(port);
        return;
    } catch (IOException e) {
        assertThat(e).hasMessageContaining("Address already in use");
    }
}
fail("No free port in the range found");

3. 自动获取可用端口

显式指定端口号不总是靠谱,特别是在测试或动态部署场景中。我们可以通过一些方式让系统自动为我们分配一个可用端口。

3.1. 使用纯 Java

ServerSocket 构造函数中传入端口号 0,Java 会自动为我们分配一个可用端口:

try (ServerSocket serverSocket = new ServerSocket(0)) {
    assertThat(serverSocket).isNotNull();
    assertThat(serverSocket.getLocalPort()).isGreaterThan(0);
} catch (IOException e) {
    fail("Port is not available");
}

3.2. 使用 Spring Framework

Spring 提供了 SocketUtils 工具类,可以方便地获取一个可用的 TCP 端口:

int port = SocketUtils.findAvailableTcpPort();
try (ServerSocket serverSocket = new ServerSocket(port)) {
    assertThat(serverSocket).isNotNull();
    assertThat(serverSocket.getLocalPort()).isEqualTo(port);
} catch (IOException e) {
    fail("Port is not available");
}

⚠️ 注意:Spring 的 SocketUtils 内部其实也是通过 ServerSocket(0) 实现的。

4. 其他服务器实现

4.1. Jetty

Jetty 是一个非常流行的嵌入式 Java Web 服务器。如果不显式调用 setPort() 方法,它会自动分配一个可用端口:

Server jettyServer = new Server();
ServerConnector serverConnector = new ServerConnector(jettyServer);
jettyServer.addConnector(serverConnector);
try {
    jettyServer.start();
    assertThat(serverConnector.getLocalPort()).isGreaterThan(0);
} catch (Exception e) {
    fail("Failed to start Jetty server");
} finally {
    jettyServer.stop();
    jettyServer.destroy();
}

4.2. Tomcat

Tomcat 是另一个常见的嵌入式服务器。与 Jetty 类似,可以通过 setPort(0) 来自动分配端口:

Tomcat tomcatServer = new Tomcat();
tomcatServer.setPort(0);
try {
    tomcatServer.start();
    assertThat(tomcatServer.getConnector().getLocalPort()).isGreaterThan(0);
} catch (LifecycleException e) {
    fail("Failed to start Tomcat server");
} finally {
    tomcatServer.stop();
    tomcatServer.destroy();
}

⚠️ 注意:如果不调用 setPort(),Tomcat 会默认使用 8080 端口,而这个端口很容易被占用。

5. 总结

在本文中我们介绍了以下几种方式来获取可用端口:

✅ 检查某个端口是否被占用
✅ 遍历端口范围寻找可用端口
✅ 使用 ServerSocket(0) 自动分配端口
✅ 利用 Spring 的 SocketUtils 工具类
✅ 嵌入式服务器(Jetty、Tomcat)的端口自动分配机制

这些方法在编写自动化测试、微服务动态部署、或本地调试时非常实用。

完整代码示例可从 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-networking-3


原始标题:Finding a Free Port in Java