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.5Servlet 3.1javax.servletTomcat 9.xServlet 4.0javax.servletTomcat 10.0.xServlet 5.0jakarta.servletTomcat 10.1.xServlet 6.0jakarta.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都在这里libTomcat自身的库servlet-api.jar就在里面logs日志输出webapps应用部署目录war包放这里workJSP编译后的临时文件目录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;charsetUTF-8); resp.getWriter().write(h1Hello, Servlet deployed on Tomcat!/h1); } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } }注意到doGet方法里我把响应类型设置成了text/html并且指定了charsetUTF-8。这一步很多人会漏漏了的结果就是浏览器里中文全变成乱码。后面问题排查部分还会再讲。如果项目用的是Tomcat 10只需要把代码里的javax全部替换成jakarta逻辑完全一样。3.3 配置web.xml告诉Tomcat路由规则光有类还不行Tomcat不知道这个类对应哪个URL必须在web.xml里声明。web.xml是Servlet规范里约定的部署描述文件容器启动时会解析它。?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-nameDeployDemo/display-name servlet servlet-namehello/servlet-name servlet-classcom.example.demo.HelloServlet/servlet-class load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namehello/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包启动后对应一个Contextcontext路径默认就是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 为什么访问页面却看到404404这个状态码在Servlet部署场景里有两个高频原因。一是上下文路径输错了。比如war包叫demo.war你访问的时候写成了deploy-demo。这个错误很常见尤其是部署了多个版本的历史war包时。二是应用根本没有成功加载。Tomcat启动时如果加载失败应用不会启动访问自然404。排查看日志是最快的路径不要只盯着浏览器。Tomcat启动时如果加载失败日志里的异常堆栈会直接告诉你问题在哪比如web.xml解析失败、类没有找到。5. 常见问题与排查技巧实录5.1 高频问题速查表这一部分是我在帮别人排错时遇到的高频问题整理成一个速查表按现象、原因、处理办法来组织。问题现象可能原因处理办法访问返回404上下文路径写错核对war包名和URL路径访问返回404web.xml映射错误检查servlet-mapping的url-pattern和servlet-name访问返回500Servlet代码抛异常看catalina.out日志堆栈ClassNotFoundExceptionTomcat版本与包名不匹配Tomcat 10用jakarta.servlet9用javax.servletUnsupportedClassVersionError编译版本高于运行版本用与服务器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 finalNamedeploy-demo/finalName plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-war-plugin/artifactId version3.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_NAMEdeploy-demo TOMCAT_HOME/opt/tomcat WAR_FILEtarget/${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实例。试过很多次一个跑过多个项目的Tomcatconf目录里改过的配置可能已经被上一个项目玩坏了与其仔细排查不如直接解压一个新Tomcat一次到位。结尾我个人在实际操作中的体会是Servlet部署不难难在理解Tomcat的工作方式。把webapps目录、Context、WEB-INF、war解压这几个概念真正理解透遇到问题就有基本的排查思路。很多人卡在404上一下午就是因为不知道Tomcat有日志这回事也不知道上下文路径和war包名的对应关系。最后再分享一个小技巧如果你只是想快速验证Tomcat环境干不干净可以用Tomcat自带的examples应用。访问一下http://localhost:8080/examples/能打开说明容器本身没问题接下来再排查你自己的应用配置。这个技巧帮我省掉过很多次无谓的怀疑。掌握了这里面的原理之后后面再学Spring Boot的内嵌容器你会发现很多思想是相通的。内嵌Tomcat无非是把容器搬进了应用里但还是同一套Servlet规范的底层逻辑。所以说Servlet部署这个基本功值得认真过一遍。