一、说说为什么要折腾这次升级

很多朋友一听到升级应用服务器就头疼,觉得老项目跑得好好的,动它干嘛。其实我们遇到的情况非常典型:老版本 Tomcat 身上的问题越来越多,比如性能跟不上、安全漏洞不好补、新功能没法用。尤其现在网络安全要求越来越严格,那些公开的漏洞如果不能及时打上补丁,对线上服务就是一颗定时炸弹。另外,新版本的 Tomcat 在资源占用、并发处理上做了很多优化,升级后能明显感觉到服务更轻快。这不仅是“追新”,更是一种现实需求。

从实际场景看,升级通常发生在下面几种情况:

  • 老项目要部署到新的服务器环境,而机房环境只提供了新版本的 Tomcat。
  • 客户安全检查报告要求必须修复某个和 Tomcat 版本相关的漏洞,否则不给上线。
  • 团队想在项目里用上新的 Servlet 特性,例如异步请求、HTTP/2 支持,这些在老版本上没法用。
  • 原来的服务器负载太高,老版本调优到极限也扛不住,新版本对高并发更友好。

在这些情况下,我们都得硬着头皮把 Tomcat 从旧版本迁到新版本。麻烦的是,直接替换安装目录往往不够,因为项目代码和配置文件里可能藏着很多只有旧版本才认识的东西。

二、升级前需要搞清楚的关键概念

2.1 Tomcat 和 Servlet 规范的关系

Tomcat 本质上是一个“Servlet 容器”。它实现的是一套叫 Servlet 的 Java 标准规范。不同版本的 Tomcat,对应的规范版本也不一样。比如 Tomcat 8.5 支持 Servlet 3.1,Tomcat 9 支持 Servlet 4.0,Tomcat 10.1 支持 Servlet 6.0,到了 Tomcat 11 已经对应 Servlet 6.1。这个对应关系特别重要,因为项目里那些过滤器、监听器、控制器,本质上都是在照着某个 Servlet 版本写代码。

如果旧项目是用 Servlet 3.0 写的,你换成 Tomcat 9 可能没啥大问题,可是直接跳到 Tomcat 10.1,问题就来了。为什么?因为从 Tomcat 10 开始,官方把 Java EE 的包名从 javax.* 改成了 jakarta.*。这意味着你原来写好的 import javax.servlet.http.HttpServlet; 全都要变成 import jakarta.servlet.http.HttpServlet;。这不是改一个配置就能带过去的,而是要动源代码。

2.2 配置文件的版本声明

除了代码里 import 变了,web.xml 这个老熟人也需要换新的头信息。旧版本的 web.xml 用的是 http://xmlns.jcp.org/xml/ns/javaee 这个命名空间,新版本用的是 https://jakarta.ee/xml/ns/jakartaee。如果头信息对不上,Tomcat 启动的时候会直接报错,说无法解析 web.xml。

下面我们用一个最直接的例子来感受一下。这是旧版常见的写法:

<!-- 旧版 web.xml 头声明,适用于 Tomcat 9 及之前的项目 -->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <display-name>OldWebApp</display-name>
</web-app>

这个文件在 Tomcat 9 上跑得很顺,但放到 Tomcat 10.1 上,启动时常常会看到 “Parse error in application web.xml file” 之类的提示。原因是新版本不认识旧的命名空间了。

三、迁移过程中最典型的兼容性问题

3.1 项目依赖中的包名找不到

很多项目的代码里写了一大堆 import javax.servlet.*。升级到 Tomcat 10 之后,启动的时候会疯狂抛出 ClassNotFoundException,原因是 Tomcat 10 的 lib 目录下已经没有 javax.servlet 相关 jar 包了,全部换成了 jakarta.servlet。如果你的项目使用 Maven 来管理依赖,那么依赖声明里也得把 javax.servlet-api 换成 jakarta.servlet-api。

下面是一个 Maven 依赖的对比。升级之前你可能用的是这样的代码:

<!-- 升级前的依赖声明,适用于 Tomcat 9 -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
</dependency>

升级之后要改成:

<!-- 升级后的依赖声明,适用于 Tomcat 10.1 -->
<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>6.0.0</version>
    <scope>provided</scope>
</dependency>

注意 scope 一定要是 provided,因为 Tomcat 本身已经带有这些库,免得项目打包的时候又塞一份进去,和容器里的冲突。

3.2 web.xml 头信息不认识

就像前面说的,web.xml 的头部声明需要改动。如果你用的是老版本,需要把命名空间和 schema 地址都换成新版的。对于 Tomcat 10.1 来说,正确的新版头信息是这样的:

<!-- 新版 web.xml 头声明,适用于 Tomcat 10.1 及之后 -->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee 
                             https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">
    <display-name>NewWebApp</display-name>
</web-app>

如果你的项目是用注解方式配置 Servlet,比如 @WebServlet、@WebListener 这些,那么对应的 import 也得一并改。来看一个具体的 Java 类:

// 升级前,用 javax 包
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.setContentType("text/plain;charset=UTF-8");
        resp.getWriter().write("Hello, Tomcat!");
    }
}

升级后,只需要把 javax 替换成 jakarta,其他逻辑完全一样:

// 升级后,用 jakarta 包
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.setContentType("text/plain;charset=UTF-8");
        resp.getWriter().write("Hello, Tomcat!");
    }
}

从代码上看,就是包名前缀变了一下,别的没有区别。但如果你项目里的类文件是编译后的 jar 包,那就得重新编译或者等第三方库出新版本。

3.3 默认行为变化带来的坑

除了明面上的包名,新旧版本在默认行为上也有一些不同点。比如 Tomcat 对 URL 路径的匹配规则、对请求编码的处理、对 Cookie 的解析,都可能有细微变化。有些老项目会依赖一些“不标准”但能跑的行为,升级后这些行为可能就变严格了。

举个常见的例子:Tomcat 9 之前对请求参数中的无效编码比较宽容,乱码也照常处理;Tomcat 10 之后如果配置了严格的 URI 编码校验,遇到非法字符会直接返回 400 错误。这类问题通常不会在启动时暴露,而是在用户访问某个特殊页面时才冒出来。

四、手把手带你解决这些兼容性问题

下面所有示例统一使用 Java 技术栈,包括 Java 代码、Maven 配置、Tomcat 配置和 JSP 文件;其中出现的命令行只是辅助排查工具,不引入其他开发语言。我们将一次典型的 Tomcat 9 到 Tomcat 10.1 的迁移过程拆解成几个步骤。

4.1 先给项目做一次全方位扫描

不要一上来就换 Tomcat 安装包。先用文本搜索工具,比如直接用 grep 命令,在项目源码里搜 javax.servlet,看看有多少引用。然后再搜 web-app 相关的头信息,把所有使用旧版命名的文件列出来。

# 在项目根目录下搜索旧的 javax.servlet 引用
grep -r "javax\.servlet" --include="*.java" .

# 搜索 web.xml 里的旧命名空间
grep -r "xmlns.jcp.org" --include="*.xml" .

如果你发现文件很多,而且你的项目是 Maven 项目,可以借助一个叫 tomcat-migration 的辅助工具。这不是必须的,但能省不少力气。这个工具的作用就是帮你把代码里的 javax 批量替换成 jakarta。当然,最稳妥的方式还是自己动手替换,因为自动替换偶尔会碰到不该改的地方,比如注释里的历史说明、你自己写的工具类名。

4.2 修改构建脚本和依赖

用 Maven 的话,除了改刚才提到的依赖坐标,还要确认 maven-compiler-plugin 的版本和源码编译级别。建议把项目编译级别设置在 Java 11 或更高,因为 Tomcat 10.1 要求 Java 11 以上。下面是 pom.xml 里关键部分的示例:

<!-- pom.xml 中的属性配置 -->
<properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

如果你的团队还在用 Java 8,那得先升级 JDK 才能升级 Tomcat,否则 Tomcat 10.1 根本启动不起来。这一步一定要提前和团队确认好,别等到上了测试环境才报 UnsupportedClassVersionError。

4.3 调整服务器配置文件

Tomcat 自身的配置文件也在 conf 目录下。升级后,某些配置项的默认值会变。比如 conf/web.xml 里默认启用的 servlet 列表、conf/server.xml 里的连接器参数。如果你之前手工修改过这些文件,升级时不要直接拷贝旧文件覆盖新的,最好在新版的默认配置基础上重新改。因为新版的默认配置里可能已经加入了新的安全项,你完全覆盖之后这些安全项就没了。

conf/tomcat-users.xml 也是一个容易出问题的地方。旧版里定义用户和角色的写法用的是 role、user 标签,新版里这些标签没有变,但默认的用户名和密码策略可能不一样。比如 Tomcat 10 之后的管理界面默认更严格,如果你没有设置用户,远程访问 Manager App 会直接被拒绝。下面是一个在 Tomcat 10.1 里配置管理员账号的示例:

<!-- conf/tomcat-users.xml 中的用户配置片段 -->
<tomcat-users xmlns="http://tomcat.apache.org/xml"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
              version="1.0">
    <!-- 定义一个具有管理员权限的角色 -->
    <role rolename="manager-gui"/>
    <role rolename="admin-gui"/>
    <!-- 定义一个测试用户,注意这里只是演示,生产环境请使用强密码 -->
    <user username="admin" password="ChangeMe" roles="manager-gui,admin-gui"/>
</tomcat-users>

这个文件本身格式变化不大,但要注意修改之后重启 Tomcat 才生效。而且不要把密码设置得太简单,否则很容易被扫描器撞库。

4.4 处理第三方库的兼容性

很多项目不是单纯的 Servlet,还用了 Spring、MyBatis 等框架。升级 Tomcat 版本时,这些框架的版本也要跟着检查。比如 Spring Framework 5.x 只支持 javax,你必须升级到 Spring Framework 6.x 才能支持 jakarta。如果你用的是 Spring Boot,那么 Spring Boot 3.x 才对应 Jakarta EE 9/10。这是一个连锁反应,不是只升 Tomcat 就完了。

这里有一个常见的误区:有人以为只要在 Maven 里排除掉 javax.servlet-api,再引入一个 jakarta.servlet-api 就能解决所有问题。实际上,第三方框架内部编译时引用的还是老包名,靠依赖排除是没用的。你必须把整个依赖链条中所有基于 javax 的库都升级到对应的 jakarta 版本。举例来说,Spring 的 DispatcherServlet 类在 Spring 5 里实现的是 javax.servlet.Servlet,到了 Spring 6 才换成了 jakarta.servlet.Servlet。所以你的 Spring 版本必须跟着动。

可以通过下面的命令查看项目里到底有哪些依赖引用到了 javax.servlet:

# 使用 Maven 依赖分析,列出所有包含 javax.servlet 的依赖
mvn dependency:tree -Dincludes=javax.servlet

如果输出结果为空,说明你的项目里已经没有直接依赖老包了。如果还有结果,就需要继续排查对应的第三方库,看看有没有支持 jakarta 的新版本。

4.5 JSP 也要跟着检查

JSP 文件通常放在 webapp 目录下,里面如果直接写了 <%@ page import="javax.servlet.*" %> 这样的指令,也需要改成 jakarta。大多数情况下,JSP 里用到的隐式对象比如 request、response 不需要改,因为 Tomcat 会自动处理。但如果你在 JSP 里显式引用了 Servlet API 的类,那就要改。

来看一个简单的 JSP 示例:

<!-- 升级前,JSP 中使用 javax 包 -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="javax.servlet.http.HttpSession" %>
<html>
<body>
<%
    HttpSession session = request.getSession(); // 获取会话对象
    out.print("当前会话ID:" + session.getId());
%>
</body>
</html>

升级后,把它改成:

<!-- 升级后,JSP 中使用 jakarta 包 -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="jakarta.servlet.http.HttpSession" %>
<html>
<body>
<%
    HttpSession session = request.getSession(); // 获取会话对象
    out.print("当前会话ID:" + session.getId());
%>
</body>
</html>

注意 JSP 文件也需要被重新编译,第一次访问的时候 Tomcat 会自动做这件事,所以如果只改了 Java 代码,没有改 JSP,启动时可能不报错,但第一个用户访问页面时会突然报 500。升级测试时一定要把关键页面都跑一遍。

五、升级后的收益和需要留的心眼

5.1 新版本带来的直接好处

升级后的收益是很明显的。首先是安全性,新版本会修复大量已知漏洞,尤其是远程代码执行和拒绝服务类的高危漏洞。其次是性能和稳定性,Tomcat 10 之后改进了日志解析和线程池管理,在高并发情况下响应速度更快,垃圾回收的压力也更小。另外,新规范支持 HTTP/2、WebSocket 增强等功能,这些在用老版本的时候根本用不上。

5.2 升级后容易忽视的隐藏坑

升级完成后,不要急着把旧版本删了。保留一份旧安装包,至少保留一个版本,方便随时回退。同时,要在测试环境完整验证所有功能,包括文件上传、下载、登录、大数据量查询等场景。特别注意那些用反射加载类的地方,因为包名变更后,有些反射代码里写的字符串还是 javax,运行时要到 Class.forName("javax.servlet...") 就会报类找不到。这种错误往往要触发到特定功能才会出现。

还有一点是日志和监控。升级后 Tomcat 的日志默认格式可能变化,日志路径也可能不同。如果你的日志采集系统配置的是老路径,升级后可能会收不到日志,造成监控盲区。要提前检查 conf/logging.properties 里配置的文件路径,确保日志能正常输出。

另外一个实际建议是:升级操作不要在高峰期做。最好凌晨发版,同时准备好页面。一旦遇到问题,能快速切回旧版本。整个迁移流程应该提前写成一份检查清单,谁来做、做什么、怎么验证都写清楚,不要拍脑袋临时发挥。

5.3 如何制定回退方案

回退方案其实很简单:只要在升级前把整个 Tomcat 目录和项目部署目录的备份打好,回退时直接换回去就行。但要注意数据库结构或数据文件在升级过程中是否发生了变更,如果有,那么回退的时候要把这些变更也一起还原,否则新版本的数据可能没法在老版本里读。这也是为什么强烈建议升级前先做一次全量备份。

六、总结

Tomcat 版本升级说难不难,说简单也不简单。难的地方在于兼容性问题往往藏在代码和配置的细节里,比如一个 javax 和 jakarta 的区别就能让项目无法启动。简单的地方在于,只要你搞清楚了 Tomcat 和 Servlet 规范之间的关系,然后按照步骤去处理依赖、配置、代码这三块内容,大部分问题都能提前规避。最关键的一点是升级前把功课做足,升级后把验证做全,不要抱有侥幸心理。希望这篇文章里提到的排查方法和示例,能帮你顺利迁移到新版本,少踩一些坑。