一、问题初现:升级后启动直接崩了
最近公司里有个老项目要升级Tomcat,从8.5升到9.0。本来以为只是换个服务器版本,结果刚把安装包解压,部署完应用,一点启动,控制台直接抛出一堆异常,应用根本起不来。大家围在一起看日志,发现错误信息里到处都是java.lang.NoSuchMethodError、java.lang.NoClassDefFoundError,一开始还以为是jar包冲突,后来仔细一看,原来根子出在Servlet API上。
我们举个例子,假设项目里有一个很普通的Servlet,代码大概长这样:
// 技术栈:Java 8 + Tomcat 8.5 升级到 Tomcat 9.0
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 简单的示例Servlet,打印一段文字
*/
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
// 设置响应内容类型
response.setContentType("text/plain;charset=UTF-8");
// 往客户端写一句话
response.getWriter().write("Hello, Tomcat!");
}
}
这段代码在Tomcat 8.5下面跑得好好的,但是在Tomcat 9.0下面启动时就报错了。为什么会这样?就是因为Tomcat 9.0对Servlet API的版本要求变了,从上往下兼容有了裂痕。
我们再看看Maven项目里的依赖,通常会有这样一个东西:
<!-- 技术栈:Java 8 + Maven -->
<dependency>
<!-- 这里是Servlet API,scope设置为provided,
因为实际Servlet API由Tomcat提供,避免打包冲突 -->
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
这个依赖用的是javax.servlet这个命名空间。而Tomcat 9.0虽然还是支持javax.servlet,但是Servlet API版本已经升级到4.0了。如果你的应用里还依赖着老的3.1版本,并且某些API在4.0里变了,就有可能出现版本冲突。
注意,等到了Tomcat 10,变化更彻底,它把整个命名空间从javax.servlet换成了jakarta.servlet。这个改动可大了,相当于把一栋楼的每户门牌号全换了。如果你直接把老项目扔到Tomcat 10里面,那妥妥起不来,而且报错比上面例子还多。
二、核心原因:Servlet API版本变了
要搞清楚这个问题,我们得先明白Tomcat和Servlet API之间的关系。Tomcat本身是一个Servlet容器,它的职责就是实现Servlet规范。不同的Tomcat版本对应着不同版本的Servlet规范。我整理了一张简表(在文字里说不画图了),大概是这样:
- Tomcat 7 -> Servlet 3.0 ->
javax.servlet - Tomcat 8/8.5 -> Servlet 3.1 ->
javax.servlet - Tomcat 9 -> Servlet 4.0 ->
javax.servlet - Tomcat 10/11 -> Servlet 5.0/6.0 ->
jakarta.servlet
你看,从Tomcat 10开始,命名空间都换了。这个变更源自Oracle把Java EE技术移交给Eclipse基金会后,所有Java EE相关的包名都要变化,因为Oracle不让别人继续用javax这个商标。所以之后所有官方标准库都改名成jakarta。
顺便说一下,Servlet规范可以理解为一份合同,它规定了Web容器和你的应用之间怎么配合。比如请求怎么进来,响应怎么出去,过滤器怎么生效。合同版本变了,你应用里用的条款也要跟着变。这也是为什么升级Tomcat后,代码里那些import语句会直接决定程序的生死。
这就给应用开发带来了一个很大的坑。你的项目里如果有很多依赖了javax.servlet的第三方库,比如老版本的Spring、Struts、Shiro等,升级Tomcat到10之后,这些库可能全都没法用。
三、排查步骤详解
那面对这种启动报错,我们得一步步排查。别着急,我带你走一遍完整的思路。
3.1 查看实际加载的Servlet API版本
首先,我们要确认当前Tomcat实际提供的Servlet API到底是哪个版本。你可以去Tomcat的安装目录下的lib文件夹里看看,一般会有一个类似于servlet-api.jar的文件。在Tomcat 9里,这个jar包还是servlet-api.jar;到了Tomcat 10,变成了jakarta.servlet-api.jar。你可以用下面这个命令直接查看jar包里的类路径:
# 技术栈:Shell/Linux
# 查看Tomcat lib目录下的所有jar文件
ls -l $CATALINA_HOME/lib/*servlet*
# 解压jar包,看里面META-INF/MANIFEST.MF,里面会有规范版本信息
jar xf $CATALINA_HOME/lib/servlet-api.jar META-INF/MANIFEST.MF
cat META-INF/MANIFEST.MF
这个命令会显示一些版本信息,比如Bundle-Version等。如果找不到这个jar,那可能你的Tomcat是简化版,或者你的应用用的是嵌入式Tomcat。嵌入式Tomcat的话,你需要检查你的应用依赖的Spring Boot版本。
3.2 检查项目依赖中的Servlet API
如果应用是Maven项目,你可以用依赖树命令看看项目中到底引了哪些Servlet API。命令行如下:
# 技术栈:Shell/Maven
# 查看项目中所有与servlet相关的依赖
mvn dependency:tree -Dincludes=javax.servlet:*,jakarta.servlet:*
这个命令会把所有和servlet相关的依赖都列出来,你就能看到有没有版本冲突。比如可能你的项目里某个第三方库间接引用了servlet-api 2.5,而你的主代码用的是3.1,这就容易出问题。
看依赖树输出的时候,注意有没有出现多个不同版本的servlet-api,有的话,就得用<exclusions>排除掉一些。
3.3 常见异常类型及原因
升级Tomcat后启动报错,最常见的异常有这么几种:
- NoClassDefFoundError:意思是有个类在编译期存在,运行期却找不到。比如代码里写了
import javax.servlet.http.HttpServletRequest,但Tomcat 10里没有javax这个包了,所以你一加载这个类,就报错。 - NoSuchMethodError:类还在,方法没了。比如Servlet 3.1有
ServletRequest#getAsyncContext,但某些老版本没有,或者被移除了,就会这样。 - ClassCastException:强转失败。比如一个对象在Tomcat 8里能被强制转换成
javax.servlet.Servlet,在Tomcat 10里实际是jakarta.servlet.Servlet,类型不匹配,直接抛异常。
我们来看一个具体的报错片段(这是控制台日志,不是可运行代码):
// 技术栈:Java 控制台日志(用于描述报错信息)
// 实际报错可能长这样:
java.lang.NoClassDefFoundError: javax/servlet/ServletException
at com.example.MyApp.init(MyApp.java:15)
at org.apache.catalina.core.StandardContext.init(StandardContext.java:1234)
...
只要看到javax/servlet的包路径,就知道是命名空间切换导致的。
四、兼容性修复方案
既然问题清楚了,那么怎么修?这里分几种情况。
4.1 全量替换javax到jakarta
如果你是从Tomcat 8/9升到Tomcat 10及以上,那么你的代码中所有javax.servlet开头的import都要改成jakarta.servlet。这工作量如果手工改,那真是大海捞针。不过我们可以用一些自动工具,或者干脆用IDE的全局替换功能。但要注意,替换不是简单把字符串换一下就行,因为有些javax.servlet的依赖可能是从第三方传递过来的,你得把第三方库也升级到支持jakarta的版本。
先看一个简单的例子,把原来的Servlet改成兼容Tomcat 10:
// 技术栈:Java 11 + Tomcat 10
// 注意import语句的变化
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 兼容Tomcat 10的Servlet
*/
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
// 设置响应类型
response.setContentType("text/plain;charset=UTF-8");
// 输出内容
response.getWriter().write("Hello, Tomcat 10!");
}
}
看到没有,只是import改了一下。但是你的项目里可能有几十个文件都这样,那就要用工具。
4.2 使用迁移工具
目前最常用的是Eclipse Transformer,它可以把javax替换成jakarta,同时还能处理一些其它元数据。另一个是OpenRewrite,它更智能,可以自动改代码,还能改配置文件。
比如用Eclipse Transformer的命令行工具,可以这样执行:
# 技术栈:Shell
# 下载Eclipse Transformer后,对项目根目录执行:
java -jar transformer.jar -r -p "**/*.java" -m javax.servlet=jakarta.servlet .
# 参数解释:
# -r 代表递归处理
# -p 指定文件匹配模式
# -m 指定要替换的包名映射
这个命令会遍历当前目录下所有.java文件,把javax.servlet替换成jakarta.servlet。当然这只是最简单的用法,实际项目还需要处理一些其它细节。
如果你用的是Maven,还可以引入OpenRewrite插件,在pom.xml里配置一下。
4.3 针对非标准库的兼容
如果你的项目里用了像Spring Framework 4.x或5.0这种比较老的版本,它们内部用的是javax.servlet,那你光改自己的代码没用,得升级Spring版本。Spring从5.3开始支持jakarta命名空间,6.0起完全切换到jakarta。所以你要把Spring升级到5.3+或6.0+,同时还有其它第三方库,比如MyBatis、Shiro等,也要找支持jakarta的版本。
这里给个建议:先列一个全项目依赖清单,然后逐个排查。
五、配置变更与迁移注意事项
除了代码,配置也有很多坑。很多人升级Tomcat后,代码改好了,结果配置不对,还是起不来。
5.1 Tomcat环境变量和JVM参数
老项目可能会有自己的setenv.sh或catalina.bat,里面配置了JVM内存。升级后这些配置一般还能用,但注意Tomcat 9+默认的垃圾回收器变了,如果你之前指定了老的GC参数,可能会不兼容。比如-XX:+UseConcMarkSweepGC这种在Tomcat 9的默认JVM(JDK 11+)上可能已经被移除了。
建议先在测试环境用简单配置启动,再逐步加回原来的参数。
5.2 server.xml配置变化
Tomcat 9和10的server.xml里,Connector标签的一些属性变了。比如protocol属性,老版本可能会写HTTP/1.1,新版本支持Http11NioProtocol等。如果你之前配置了maxThreads、minSpareThreads这些,新版本有了一些默认值调整。总体影响不大,但还是要仔细比对官方文档。
这里给一个典型的Connector配置示例:
<!-- 技术栈:Tomcat server.xml配置 -->
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
maxThreads="200"
minSpareThreads="25"
acceptCount="100" />
这个配置在Tomcat 9下没问题,在Tomcat 10下也兼容,但protocol名称最好改成org.apache.coyote.http11.Http11NioProtocol,更明确。
5.3 context.xml配置变化
如果应用用了JNDI数据源,在context.xml里配置了数据库连接。升级后,注意数据源类的包路径也可能变化。比如DBCP连接池,老版本用org.apache.tomcat.dbcp.dbcp2.BasicDataSource,新版本用org.apache.tomcat.dbcp.dbcp2.BasicDataSource(其实一样)。但要注意Tomcat 10里,javax.sql.DataSource已经变成jakarta.sql.DataSource了,所以如果你的代码里直接用了javax.sql.DataSource,也要改。
另外,web.xml文件的头声明从javax.servlet变成了jakarta.servlet,注意版本号。
<!-- 技术栈:Tomcat 10 的 web.xml 头声明 -->
<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">
</web-app>
在Tomcat 10里,web.xml的命名空间其实已经改成了jakarta.xml,但为了兼容,Tomcat 10也允许用旧的。最好改成新的,以免将来还要改。
六、最佳实践建议
这部分是重点,我们把所有经验浓缩成清单。
6.1 升级前评估清单
- 确定从哪个Tomcat升到哪个Tomcat,比如8->9,还是8->10。搞清楚Servlet API版本跨度。
- 列出所有依赖的第三方库,看是否支持目标Servlet版本。
- 检查代码中有没有直接使用
javax.servlet的import,统计一下数量。 - 检查web.xml、context.xml、server.xml等配置文件的命名空间和属性。
- 准备一个可以随时回滚的部署包,包括旧版Tomcat。
6.2 升级后验证清单
- 先跑一个最简单的健康检查接口,确认应用能启动。
- 把启动日志里的WARN和ERROR都过一遍。
- 跑一遍核心业务流程,特别是有文件上传、异步请求、WebSocket这些依赖Servlet API的地方。
- 使用JMeter做一次冒烟性能测试,确认Tomcat对新版本没有性能异常。
6.3 回滚方案
万一升级失败,要能快速回退。我的做法是保留旧版本的Tomcat目录,不删除。然后把部署包用软链接指向旧Tomcat。切换时只需要改一下端口映射即可。注意应用本身也要能兼容旧版本,所以升级前最好把应用代码和配置做成与版本无关的。
七、文章总结
这次升级Tomcat的踩坑经历,让我们深刻认识到一个道理:光换服务器版本不等于安全升级,Servlet API的兼容性才是核心。从Tomcat 8到9,变化还不算太剧烈,javax.servlet还在;但从9到10,整个命名空间都变了,这个迁移工作量堪比一次小重构。所以大家在升级之前,一定要做好评估,把代码、依赖、配置的变更点全部找出来,再动手。升级后也不要掉以轻心,按功能逐一验证。希望这篇文章能帮你少踩几个坑,顺利把Tomcat的问题解决掉。
评论
围绕“升级Tomcat版本后应用启动报错问题深度解析:Servlet API兼容性详细排查、配置变更及迁移注意事项完整清单与最佳实践”参与讨论