Servlet部署到Tomcat全流程:从war包到web.xml的实践指南
写Java Web的程序员,大概率都经历过这样一个场景:本地IDE里跑得好好的接口,一到终端环境就404,折腾半天发现是war包没打对,或者是web.xml漏了一行配置。Servlet部署这件事,代码写得再漂亮,最后都要落到“怎么把应用正确装进Tomcat”这一步。这篇教程就从零开始,把Servlet基于Tomcat部署的完整流程、底层原理、还有我踩过的坑,一次讲清楚。
适合谁看?刚学完Java SE、准备进入Java Web阶段的同学,或者在工作中需要维护老项目、偶尔要手动部署应用的开发。不管你用的是IDE还是纯命令行,看完之后都能手动完成一个Servlet从编译、打包到部署的全流程。我尽量不讲多余的理论,每一步都给你能直接照抄的操作。
1. Servlet和Tomcat分别是什么,为什么部署的不是“一个类”
1.1 Servlet本质是一套规范
很多初学者会把Servlet理解成一个类,觉得写一个继承了HttpServlet的类,它就是Servlet了。这么说不准确。Servlet本身是Java EE(后来叫Jakarta EE)定义的一套接口规范,规定了服务器端如何处理HTTP请求、如何响应客户端。你写的那个类,本质上是这套规范的一个具体实现。
用一个生活化的类比。Servlet规范就像餐饮行业的卫生标准,你开一家饭店,就是实现了这套标准。但光有你的饭店还不行,你还得租在一个商场里才能正常营业。这个“商场”就是Tomcat。
1.2 Tomcat的角色:Servlet容器
Tomcat是一个Servlet容器(也叫Web容器),它负责两件核心的事:管理Servlet的生命周期,处理底层HTTP网络通信。
你写的Servlet类,编译之后就是一个普通class文件,它自己不会监听端口,不会解析HTTP报文,更不会处理并发请求。Tomcat把这些底层工作全部接管了。当请求进来,Tomcat根据URL定位到对应的Servlet,判断该实例化还是复用已有实例,然后调用service方法,最后再把响应写回客户端。Servlet只管业务逻辑,剩下的脏活累活都是容器干的。
这个“容器”身份很关键,后面在排查一些类加载、实例化异常时,你都会反复用到这个认知。
1.3 部署的本质是什么
明白了上面两点,“部署”这个词就好理解了。部署的本质,就是把你的class文件、依赖的jar包、配置文件,按照容器能识别的目录结构组织起来,放到Tomcat指定的位置,让它能加载并执行。Tomcat在启动的时候会扫描这些位置,解析配置,把Servlet实例化并注册到映射表里。映射表就是“哪个URL对应哪个类”的一份记录,web.xml里配的servlet-mapping,干的就是这件事。
很多同学部署失败,就是因为不理解“目录结构是约定”。Tomcat不会智能地去找你的类,它只认它约定的那套结构。war包为什么能一键部署?因为它本质上就是把约定好的目录结构压缩成一个文件,Tomcat解压之后就能直接识别。
2. 环境准备与版本矩阵:先把手里的牌理清楚
2.1 JDK版本怎么选
部署Servlet项目,第一步是确认JDK版本。不同版本的Servlet规范、Tomcat版本对JDK的要求不一样。我给的推荐原则是:生产环境用稳定版本,新项目用LTS版本。
当前环境下,JDK 8和JDK 11仍然大量存在,JDK 17也已经非常主流。如果这个项目要跑在别人的老服务器上,先问清楚对方的Java版本,再决定用哪个版本编译。编译版本如果高于服务器运行版本,启动或运行时会直接报UnsupportedClassVersionError,这个错误信息里会带上版本号,看到它基本就能定位。
我自己的习惯是:新项目统一用JDK 17,本地开发和生产环境保持一致。老项目维护则看对方服务器跑的是什么,不轻易升级。
2.2 Tomcat版本和Servlet版本怎么对应
Tomcat的版本号,和Servlet/JSP规范版本有对应关系。简单记几个常用的:
| Tomcat版本 | 对应Servlet规范 | Servlet包名 |
|---|---|---|
| Tomcat 8.5 | Servlet 3.1 | javax.servlet |
| Tomcat 9.x | Servlet 4.0 | javax.servlet |
| Tomcat 10.0.x | Servlet 5.0 | jakarta.servlet |
| Tomcat 10.1.x | Servlet 6.0 | jakarta.servlet |
这是我们最常遇到的一个坑。Tomcat 10之后,包名从javax.servlet换成了jakarta.servlet。如果你还用javax.servlet的包名去写代码,本地编译能过(因为本地引的是旧版servlet-api),一部署到Tomcat 10上就报ClassNotFoundException。
所以,自己搭环境的时候,建议先用Tomcat 9,用javax.servlet包名,网上大多数老教程的代码都能直接跑。如果你用Tomcat 10,就要注意引入的是jakarta.servlet-api,代码里的import也要改成jakarta开头的包名。
2.3 Tomcat目录结构要先看懂
Tomcat解压之后的主要目录(以Tomcat 9为例):
- bin:启动和关闭脚本
- conf:核心配置,server.xml、web.xml都在这里
- lib:Tomcat自身的库,servlet-api.jar就在里面
- logs:日志输出
- webapps:应用部署目录,war包放这里
- work:JSP编译后的临时文件目录
webapps是部署最相关的目录,后面的实操主要就围绕它。
另外,conf/server.xml里有连接器的配置,端口就在这里改。默认是8080,如果被占用了可以改,但不建议轻易乱改,因为改了之后别人的脚本、监控、防火墙规则可能就全部对不上了。
3. 从零写一个Servlet并完成一次完整部署
3.1 手工创建工程结构
这一步不需要任何IDE,先用命令行把项目搭出来。这一步走通了,你就能理解IDE背后帮你做的事。
deploy-demo ├── src │ └── com │ └── example │ └── demo │ └── HelloServlet.java ├── web │ ├── WEB-INF │ │ └── web.xml │ └── index.html └── lib说明一下这个结构的用意:
- src存放Java源文件,按包结构组织。
- web是Web应用的根目录,WEB-INF必须放在这里,web.xml也必须在WEB-INF下。
- lib目录是给项目自己的依赖jar包用的,比如连接数据库的驱动。
WEB-INF这个目录比较特殊,它里面的内容对浏览器不可见,是给Tomcat用的。配置文件放这里,外部用户无法直接访问,安全性上有保障。很多人刚学的时候把Java文件直接丢在web目录下,Tomcat根本不会去编译,也不会执行,因为没有class文件可加载。
3.2 写一个最简单的Servlet
我写一个HelloServlet,处理GET请求,输出一行HTML。
package com.example.demo; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); resp.getWriter().write("<h1>Hello, Servlet deployed on Tomcat!</h1>"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } }注意到doGet方法里,我把响应类型设置成了text/html,并且指定了charset=UTF-8。这一步很多人会漏,漏了的结果就是浏览器里中文全变成乱码。后面问题排查部分还会再讲。
如果项目用的是Tomcat 10,只需要把代码里的javax全部替换成jakarta,逻辑完全一样。
3.3 配置web.xml,告诉Tomcat路由规则
光有类还不行,Tomcat不知道这个类对应哪个URL,必须在web.xml里声明。web.xml是Servlet规范里约定的部署描述文件,容器启动时会解析它。
<?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>DeployDemo</display-name> <servlet> <servlet-name>hello</servlet-name> <servlet-class>com.example.demo.HelloServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>hello</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> </web-app>这里面有三个点要讲清楚。
第一,servlet-name是逻辑名,servlet-class才是全限定类名,映射的时候通过逻辑名关联。两个name必须完全一致,写错大小写、多个空格都不行。
第二,url-pattern就是浏览器访问的路径。配了/hello之后,通过http://localhost:8080/应用名/hello就能访问到这个Servlet。url-pattern的写法要符合Servlet规范,比如/开头的路径,或者*.do这样的扩展名映射,不能随便写。
第三,load-on-startup表示是否在Tomcat启动时就实例化这个Servlet。默认是第一次访问时才实例化(懒加载),配了数字之后,数字越小越先加载。生产环境里,初始化比较重的Servlet建议配置这个属性,启动时完成初始化,避免第一个用户访问时等很久。
3.4 编译,注意classpath要带上servlet-api
编译类的操作很简单,但是classpath必须包含servlet-api.jar,否则编译报错找不到HttpServlet。
javac -encoding UTF-8 \ -classpath /path/to/tomcat/lib/servlet-api.jar \ -d web/WEB-INF/classes \ src/com/example/demo/HelloServlet.java-d参数指定class文件的输出目录。我直接输出到了web/WEB-INF/classes,这是Tomcat约定的一个“类搜索路径”,class文件放在这里,Tomcat启动时就能根据全限定名找到。
为什么要加encoding参数?Windows平台的默认编码可能是GBK,源文件如果本身是UTF-8,编译时不指定-encoding,中文字符串在运行时就可能出现乱码,甚至编译报错。这个参数平时不显眼,在跨平台部署时能省掉很多麻烦。
3.5 打包成war包
编译完成之后,整个web目录就是一个标准的Web应用目录结构。要把它部署到Tomcat,最标准的做法是打包成war包。war包本质就是jar包格式,只不过约定好了里面的内容是一个Web应用。
在web目录的上一级执行:
jar cvf deploy-demo.war -C web .这条命令的意思是把web目录下的所有内容打包,deploy-demo.war就生成了。注意-C先进入web目录再打包,确保war包内的路径是以根开始,而不是带一级“web”路径。否则Tomcat解压之后会多一层web目录,访问路径就变了。
打出来的war包,可以用jar tf deploy-demo.war确认里面结构是否正确,重点看WEB-INF/classes/com/example/demo/HelloServlet.class和WEB-INF/web.xml是否在正确位置。
3.6 部署到Tomcat,两种方式
方式一:把war包直接拷贝到webapps目录。
cp deploy-demo.war /path/to/tomcat/webapps/ cd /path/to/tomcat/bin ./startup.sh # Windows下是 startup.batTomcat启动时会自动扫描webapps下的war包,把它解压到同名的目录deploy-demo。然后访问:
http://localhost:8080/deploy-demo/hello就能看到浏览器输出“Hello, Servlet deployed on Tomcat!”。
方式二:开发期直接把目录拷过去。
如果没有打war包,也可以把整个web目录改名为应用名后拷贝到webapps下,同时保留WEB-INF结构。
cp -r web /path/to/tomcat/webapps/deploy-demo这种方式在开发阶段非常方便。改完Java代码,重新编译覆盖class文件,刷新浏览器就能看到效果,不需要重启Tomcat。我做日常调试时经常用这个方法,省时间。
3.7 验证三个关键点
部署完了一定要验证,而且要按顺序验证。
第一,看Tomcat启动日志有没有报错。日志在logs目录下,catalina.out记录了启动过程,如果有异常,基本能从这里看到。特别是SEVERE级别的日志,基本就是部署失败的原因。
第二,确认war解压成功。看webapps下是否生成了deploy-demo目录。如果war包损坏,Tomcat会解压失败,此时访问必404。
第三,访问URL是否正确。上下文路径是war包文件名,所以war包名叫deploy-demo.war,访问路径就是/deploy-demo。如果你把它改成demo.war,路径就变成/demo/hello。改文件名会直接影响URL,这个要特别注意。
4. 部署背后的工作细节:Tomcat怎么加载你的应用
4.1 应用上下文Context是什么
Tomcat把每个Web应用称为一个Context。webapps下一个war包,启动后对应一个Context,context路径默认就是war包名(不含.war后缀)。这个路径决定了URL的第一段。
如果想把应用部署在根路径,访问时直接http://localhost:8080/hello,需要把war包改名为ROOT.war。这是Tomcat内置的默认Web应用约定。注意大小写,ROOT不能写成root,我见过几次因为文件命名不规范导致部署无效,页面怎么都访问不到。
4.2 WAR包解压机制
Tomcat在启动扫描到war包时,会把它自动解压到同名目录。下次启动时,如果发现同名目录已经存在,并且war包没有更新,就不会重新解压。这个机制带来一个很实用的技巧:更新应用时直接拷贝新的war包覆盖,但一定要记得先删除旧的解压目录,否则可能出现新代码没有生效的情况。
这个“明明改了代码、运行还是旧的”的情况,我排查过很多次,十有八九是解压目录没有清干净。work目录下的临时文件同理,JSP修改后没有重新编译时,也可以清一下work目录。
4.3 不部署war包的另一种方式:Context描述文件
如果你的应用不想放在webapps下,还可以在conf/Catalina/localhost下放一个context描述文件,指向任意磁盘路径。
比如创建conf/Catalina/localhost/demo.xml,内容如下:
<Context docBase="/data/webapp/deploy-demo" path="/demo" />这样配置之后,Tomcat会把/data/webapp/deploy-demo这个目录加载为一个Context,访问路径/demo就指向这个应用。这个方式在生产环境里比较常用,应用放在磁盘指定位置,日志、数据目录也相对集中,升级时不需要动Tomcat目录。
注意path属性在较新的Tomcat版本中可能被忽略,实际路径以文件名为主。也就是说,文件叫demo.xml,上下文路径就是/demo。这个细节很多人不知道,配置了半天发现路径不对,其实就是文件命名问题。
4.4 为什么访问页面却看到404
404这个状态码,在Servlet部署场景里有两个高频原因。
一是上下文路径输错了。比如war包叫demo.war,你访问的时候写成了deploy-demo。这个错误很常见,尤其是部署了多个版本的历史war包时。
二是应用根本没有成功加载。Tomcat启动时如果加载失败,应用不会启动,访问自然404。排查看日志是最快的路径,不要只盯着浏览器。Tomcat启动时如果加载失败,日志里的异常堆栈会直接告诉你问题在哪,比如web.xml解析失败、类没有找到。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
这一部分是我在帮别人排错时遇到的高频问题,整理成一个速查表,按现象、原因、处理办法来组织。
| 问题现象 | 可能原因 | 处理办法 |
|---|---|---|
| 访问返回404 | 上下文路径写错 | 核对war包名和URL路径 |
| 访问返回404 | web.xml映射错误 | 检查servlet-mapping的url-pattern和servlet-name |
| 访问返回500 | Servlet代码抛异常 | 看catalina.out日志堆栈 |
| ClassNotFoundException | Tomcat版本与包名不匹配 | Tomcat 10用jakarta.servlet,9用javax.servlet |
| UnsupportedClassVersionError | 编译版本高于运行版本 | 用与服务器JDK匹配的版本重新编译 |
| 页面中文乱码 | 响应未设置charset | 在response设置ContentType或统一配置编码Filter |
| 端口被占用 | 其他进程占用了8080 | 换端口,或停掉占用进程 |
| 更新的代码不生效 | 旧解压目录未清理 | 删除webapps下解压目录后重启 |
| 依赖类找不到 | 第三方jar包没有打进war | 把jar放到WEB-INF/lib目录 |
5.2 几个重点问题的排查现场
端口占用这个问题很常见。Linux下用lsof -i:8080查占用情况,Windows下用netstat -ano | findstr 8080。定位到PID之后,确认不是关键进程再处理。如果你只是本地测试,也可以直接改conf/server.xml里的Connector端口,改成8081,重启即可。但改了端口,所有访问路径都要跟着改,这点要记住。
类找不到的问题,最大的坑是Tomcat版本和Servlet包名不匹配。Tomcat 10把javax.servlet全面换成jakarta.servlet,老代码直接部署必挂。新项目建议一上来就用Tomcat 10,用jakarta包名,后面升级不会有这个问题。旧项目如果是跑在Tomcat 9上,就保持javax不动。
还有一个很多人会踩的坑:把自己项目的lib目录里放了servlet-api.jar再部署,结果启动报重复类定义错误。servlet-api.jar是容器提供的,不要在应用里带,否则会出现类冲突。Tomcat的类加载机制是“父委托”,应用目录里的类会优先被加载,放一个旧版本进去,反而会把容器自己的版本屏蔽掉,导致各种莫名的NoSuchMethodError。
编码问题的重灾区是Windows环境。一个项目里,源文件是UTF-8,跳转页面是GBK,响应头里又没设置charset,出来的中文必乱码。建议大家统一用UTF-8,源文件、配置文件、数据库连接URL、响应ContentType全部统一,乱码问题能减少九成。
5.3 看日志的正确姿势
很多人部署失败之后第一反应是问我,我就反问一句:日志看了没?看日志是排查的第一步,也是基本功。
Tomcat日志主要在logs目录下。catalina.out是主日志,启动过程、应用加载、异常堆栈都在这里。localhost.log记录Web应用初始化相关信息,部署失败的关键信息经常在这里。还有localhost_access_log开头的文件记录访问日志,排查请求有没有到达Tomcat非常有用。
当你访问页面返回404,先去看localhost_access_log,确认请求到底有没有进来。如果日志里根本没有这个请求,那问题可能出在端口、防火墙、上下文路径。如果有请求但返回404,再看是不是应用没加载。
看日志不是瞎看,要按时间顺序看,从启动的第一行看到最后一行,重点关注第一次出现ERROR或SEVERE的位置,那往往就是根因所在。
6. 部署优化与自动化建议
6.1 用构建工具替换手工打包
手工流程走一遍是为了理解原理,但真正上了项目,还用命令行打包就太累了。建议用Maven或Gradle管理项目。
Maven项目的pom.xml里,打包插件配置好之后,mvn clean package就能直接生成war包。war包名称可以用finalName控制,避免默认带版本号导致上下文路径变化。
<build> <finalName>deploy-demo</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.3.2</version> </plugin> </plugins> </build>这里有个非常实用的经验:war包名一旦定下来,就不要随便改。每次构建生成固定名字的war,部署脚本和监控脚本都依赖这个路径。如果maven每次都生成带版本号的包,比如deploy-demo-1.0.0.war,那么上下文路径就会变成/deploy-demo-1.0.0,访问地址跟着变,发布脚本也要跟着改,很痛苦。
6.2 自动化部署与健康检查
发布频率高了之后,人肉拷贝不是办法,可以写一个简单脚本。
bash脚本的核心逻辑大概是这样的:备份旧war包,拷贝新war包,删除旧解压目录,启动Tomcat,然后循环探测健康检查URL。
#!/bin/bash APP_NAME=deploy-demo TOMCAT_HOME=/opt/tomcat WAR_FILE=target/${APP_NAME}.war # 1. 停掉Tomcat ${TOMCAT_HOME}/bin/shutdown.sh # 2. 清理旧版本 rm -rf ${TOMCAT_HOME}/webapps/${APP_NAME}* cp ${WAR_FILE} ${TOMCAT_HOME}/webapps/ # 3. 启动 ${TOMCAT_HOME}/bin/startup.sh # 4. 轮询探测应用是否启动成功 for i in $(seq 1 30); do if curl -s http://localhost:8080/${APP_NAME}/hello | grep -q "Hello"; then echo "Deploy success" exit 0 fi sleep 2 done echo "Deploy failed, check logs" exit 1注意脚本里curl探测的是具体业务页面,而不是只看Tomcat端口通不通。因为Tomcat进程起来了,不代表应用加载成功。有时候应用初始化失败,Tomcat照样监听8080,但访问应用页面就是404。用业务URL做健康检查,可以避免这个假象。
6.3 部署环境尽量保持一致
Java Web项目在跨环境时,最容易出问题的就是JDK版本、Tomcat版本、配置文件这三者。版本不一致,很多诡异问题根本不会发生,一旦发生就很难解释。
我建议本地开发、测试环境、生产环境统一用一个Tomcat版本,至少是一个大版本。比如全部用Tomcat 9.0.x,而不是这边用8.5那边用9.0。JDK也一样,不要本地17、服务器8这么混搭。
配置文件这块,数据库连接配置尽量通过环境变量或外部配置文件注入,不要写死在代码里。这样部署脚本只需要维护一套模板,不同环境替换环境变量就行。
还有最后一个经验:部署之前确认一个干净的Tomcat实例。试过很多次,一个跑过多个项目的Tomcat,conf目录里改过的配置可能已经被上一个项目玩坏了,与其仔细排查,不如直接解压一个新Tomcat,一次到位。
结尾
我个人在实际操作中的体会是,Servlet部署不难,难在理解Tomcat的工作方式。把webapps目录、Context、WEB-INF、war解压这几个概念真正理解透,遇到问题就有基本的排查思路。很多人卡在404上一下午,就是因为不知道Tomcat有日志这回事,也不知道上下文路径和war包名的对应关系。
最后再分享一个小技巧:如果你只是想快速验证Tomcat环境干不干净,可以用Tomcat自带的examples应用。访问一下http://localhost:8080/examples/,能打开说明容器本身没问题,接下来再排查你自己的应用配置。这个技巧帮我省掉过很多次无谓的怀疑。
掌握了这里面的原理之后,后面再学Spring Boot的内嵌容器,你会发现很多思想是相通的。内嵌Tomcat无非是把容器搬进了应用里,但还是同一套Servlet规范的底层逻辑。所以说,Servlet部署这个基本功,值得认真过一遍。