最近接手了一个交接过来的老项目,用的还是 Tomcat 9,而新起的服务又要求跑在 Tomcat 11 上。开发机就一台,来回卸了装、装了卸,折腾两天后我实在忍不了了——干脆研究了一下怎么让两个版本在同一台电脑上和平共处。这篇文章就是把我实际操作中踩过的坑、验证过的方法完整记录下来,从下载解压到端口规划,从环境变量避坑到常见故障排查,希望能帮到同样被双版本折磨的人。先说结论:Tomcat 的多版本共存本身不难,难的是大多数教程只教你装单个实例,没人提醒你其实有两套独立配置在互相干扰。只要理解了 Tomcat 启动时到底依赖哪些目录和端口,后面所有操作都顺理成章。1. 为什么要在一台电脑上同时跑两个 Tomcat1.1 哪些场景真的需要双版本共存我身边最常见的两种情况,一是公司内部的老系统还跑在 Servlet 4.0 规范上,依赖 Tomcat 9 以及对应的 JDK 版本,而新项目已经切到 Jakarta EE 10/11 规范,必须用 Tomcat 11;二是开发人员接到版本兼容性测试任务,需要在本地模拟生产环境从 9 往 11 迁移时的行为差异。还有一种是纯个人学习场景:手头教程讲 Tomcat 9 的 JSP/Servlet 用法,但你自己想试试 Tomcat 11 的新特性。与其一台机器上反复卸载重装,不如让两个版本各占一个端口、各用一套目录,想用哪个用哪个,切换成本几乎为零。1.2 卡住大多数人的三个关键问题第一次尝试双版本共存时,我遇到的阻碍基本集中在这三件事上:端口冲突。默认情况下 Tomcat 会占用 8080 端口作为 HTTP 端口、8005 作为关闭指令端口、8009 作为 AJP 端口。两个实例都用默认配置,第二个必然启动失败。环境变量串扰。CATALINA_HOME只能指向一个 Tomcat 目录,如果全局设置只指向 Tomcat 9,那么启动 Tomcat 11 时依旧会去 9 的目录里找配置。很多报错看起来像Address already in use,实际根因却是配置目录串了。启动脚本找错。bin/startup.bat(Windows)或bin/startup.sh(Linux/macOS)默认读取的配置来源不同,如果不做任何处理,双击启动的是哪个实例完全取决于你当前的环境变量状态。把这三点想明白,后面的配置其实就是按图索骥。2. 理解 Tomcat 的目录结构与实例化机制2.1 Tomcat 启动时到底在找什么Tomcat 的安装目录里,最重要的几个文件夹分别是conf(配置文件)、webapps(部署目录)、logs(日志)、temp(临时文件)和work(JSP 编译缓存)。启动时,Tomcat 脚本会按照目录约定去加载这些路径下的内容。这里有一个容易忽略的细节:默认情况下,CATALINA_HOME指向整个 Tomcat 安装根目录,实例相关的所有路径也都从CATALINA_HOME派生。如果你设置了全局CATALINA_HOME,那么无论你执行哪个版本目录下的startup.sh,脚本都可能因为环境变量优先级的关系,实际使用的是环境变量里指定的那个 Tomcat。举个例子:你的环境变量CATALINA_HOME=E:\dev\tomcat9,你从E:\dev\tomcat11\bin目录下手动执行startup.bat,但脚本内部如果先读取了系统环境变量,那最终加载的可能是tomcat9的配置和部署目录。这种串扰非常隐蔽,报错还常常指向端口冲突或者找不到JAVA_HOME,排查起来很费时间。2.2 双实例共存的三个隔离原则知道了 Tomcat 会去找什么,就能提炼出三个共存原则:目录隔离。两个版本必须安装在不同目录,且目录名中不要出现空格或中文。我在某台 Windows 机器上见过把 Tomcat 解压到D:\Program Files\Tomcat 11的情况,后面写脚本和改配置文件时各种转义问题层出不穷,建议直接用D:\dev\tomcat9和D:\dev\tomcat11这种结构。端口隔离。两个实例监听的端口必须完全不同。不只是 HTTP 端口,shutdown端口和 AJP 端口也要改。环境变量隔离。要么完全不依赖全局CATALINA_HOME,要么为不同实例分别指定CATALINA_BASE。后者更像是生产环境的标准做法,本地开发用前者反而更省心。这三个原则落实到位,双版本共存的问题就解决了一大半。3. 动手前的准备:JDK 版本匹配与 Tomcat 下载3.1 先确认你的 JDK 能不能带得动 Tomcat 11Tomcat 9 要求 JDK 8 及以上,Tomcat 11 则要求 JDK 17 及以上。这是硬性门槛,如果本机只有 JDK 8,那 Tomcat 11 根本起不来。建议在安装前先执行java -version看一下当前版本。如果本机装了多个 JDK,那么需要保证JAVA_HOME指向的版本满足当前要启动的 Tomcat 要求。这里又出现一个常见误区:有人装了 JDK 17,以为 Tomcat 9 跑不了,其实 Tomcat 9 在 JDK 17 下也能运行,只是它原本的设计目标更多对应 JDK 8/11 时代。反过来,Tomcat 11 对 JDK 版本的要求则严格得多,低于 17 会直接报UnsupportedClassVersionError或启动脚本中的版本校验失败。实际操作中,我建议至少安装 JDK 17。这样 Tomcat 9 和 Tomcat 11 都能启动,Tomcat 9 的旧项目如果对 JDK 版本有特殊要求,再考虑额外装一个 JDK 8 单独给 9 用。3.2 下载 ZIP 版而不是安装版从官方站点下载 Tomcat 时,注意选择Core分类下的zip压缩包。Windows 的 exe 安装版会注册系统服务,对于多版本共存来说反而碍事——服务模式下每次只能启动一个注册过的实例,除非你还得去手动改服务参数。ZIP 版解压即用,想启动哪个就进哪个目录执行启动脚本,互不干扰。下载完成后,目录规划我建议这样:D:\dev ├── jdk17 ├── tomcat9 │ ├── bin │ ├── conf │ └── webapps └── tomcat11 ├── bin ├── conf └── webapps两个目录级别相同,不存在嵌套关系。这样从路径上一眼就能看出哪个是 9、哪个是 11,后面改配置文件也不容易搞混。3.3 解压后先检查 bin 目录ZIP 包解压后,先确认bin目录下存在startup.bat(Windows)或startup.sh(Linux/macOS),以及catalina.bat或catalina.sh。有些发行版会把启动脚本放在额外层级中,比如解压后多套了一层同名文件夹,这种情况稍不注意就会因为路径错误而报无法找到脚本。检查命令如下(Windows PowerShell 示例):cd D:\dev\tomcat11\bin Test-Path .\startup.bat Test-Path .\catalina.bat返回True就说明路径没问题。4. 端口规划:修改 server.xml 的关键动作4.1 一张表理清三个端口每个 Tomcat 实例的conf\server.xml中有三处端口需要修改:端口用途Tomcat 9 默认值Tomcat 11 建议值修改位置关闭指令端口80058006<Server port="8006" shutdown="SHUTDOWN">HTTP/1.1 连接端口80808081<Connector port="8081" ...>AJP 连接端口80098010<Connector port="8010" protocol="AJP/1.3" ...>端口不是只能固定用 8081,只要避开本机已占用端口就行。但要注意 8006、8010 这种偶数值不是钦定的,只是我习惯这样规划,核心原则是两台实例之间以及和系统其他服务之间不冲突。检测端口是否已被占用,Windows 下用:netstat -ano | findstr :8081Linux/macOS 下用:lsof -i :8081如果没有任何输出,说明该端口可用。4.2 手动编辑 server.xml 的完整流程用文本编辑器打开D:\dev\tomcat11\conf\server.xml,依次定位以下几处。第一处是 Server 标签:<Server port="8006" shutdown="SHUTDOWN">第二处是 HTTP Connector:<Connector port="8081" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxParameterCount="1000" />第三处是 AJP Connector(如果不需要 AJP 协议,可以直接注释掉这一整段,但初学阶段建议还是改端口保留):<Connector port="8010" protocol="AJP/1.3" redirectPort="8443" />注意redirectPort="8443"这一项。它表示当请求需要 HTTPS 重定向时使用的端口,默认是 8443。因为 Tomcat 9 和 11 的 HTTP Connector 都默认指向 8443,如果两个实例同时启用 HTTPS 重定向,这里又会冲突。稳妥起见,Tomcat 9 保持原来的 8443,Tomcat 11 的改成 8444,顺手把redirectPort也隔离掉。修改完保存,再检查 Tomcat 9 的server.xml确认三处端口仍为默认值。这样后续启动时,9 占用 8080/8005/8009,11 占用 8081/8006/8010,各走各的。4.3 验证 server.xml 是否改对改完后不要急着启动,可以先快速搜索一遍文件里的端口号,确认没有遗漏。Select-String -Path D:\dev\tomcat11\conf\server.xml -Pattern "8080|8005|8009"如果还有输出,说明漏改了某处。正常情况下这条命令应该只匹配到注释或非连接器位置。这一步能省下你后面排查端口冲突的一堆时间。5. 环境变量处理与两个版本的启动方式5.1 最稳妥的办法:不配置全局 CATALINA_HOME如果你只是本地开发用,我的建议是根本不设置全局的CATALINA_HOME环境变量。Windows 下直接进入对应版本的bin目录双击startup.bat,Linux/macOS 下进入bin目录后执行./startup.sh,脚本会以当前所在目录推导出 HOME 路径。这样做的好处是不会出现两个版本互相抢环境变量的问题。缺点是不能在任意目录下通过命令直接启动,但本地开发很少需要这个便利。如果你已经设置了全局CATALINA_HOME,最省事的做法是删掉它。在 Windows 中:[System.Environment]::SetEnvironmentVariable("CATALINA_HOME", $null, "User")然后重新打开一个终端窗口,让修改生效。Linux 下编辑~/.bashrc或~/.zshrc,删掉对应的export CATALINA_HOME=...行,再执行source ~/.bashrc。5.2 用 CATALINA_BASE 实现更规范的双实例管理如果你希望保持全局CATALINA_HOME指向其中一个版本,同时还想在同一版本下跑多个实例,那么应该使用CATALINA_BASE机制。CATALINA_BASE指向实例目录,实例目录里只需要有conf、logs、temp、webapps、work这几个子目录;而CATALINA_HOME指向的是二进制和公共库所在目录。启动脚本加载时,先用CATALINA_BASE找配置和部署目录,再用CATALINA_HOME找类库和二进制文件。具体操作是:在某个目录下创建实例骨架目录,比如D:\instance\tomcat11-app1,把 Tomcat 11 的conf目录完整复制过去,并新建logs、temp、webapps、work空目录。然后修改实例目录下的conf\server.xml端口。启动时执行:set CATALINA_BASE=D:\instance\tomcat11-app1 set CATALINA_HOME=D:\dev\tomcat11 D:\dev\tomcat11\bin\catalina.bat run这种方法比较贴近生产环境的多实例部署方式。但假如只是为了本机同时跑 9 和 11,我仍然推荐 5.1 的不设置全局变量的方案——最简单,也最不容易出错。5.3 启动时的内存参数调整两台实例同时运行会占用双份 JVM 内存。默认情况下 Tomcat 分配的堆内存上限可能都是自动估算的,如果本机内存不够,两个实例一起跑会导致频繁 GC,甚至直接启动失败。需要调整时,Windows 下编辑bin\catalina.bat,找到JAVA_OPTS位置,设置:set "JAVA_OPTS=-Xms256m -Xmx512m"Linux/macOS 下编辑bin\catalina.sh,设置为:JAVA_OPTS="-Xms256m -Xmx512m"如果是临时调试,也可以在启动前手动在终端设置环境变量再执行脚本,效果等同于修改文件。但要注意,两个实例的catalina.bat是各自独立的,需要分别调整。6. 启动验证与实战中的高频故障排查6.1 启动后的验证清单两个 Tomcat 都执行完启动脚本后,依次检查以下项目:浏览器访问http://localhost:8080,应该看到 Tomcat 9 的默认首页。浏览器访问http://localhost:8081,应该看到 Tomcat 11 的默认首页。查看两个logs\catalina.out(Windows 下可能是logs\catalina.<日期>.log),确认启动日志中没有SEVERE或ERROR级别记录。执行netstat -ano | findstr "8080 8081"确认两个端口都在LISTENING状态。如果首页打不开,先不要怀疑配置,按下面的排查顺序走一遍。6.2 地址被占用和端口冲突这类报错通常是:SEVERE: Failed to initialize end point associated with ProtocolHandler Caused by: java.net.BindException: Address already in use: JVM_Bind解决办法很简单:确认哪个进程占用了目标端口,换一个端口。分不清是哪一个 Tomcat 占用了端口时,用:netstat -ano | findstr :8080最后一列是 PID,再用:tasklist | findstr 1234就能看到具体进程名。如果显示的是java.exe,那就是某个 Java 进程占用了端口,进一步确认是哪个 Tomcat 实例可以从启动脚本的窗口标题或日志目录来判断。6.3 启动脚本瞬间消失,没有任何日志双击startup.bat后窗口一闪而过,说明脚本启动失败,常见原因有三个。第一个是JAVA_HOME没设置或指向了不存在的目录。Tomcat 启动脚本在找到JAVA_HOME之前不会做任何事,窗口会直接关闭。检查方法是在终端里执行:echo %JAVA_HOME%为空或者路径不对,就去设置一下。第二个是解压目录路径中有特殊字符。比如路径里包含空格,脚本解析时可能出现问题。虽然现代 Tomcat 对空格的处理已经好了很多,但为了省心,还是建议把两个目录放在简洁路径下。第三个是你同时执行了 Tomcat 9 和 Tomcat 11 的startup.bat且全局CATALINA_HOME指向了 9。这时候 11 的启动脚本可能去找 9 的配置目录,加载完 9 的 server.xml 后发现 8080 已被 9 自己占用,于是报错退出。解决办法就是上面说的:删掉全局CATALINA_HOME,或者给 11 单独设置CATALINA_BASE。排查这类问题时,最快的办法是不要用双击启动,而是打开终端,在bin目录下执行:.\startup.bat这样错误信息会保留在终端里,而不是一闪而过。6.4 Tomcat 11 启动时报 UnsupportedClassVersionError报错原因基本就是 JDK 版本太低。Tomcat 11 需要 JDK 17 以上,如果本机有多个 JDK,而JAVA_HOME指向了 JDK 8,就会触发这个错误。解决办法是在启动前临时指定 JDK 路径。Windows 下:set JAVA_HOME=D:\dev\jdk17Linux 下:export JAVA_HOME=/usr/lib/jvm/jdk-17然后再执行启动脚本。确认 Tomcat 9 和 11 分别使用哪个 JDK,是双版本共存中很容易忽略的一环,建议提前规划好。6.5 日志文件里出现大量乱码Windows 下启动 Tomcat 时,控制台和日志文件经常出现中文乱码。这是因为 Tomcat 日志输出默认按 UTF-8 编码,而 Windows 控制台代码页可能是 GBK。解决方案是在conf\logging.properties中修改字符编码相关配置,或者把系统区域设置调整为使用 UTF-8。不过这个现象不影响服务本身运行,如果只是本地开发用,可以暂时不管。真要处理时,优先检查java.util.logging.ConsoleHandler.encoding的值,把默认的 UTF-8 改成 GBK 或注释掉让系统自行决定即可。7. 日常使用中的管理技巧与个人操作习惯7.1 Windows 下给两个 Tomcat 分别创建快捷启动既然两个版本装在不同目录,最直接的管理方式就是把两个bin\startup.bat各发送一个快捷方式到桌面,分别重命名成Tomcat9和Tomcat11。启动时双击对应的快捷方式即可。停止服务时,优先使用bin\shutdown.bat而不是直接关闭终端窗口。直接关窗口有时会导致端口没有及时释放,下一次启动时报Address already in use。习惯了用正式脚本启动和关闭,能少踩很多莫名其妙的坑。7.2 借助 IDE 的服务器配置统一管理双实例如果你日常是用集成开发环境(IDE)开发,比如 Eclipse 或 IntelliJ IDEA,那可以直接在 IDE 里定义两个 Tomcat Server,一个指向 Tomcat 9 目录,一个指向 Tomcat 11 目录,各自指定 HTTP 端口。IDE 会负责管理进程生命周期,不需要你手动去执行启动脚本。在 IDE 里配置时,有个细节值得注意:它读取的还是conf\server.xml中的端口,所以你得保证 IDE 里填的端口和 server.xml 里一致,否则 IDE 会在启动时尝试自动修改配置,反而容易产生意外状态。我之前在 IDE 里同时启过两个实例,就因为 IDE 自动改了端口,导致其中一个项目的访问路径全变了。现在我的习惯是:IDE 配置里只填目录路径,端口完全交给 server.xml 控制,IDE 启动前先提醒自己确认一下端口不被占用。7.3 部署同一应用到两个版本进行对比验证如果你需要用同一个 Web 应用分别部署到 Tomcat 9 和 Tomcat 11 上做验证,直接把打包好的 WAR 文件复制到两个webapps目录下就行。注意两个实例的webapps是相互独立的,别把 WAR 复制错了目录。部署完成后,通过http://localhost:8080/your-app和http://localhost:8081/your-app分别访问。如果应用使用了 Servlet API 或 JSP 相关库,在 Tomcat 11 上可能会遇到包名变更导致的问题,比如javax.servlet变成jakarta.servlet。这是从 Tomcat 9 到 11 最典型的迁移点,值得单独写一个排查清单,不过那是另外的话题了。7.4 我个人偏爱的目录命名约定经过几次折腾,我现在的本机环境基本长这样:D:\dev ├── jdk8 ├── jdk17 ├── tomcat9 (搭配 jdk8 或 jdk17,端口 8080) └── tomcat11 (搭配 jdk17,端口 8081)目录名里带版本号,一眼就能看懂,也不容易在 IDE 配置里选错。双版本共存没有想象中复杂,核心就是目录独立、端口独立、环境变量不串味。如果你也被两个 Tomcat 版本折腾过,欢迎按上面的步骤试一遍。配置过程中如果遇到和我不一样的报错,大概率是卡在环境变量或端口占用上,优先查这两处,基本都能解决。
最近接手了一个交接过来的老项目,用的还是 Tomcat 9,而新起的服务又要求跑在 Tomcat 11 上。开发机就一台,来回卸了装、装了卸,折腾两天后我实在忍不了了——干脆研究了一下怎么让两个版本在同一台电脑上和平共处。这篇文章就是把我实际操作中踩过的坑、验证过的方法完整记录下来,从下载解压到端口规划,从环境变量避坑到常见故障排查,希望能帮到同样被双版本折磨的人。先说结论:Tomcat 的多版本共存本身不难,难的是大多数教程只教你装单个实例,没人提醒你其实有两套独立配置在互相干扰。只要理解了 Tomcat 启动时到底依赖哪些目录和端口,后面所有操作都顺理成章。1. 为什么要在一台电脑上同时跑两个 Tomcat1.1 哪些场景真的需要双版本共存我身边最常见的两种情况,一是公司内部的老系统还跑在 Servlet 4.0 规范上,依赖 Tomcat 9 以及对应的 JDK 版本,而新项目已经切到 Jakarta EE 10/11 规范,必须用 Tomcat 11;二是开发人员接到版本兼容性测试任务,需要在本地模拟生产环境从 9 往 11 迁移时的行为差异。还有一种是纯个人学习场景:手头教程讲 Tomcat 9 的 JSP/Servlet 用法,但你自己想试试 Tomcat 11 的新特性。与其一台机器上反复卸载重装,不如让两个版本各占一个端口、各用一套目录,想用哪个用哪个,切换成本几乎为零。1.2 卡住大多数人的三个关键问题第一次尝试双版本共存时,我遇到的阻碍基本集中在这三件事上:端口冲突。默认情况下 Tomcat 会占用 8080 端口作为 HTTP 端口、8005 作为关闭指令端口、8009 作为 AJP 端口。两个实例都用默认配置,第二个必然启动失败。环境变量串扰。CATALINA_HOME只能指向一个 Tomcat 目录,如果全局设置只指向 Tomcat 9,那么启动 Tomcat 11 时依旧会去 9 的目录里找配置。很多报错看起来像Address already in use,实际根因却是配置目录串了。启动脚本找错。bin/startup.bat(Windows)或bin/startup.sh(Linux/macOS)默认读取的配置来源不同,如果不做任何处理,双击启动的是哪个实例完全取决于你当前的环境变量状态。把这三点想明白,后面的配置其实就是按图索骥。2. 理解 Tomcat 的目录结构与实例化机制2.1 Tomcat 启动时到底在找什么Tomcat 的安装目录里,最重要的几个文件夹分别是conf(配置文件)、webapps(部署目录)、logs(日志)、temp(临时文件)和work(JSP 编译缓存)。启动时,Tomcat 脚本会按照目录约定去加载这些路径下的内容。这里有一个容易忽略的细节:默认情况下,CATALINA_HOME指向整个 Tomcat 安装根目录,实例相关的所有路径也都从CATALINA_HOME派生。如果你设置了全局CATALINA_HOME,那么无论你执行哪个版本目录下的startup.sh,脚本都可能因为环境变量优先级的关系,实际使用的是环境变量里指定的那个 Tomcat。举个例子:你的环境变量CATALINA_HOME=E:\dev\tomcat9,你从E:\dev\tomcat11\bin目录下手动执行startup.bat,但脚本内部如果先读取了系统环境变量,那最终加载的可能是tomcat9的配置和部署目录。这种串扰非常隐蔽,报错还常常指向端口冲突或者找不到JAVA_HOME,排查起来很费时间。2.2 双实例共存的三个隔离原则知道了 Tomcat 会去找什么,就能提炼出三个共存原则:目录隔离。两个版本必须安装在不同目录,且目录名中不要出现空格或中文。我在某台 Windows 机器上见过把 Tomcat 解压到D:\Program Files\Tomcat 11的情况,后面写脚本和改配置文件时各种转义问题层出不穷,建议直接用D:\dev\tomcat9和D:\dev\tomcat11这种结构。端口隔离。两个实例监听的端口必须完全不同。不只是 HTTP 端口,shutdown端口和 AJP 端口也要改。环境变量隔离。要么完全不依赖全局CATALINA_HOME,要么为不同实例分别指定CATALINA_BASE。后者更像是生产环境的标准做法,本地开发用前者反而更省心。这三个原则落实到位,双版本共存的问题就解决了一大半。3. 动手前的准备:JDK 版本匹配与 Tomcat 下载3.1 先确认你的 JDK 能不能带得动 Tomcat 11Tomcat 9 要求 JDK 8 及以上,Tomcat 11 则要求 JDK 17 及以上。这是硬性门槛,如果本机只有 JDK 8,那 Tomcat 11 根本起不来。建议在安装前先执行java -version看一下当前版本。如果本机装了多个 JDK,那么需要保证JAVA_HOME指向的版本满足当前要启动的 Tomcat 要求。这里又出现一个常见误区:有人装了 JDK 17,以为 Tomcat 9 跑不了,其实 Tomcat 9 在 JDK 17 下也能运行,只是它原本的设计目标更多对应 JDK 8/11 时代。反过来,Tomcat 11 对 JDK 版本的要求则严格得多,低于 17 会直接报UnsupportedClassVersionError或启动脚本中的版本校验失败。实际操作中,我建议至少安装 JDK 17。这样 Tomcat 9 和 Tomcat 11 都能启动,Tomcat 9 的旧项目如果对 JDK 版本有特殊要求,再考虑额外装一个 JDK 8 单独给 9 用。3.2 下载 ZIP 版而不是安装版从官方站点下载 Tomcat 时,注意选择Core分类下的zip压缩包。Windows 的 exe 安装版会注册系统服务,对于多版本共存来说反而碍事——服务模式下每次只能启动一个注册过的实例,除非你还得去手动改服务参数。ZIP 版解压即用,想启动哪个就进哪个目录执行启动脚本,互不干扰。下载完成后,目录规划我建议这样:D:\dev ├── jdk17 ├── tomcat9 │ ├── bin │ ├── conf │ └── webapps └── tomcat11 ├── bin ├── conf └── webapps两个目录级别相同,不存在嵌套关系。这样从路径上一眼就能看出哪个是 9、哪个是 11,后面改配置文件也不容易搞混。3.3 解压后先检查 bin 目录ZIP 包解压后,先确认bin目录下存在startup.bat(Windows)或startup.sh(Linux/macOS),以及catalina.bat或catalina.sh。有些发行版会把启动脚本放在额外层级中,比如解压后多套了一层同名文件夹,这种情况稍不注意就会因为路径错误而报无法找到脚本。检查命令如下(Windows PowerShell 示例):cd D:\dev\tomcat11\bin Test-Path .\startup.bat Test-Path .\catalina.bat返回True就说明路径没问题。4. 端口规划:修改 server.xml 的关键动作4.1 一张表理清三个端口每个 Tomcat 实例的conf\server.xml中有三处端口需要修改:端口用途Tomcat 9 默认值Tomcat 11 建议值修改位置关闭指令端口80058006<Server port="8006" shutdown="SHUTDOWN">HTTP/1.1 连接端口80808081<Connector port="8081" ...>AJP 连接端口80098010<Connector port="8010" protocol="AJP/1.3" ...>端口不是只能固定用 8081,只要避开本机已占用端口就行。但要注意 8006、8010 这种偶数值不是钦定的,只是我习惯这样规划,核心原则是两台实例之间以及和系统其他服务之间不冲突。检测端口是否已被占用,Windows 下用:netstat -ano | findstr :8081Linux/macOS 下用:lsof -i :8081如果没有任何输出,说明该端口可用。4.2 手动编辑 server.xml 的完整流程用文本编辑器打开D:\dev\tomcat11\conf\server.xml,依次定位以下几处。第一处是 Server 标签:<Server port="8006" shutdown="SHUTDOWN">第二处是 HTTP Connector:<Connector port="8081" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxParameterCount="1000" />第三处是 AJP Connector(如果不需要 AJP 协议,可以直接注释掉这一整段,但初学阶段建议还是改端口保留):<Connector port="8010" protocol="AJP/1.3" redirectPort="8443" />注意redirectPort="8443"这一项。它表示当请求需要 HTTPS 重定向时使用的端口,默认是 8443。因为 Tomcat 9 和 11 的 HTTP Connector 都默认指向 8443,如果两个实例同时启用 HTTPS 重定向,这里又会冲突。稳妥起见,Tomcat 9 保持原来的 8443,Tomcat 11 的改成 8444,顺手把redirectPort也隔离掉。修改完保存,再检查 Tomcat 9 的server.xml确认三处端口仍为默认值。这样后续启动时,9 占用 8080/8005/8009,11 占用 8081/8006/8010,各走各的。4.3 验证 server.xml 是否改对改完后不要急着启动,可以先快速搜索一遍文件里的端口号,确认没有遗漏。Select-String -Path D:\dev\tomcat11\conf\server.xml -Pattern "8080|8005|8009"如果还有输出,说明漏改了某处。正常情况下这条命令应该只匹配到注释或非连接器位置。这一步能省下你后面排查端口冲突的一堆时间。5. 环境变量处理与两个版本的启动方式5.1 最稳妥的办法:不配置全局 CATALINA_HOME如果你只是本地开发用,我的建议是根本不设置全局的CATALINA_HOME环境变量。Windows 下直接进入对应版本的bin目录双击startup.bat,Linux/macOS 下进入bin目录后执行./startup.sh,脚本会以当前所在目录推导出 HOME 路径。这样做的好处是不会出现两个版本互相抢环境变量的问题。缺点是不能在任意目录下通过命令直接启动,但本地开发很少需要这个便利。如果你已经设置了全局CATALINA_HOME,最省事的做法是删掉它。在 Windows 中:[System.Environment]::SetEnvironmentVariable("CATALINA_HOME", $null, "User")然后重新打开一个终端窗口,让修改生效。Linux 下编辑~/.bashrc或~/.zshrc,删掉对应的export CATALINA_HOME=...行,再执行source ~/.bashrc。5.2 用 CATALINA_BASE 实现更规范的双实例管理如果你希望保持全局CATALINA_HOME指向其中一个版本,同时还想在同一版本下跑多个实例,那么应该使用CATALINA_BASE机制。CATALINA_BASE指向实例目录,实例目录里只需要有conf、logs、temp、webapps、work这几个子目录;而CATALINA_HOME指向的是二进制和公共库所在目录。启动脚本加载时,先用CATALINA_BASE找配置和部署目录,再用CATALINA_HOME找类库和二进制文件。具体操作是:在某个目录下创建实例骨架目录,比如D:\instance\tomcat11-app1,把 Tomcat 11 的conf目录完整复制过去,并新建logs、temp、webapps、work空目录。然后修改实例目录下的conf\server.xml端口。启动时执行:set CATALINA_BASE=D:\instance\tomcat11-app1 set CATALINA_HOME=D:\dev\tomcat11 D:\dev\tomcat11\bin\catalina.bat run这种方法比较贴近生产环境的多实例部署方式。但假如只是为了本机同时跑 9 和 11,我仍然推荐 5.1 的不设置全局变量的方案——最简单,也最不容易出错。5.3 启动时的内存参数调整两台实例同时运行会占用双份 JVM 内存。默认情况下 Tomcat 分配的堆内存上限可能都是自动估算的,如果本机内存不够,两个实例一起跑会导致频繁 GC,甚至直接启动失败。需要调整时,Windows 下编辑bin\catalina.bat,找到JAVA_OPTS位置,设置:set "JAVA_OPTS=-Xms256m -Xmx512m"Linux/macOS 下编辑bin\catalina.sh,设置为:JAVA_OPTS="-Xms256m -Xmx512m"如果是临时调试,也可以在启动前手动在终端设置环境变量再执行脚本,效果等同于修改文件。但要注意,两个实例的catalina.bat是各自独立的,需要分别调整。6. 启动验证与实战中的高频故障排查6.1 启动后的验证清单两个 Tomcat 都执行完启动脚本后,依次检查以下项目:浏览器访问http://localhost:8080,应该看到 Tomcat 9 的默认首页。浏览器访问http://localhost:8081,应该看到 Tomcat 11 的默认首页。查看两个logs\catalina.out(Windows 下可能是logs\catalina.<日期>.log),确认启动日志中没有SEVERE或ERROR级别记录。执行netstat -ano | findstr "8080 8081"确认两个端口都在LISTENING状态。如果首页打不开,先不要怀疑配置,按下面的排查顺序走一遍。6.2 地址被占用和端口冲突这类报错通常是:SEVERE: Failed to initialize end point associated with ProtocolHandler Caused by: java.net.BindException: Address already in use: JVM_Bind解决办法很简单:确认哪个进程占用了目标端口,换一个端口。分不清是哪一个 Tomcat 占用了端口时,用:netstat -ano | findstr :8080最后一列是 PID,再用:tasklist | findstr 1234就能看到具体进程名。如果显示的是java.exe,那就是某个 Java 进程占用了端口,进一步确认是哪个 Tomcat 实例可以从启动脚本的窗口标题或日志目录来判断。6.3 启动脚本瞬间消失,没有任何日志双击startup.bat后窗口一闪而过,说明脚本启动失败,常见原因有三个。第一个是JAVA_HOME没设置或指向了不存在的目录。Tomcat 启动脚本在找到JAVA_HOME之前不会做任何事,窗口会直接关闭。检查方法是在终端里执行:echo %JAVA_HOME%为空或者路径不对,就去设置一下。第二个是解压目录路径中有特殊字符。比如路径里包含空格,脚本解析时可能出现问题。虽然现代 Tomcat 对空格的处理已经好了很多,但为了省心,还是建议把两个目录放在简洁路径下。第三个是你同时执行了 Tomcat 9 和 Tomcat 11 的startup.bat且全局CATALINA_HOME指向了 9。这时候 11 的启动脚本可能去找 9 的配置目录,加载完 9 的 server.xml 后发现 8080 已被 9 自己占用,于是报错退出。解决办法就是上面说的:删掉全局CATALINA_HOME,或者给 11 单独设置CATALINA_BASE。排查这类问题时,最快的办法是不要用双击启动,而是打开终端,在bin目录下执行:.\startup.bat这样错误信息会保留在终端里,而不是一闪而过。6.4 Tomcat 11 启动时报 UnsupportedClassVersionError报错原因基本就是 JDK 版本太低。Tomcat 11 需要 JDK 17 以上,如果本机有多个 JDK,而JAVA_HOME指向了 JDK 8,就会触发这个错误。解决办法是在启动前临时指定 JDK 路径。Windows 下:set JAVA_HOME=D:\dev\jdk17Linux 下:export JAVA_HOME=/usr/lib/jvm/jdk-17然后再执行启动脚本。确认 Tomcat 9 和 11 分别使用哪个 JDK,是双版本共存中很容易忽略的一环,建议提前规划好。6.5 日志文件里出现大量乱码Windows 下启动 Tomcat 时,控制台和日志文件经常出现中文乱码。这是因为 Tomcat 日志输出默认按 UTF-8 编码,而 Windows 控制台代码页可能是 GBK。解决方案是在conf\logging.properties中修改字符编码相关配置,或者把系统区域设置调整为使用 UTF-8。不过这个现象不影响服务本身运行,如果只是本地开发用,可以暂时不管。真要处理时,优先检查java.util.logging.ConsoleHandler.encoding的值,把默认的 UTF-8 改成 GBK 或注释掉让系统自行决定即可。7. 日常使用中的管理技巧与个人操作习惯7.1 Windows 下给两个 Tomcat 分别创建快捷启动既然两个版本装在不同目录,最直接的管理方式就是把两个bin\startup.bat各发送一个快捷方式到桌面,分别重命名成Tomcat9和Tomcat11。启动时双击对应的快捷方式即可。停止服务时,优先使用bin\shutdown.bat而不是直接关闭终端窗口。直接关窗口有时会导致端口没有及时释放,下一次启动时报Address already in use。习惯了用正式脚本启动和关闭,能少踩很多莫名其妙的坑。7.2 借助 IDE 的服务器配置统一管理双实例如果你日常是用集成开发环境(IDE)开发,比如 Eclipse 或 IntelliJ IDEA,那可以直接在 IDE 里定义两个 Tomcat Server,一个指向 Tomcat 9 目录,一个指向 Tomcat 11 目录,各自指定 HTTP 端口。IDE 会负责管理进程生命周期,不需要你手动去执行启动脚本。在 IDE 里配置时,有个细节值得注意:它读取的还是conf\server.xml中的端口,所以你得保证 IDE 里填的端口和 server.xml 里一致,否则 IDE 会在启动时尝试自动修改配置,反而容易产生意外状态。我之前在 IDE 里同时启过两个实例,就因为 IDE 自动改了端口,导致其中一个项目的访问路径全变了。现在我的习惯是:IDE 配置里只填目录路径,端口完全交给 server.xml 控制,IDE 启动前先提醒自己确认一下端口不被占用。7.3 部署同一应用到两个版本进行对比验证如果你需要用同一个 Web 应用分别部署到 Tomcat 9 和 Tomcat 11 上做验证,直接把打包好的 WAR 文件复制到两个webapps目录下就行。注意两个实例的webapps是相互独立的,别把 WAR 复制错了目录。部署完成后,通过http://localhost:8080/your-app和http://localhost:8081/your-app分别访问。如果应用使用了 Servlet API 或 JSP 相关库,在 Tomcat 11 上可能会遇到包名变更导致的问题,比如javax.servlet变成jakarta.servlet。这是从 Tomcat 9 到 11 最典型的迁移点,值得单独写一个排查清单,不过那是另外的话题了。7.4 我个人偏爱的目录命名约定经过几次折腾,我现在的本机环境基本长这样:D:\dev ├── jdk8 ├── jdk17 ├── tomcat9 (搭配 jdk8 或 jdk17,端口 8080) └── tomcat11 (搭配 jdk17,端口 8081)目录名里带版本号,一眼就能看懂,也不容易在 IDE 配置里选错。双版本共存没有想象中复杂,核心就是目录独立、端口独立、环境变量不串味。如果你也被两个 Tomcat 版本折腾过,欢迎按上面的步骤试一遍。配置过程中如果遇到和我不一样的报错,大概率是卡在环境变量或端口占用上,优先查这两处,基本都能解决。