杰瑞科技汇

Java获取Tomcat端口的正确方法是什么?

在 Java 中获取 Tomcat 的端口,最常用和最可靠的方法是通过 Tomcat 内置的 API,下面我将详细介绍几种方法,从推荐到不推荐,并解释其原理和适用场景。

Java获取Tomcat端口的正确方法是什么?-图1
(图片来源网络,侵删)

使用 org.apache.catalina.Server (最推荐)

这是最标准、最可靠的方法,因为它直接与 Tomcat 的核心组件交互,适用于任何将 Tomcat 作为嵌入式服务器或独立服务器运行的情况。

原理: Tomcat 的顶层组件是 Server,它包含一个或多个 Service,每个 Service 有一个或多个 Connector,而 Connector 就是负责处理网络连接(包括监听端口)的组件,通过获取 Server 实例,我们可以遍历其 Connector 来获取所有配置的端口。

代码示例:

这个方法在 Servlet 3.0+ 的环境中(Spring Boot, Spring MVC, 普通 WAR 部署)都适用。

Java获取Tomcat端口的正确方法是什么?-图2
(图片来源网络,侵删)
import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import javax.servlet.ServletContext;
import java.util.Arrays;
public class TomcatPortUtils {
    /**
     * 获取 Tomcat 的所有端口
     * @return 端口数组
     */
    public static int[] getTomcatPorts() {
        // 方法1: 从 ServletContext 获取 (适用于 Web 应用)
        Server server = getServerFromServletContext();
        if (server != null) {
            return getPortsFromServer(server);
        }
        // 方法2: 如果是嵌入式 Tomcat,可以直接传入 Tomcat 实例
        // (例如在 Spring Boot 的 TomcatServletWebServerApplicationContext 中)
        // Tomcat tomcat = getTomcatInstance(); // 你需要有自己的方式获取
        // if (tomcat != null) {
        //     return getPortsFromServer(tomcat.getServer());
        // }
        // 如果都获取不到,返回空数组
        return new int[0];
    }
    /**
     * 从 ServletContext 获取 Tomcat 的 Server 实例
     */
    private static Server getServerFromServletContext() {
        try {
            // 在 Tomcat 中,ServletContext 的getAttribute("org.apache.catalina.Server") 可以直接获取 Server 实例
            ServletContext servletContext = getServletContext(); // 你需要有自己的方式获取 ServletContext
            if (servletContext != null) {
                return (Server) servletContext.getAttribute("org.apache.catalina.Server");
            }
        } catch (Exception e) {
            // 忽略异常,可能不是 Tomcat 或 ServletContext 不可用
            e.printStackTrace();
        }
        return null;
    }
    /**
     * 从 Server 实例中解析出所有端口
     */
    private static int[] getPortsFromServer(Server server) {
        if (server == null) {
            return new int[0];
        }
        return server.findServices().stream()
                .flatMap(service -> Arrays.stream(service.findConnectors()))
                .map(Connector::getPort)
                .mapToInt(Integer::intValue)
                .toArray();
    }
    // --- 以下是辅助方法,用于演示 ---
    // 在实际应用中,你需要通过你的框架(如 Spring)来获取这些对象
    private static ServletContext getServletContext() {
        // 示例:在 Spring MVC Controller 中,可以直接注入或从 Request 获取
        // return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getServletContext();
        // 示例:在静态工具类中,可以通过 Spring 的 ApplicationContext 获取
        // WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();
        // return context.getServletContext();
        System.out.println("请在你的应用中替换此方法以获取真实的 ServletContext");
        return null;
    }
    public static void main(String[] args) {
        // 模拟一个 Tomcat 环境
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(8080);
        tomcat.getConnector(); // 默认创建一个 AJP/1.3 Connector 在端口 8009
        int[] ports = getPortsFromServer(tomcat.getServer());
        System.out.println("Tomcat 端口: " + Arrays.toString(ports)); // 输出: [8080, 8009]
    }
}

适用场景:

  • Spring Boot 应用
  • Spring MVC (WAR 部署) 应用
  • 任何使用 Tomcat 作为嵌入式服务器的应用
  • 任何部署在 Tomcat 上的传统 Web 应用

使用 org.apache.coyote.ajp.AjpProcessor (仅限 AJP 协议)

如果你的应用只关心 Tomcat 的 AJP 端口(通常用于与 Apache Nginx 等前端服务器集成),并且你确定 Tomcat 正在使用 AJP 协议,这个方法也可以工作。

原理: AJP 协议的处理器 AjpProcessor 中包含了它所监听的 endpoint,而 endpoint 知道端口号。

代码示例:

Java获取Tomcat端口的正确方法是什么?-图3
(图片来源网络,侵删)
import org.apache.coyote.ajp.AjpProcessor;
import org.apache.coyote.ajp.AbstractAjpProtocol;
import org.apache.coyote.ajp.AjpMessage;
import org.apache.coyote.RequestInfo;
import javax.servlet.ServletContext;
import java.lang.reflect.Field;
public class AjpPortFinder {
    public static int getAjpPort(ServletContext servletContext) {
        try {
            // 从 ServletContext 获取 CoyoteAdapter
            Object coyoteAdapter = servletContext.getAttribute("org.apache.catalina.adapter CoyoteAdapter");
            if (coyoteAdapter == null) {
                // 尝试另一种方式获取
                coyoteAdapter = servletContext.getAttribute("org.apache.catalina.core.StandardContext").getConnector().getCoyote();
            }
            // 从 CoyoteAdapter 获取 Processor (通过反射)
            Field processorField = coyoteAdapter.getClass().getDeclaredField("processor");
            processorField.setAccessible(true);
            AjpProcessor processor = (AjpProcessor) processorField.get(coyoteAdapter);
            // 从 Processor 获取 Protocol (通过反射)
            Field protocolField = processor.getClass().getDeclaredField("protocol");
            protocolField.setAccessible(true);
            AbstractAjpProtocol<?> protocol = (AbstractAjpProtocol<?>) protocolField.get(processor);
            // 从 Protocol 获取端口号
            return protocol.getEndpoint().getPort();
        } catch (Exception e) {
            e.printStackTrace();
            return -1; // 表示获取失败
        }
    }
    // public static void main(String[] args) {
    //     // 你需要获取真实的 ServletContext 实例
    //     // ServletContext context = ...; 
    //     // int ajpPort = getAjpPort(context);
    //     // System.out.println("AJP Port: " + ajpPort);
    // }
}

缺点:

  • 不通用:只适用于 AJP 协议,如果你的应用只使用 HTTP/HTTPS,此方法无效。
  • 依赖反射:使用了大量反射,代码脆弱,一旦 Tomcat 内部类结构发生变化,代码就会崩溃。
  • 获取复杂:需要从 ServletContext 开始,通过多个内部类才能最终拿到端口。

解析 server.xml (不推荐)

理论上,你可以读取 Tomcat 的配置文件 conf/server.xml 并用 XML 解析器来查找 <Connector> 元素的 port 属性。

为什么不推荐?

  1. 硬编码路径server.xml 的路径是固定的($CATALINA_HOME/conf/server.xml),但在不同的部署环境中(如 Docker、云服务器)可能难以确定。
  2. 权限问题:应用可能没有读取服务器文件系统的权限。
  3. 动态配置:Tomcat 的端口可以通过启动命令行参数(如 --port=9090)或环境变量来覆盖,server.xml 中的配置是无效的,但此方法会读取到错误的值。
  4. 性能开销:读取和解析文件比内存中查询对象要慢。

代码示例 (仅作演示):

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
public class ServerXmlParser {
    public static int[] getPortsFromServerXml(String tomcatHome) {
        try {
            File serverXmlFile = new File(tomcatHome, "conf/server.xml");
            if (!serverXmlFile.exists()) {
                System.err.println("server.xml not found at: " + serverXmlFile.getAbsolutePath());
                return new int[0];
            }
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(serverXmlFile);
            NodeList connectorNodes = document.getElementsByTagName("Connector");
            int[] ports = new int[connectorNodes.getLength()];
            for (int i = 0; i < connectorNodes.getLength(); i++) {
                Node node = connectorNodes.item(i);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    Element element = (Element) node;
                    String portStr = element.getAttribute("port");
                    if (!portStr.isEmpty()) {
                        ports[i] = Integer.parseInt(portStr);
                    }
                }
            }
            return ports;
        } catch (Exception e) {
            e.printStackTrace();
            return new int[0];
        }
    }
    // public static void main(String[] args) {
    //     // 需要传入 Tomcat 的安装目录
    //     String catalinaHome = System.getenv("CATALINA_HOME");
    //     if (catalinaHome == null) {
    //         System.out.println("Please set CATALINA_HOME environment variable.");
    //         return;
    //     }
    //     int[] ports = getPortsFromServerXml(catalinaHome);
    //     System.out.println("Ports from server.xml: " + Arrays.toString(ports));
    // }
}

解析启动参数 (有限制)

当 Tomcat 启动时,它会将监听端口等信息作为参数传递给 JVM,你可以通过 sun.management.VMManagement 的 API 来读取这些启动参数。

原理: Tomcat 的 Catalina 启动类会解析命令行参数,并将监听端口等设置到 SystemProperty 中。

代码示例:

import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.util.List;
public class StartupArgParser {
    public static int getPortFromStartupArgs() {
        RuntimeMXBean runtimeMxBean = ManagementFactory.getRuntimeMXBean();
        List<String> arguments = runtimeMxBean.getInputArguments();
        for (String arg : arguments) {
            if (arg.startsWith("-Dport=")) {
                return Integer.parseInt(arg.substring(7));
            }
            // Tomcat 的标准参数是 --port, 但通常会被转换为 -Dport
            if (arg.startsWith("--port=")) {
                return Integer.parseInt(arg.substring(7));
            }
        }
        return -1; // 未找到
    }
    // public static void main(String[] args) {
    //     // 需要以类似下面的方式启动 Tomcat 才能生效
    //     // catalina.sh run -Dport=9090
    //     int port = getPortFromStartupArgs();
    //     System.out.println("Port from startup args: " + port);
    // }
}

缺点:

  • 非常有限:此方法高度依赖于 Tomcat 的启动方式,在大多数应用服务器(如 JBoss/WildFly)或 Spring Boot 内嵌 Tomcat 的情况下,启动参数可能不会以这种方式设置。
  • 不可靠:不能保证一定能获取到端口信息。

总结与推荐

方法 优点 缺点 推荐度
org.apache.catalina.Server 最标准、最可靠、最通用,直接与 Tomcat 内核交互,不依赖文件或启动参数。 需要获取 ServletContextTomcat 实例。 ⭐⭐⭐⭐⭐ (首选)
AjpProcessor (反射) 可以获取 AJP 端口。 不通用(仅 AJP)、依赖反射、代码脆弱、获取复杂。 ⭐☆☆☆☆ (仅在特定场景下考虑)
解析 server.xml 思路简单。 路径硬编码、权限问题、无法获取动态端口、性能差。 ⭐☆☆☆☆ (强烈不推荐)
解析启动参数 可以获取命令行指定的端口。 非常有限、不可靠、依赖特定启动方式。 ⭐☆☆☆☆ (基本不适用)

最终结论:

请始终优先使用方法一 (org.apache.catalina.Server)。 它是获取 Tomcat 端口最健壮、最正确的方式,在你的 Java 应用(特别是基于 Spring 的应用)中,通过 ServletContext 或应用上下文(ApplicationContext)获取到 Server 实例,然后从中提取端口信息,这是最佳实践。

分享:
扫描分享到社交APP
上一篇
下一篇