
很多刚接触Java Web的同学都有过这种经历本地IDE里Servlet跑得挺欢一放到服务器上就404页面白板一片翻日志才知道类都没加载出来。其实问题多数不是代码写错了而是Servlet部署的方式不对——Tomcat根本不认你丢进去的东西它认的是约定好的目录结构。这篇文章我直接基于真实踩过的坑来写从JDK、Tomcat版本选型到三种部署方式的具体操作再到404、500这类高频报错的完整排查链路争取让你看完就能自己动手把一个Servlet稳稳部署上去。站在Tomcat的角度理解部署这件事你会发现它没那么玄乎Tomcat只会在固定位置查找class文件、lib包和web.xml它不会自动帮你编译.java源码也不会去WEB-INF目录之外扫描所谓的项目。所以部署的本质就是三件事把Servlet编译成class、按规范放到正确目录、让Tomcat知道这个应用的访问路径。下面按实际操作的顺序一步步拆开讲。1. 先搭对底座JDK与Tomcat的版本和安装细节1.1 JDK不是“装个Java就行”这里特指JDK不是JREServlet部署到Tomcat本质是让Tomcat加载class并跑在你的JVM里所以Tomcat自身是用Java写的它需要一套可用的Java运行时来启动。很多人图省事装了个JRE就以为完事结果配置环境变量时发现怎么都找不到javac这就是因为JRE不带编译器而Servlet编译恰恰需要javac。所以在开发机或者一台干净的部署机上建议直接装完整版JDK不要只装JRE。以常见的JDK 8或JDK 11为例安装完成后要重点确认三件事java -version能输出版本号说明JVM可用了。javac -version能输出版本号说明编译器可用了。环境变量里存在JAVA_HOME并且指向JDK安装根目录。在Linux服务器上我习惯先检查/etc/profile、~/.bashrc或/etc/environment里的配置用如下命令确认echo $JAVA_HOME echo $PATH java -version javac -version如果javac输出command not found就把JDK的bin目录追加到PATH里同时设置JAVA_HOME。这里有个容易被忽略的细节Tomcat虽然启动脚本会自己找java但它也依赖JAVA_HOME环境变量很多部署机Tomcat启动脚本报错找不到Java就是只配置了PATH、没配置JAVA_HOME导致的。1.2 Tomcat版本与目录结构安装前先弄清每个文件夹干什么Tomcat主版本选型这件事很多教程一句话带过实际影响却不小。如果是个人学习或内部系统用Tomcat 9或Tomcat 10都可以但如果你的Servlet代码里用到了javax.servlet包而你又装的是Tomcat 10及以上版本那就会遇到非常经典的包名找不到问题。Tomcat 10.0起把命名空间从javax.servlet迁移到了jakarta.servlet所以老项目直接扔进Tomcat 10多半是编译都过不去。我自己的建议是单纯跑Servlet教程里的示例直接选Tomcat 9兼容性最省心如果非要用Tomcat 10记得代码包名跟着改成jakarta.servlet. 这一点先记在心里后面遇到ClassNotFoundException时能少走很多弯路。解压Tomcat后目录里几个关键位置必须心里有数目录/文件作用bin/启动和关闭脚本如startup.sh、shutdown.shconf/核心配置server.xml、web.xml、context.xml都在这里lib/Tomcat运行所需的jar包也包含servlet-api.jarwebapps/默认的Web应用发布根目录war包或已编译应用都放这里work/Tomcat运行时编译JSP和临时文件的目录logs/日志目录catalina.out、localhost.log是排查问题的第一入口首次启动建议先跑一遍bin/startup.sh然后用浏览器访问默认端口8080看到Tomcat默认首页说明底座没问题。如果端口被占用启动会失败日志里会直接提示BindException或者类似信息这个时候检查一下本机是否有其他进程占用了8080而不是去改Tomcat配置思路会更清晰。2. 部署单位是“目录”不是“源码文件”Servlet项目结构拆解2.1 Tomcat识别应用的约定WEB-INF/classes、lib和web.xmlTomcat部署应用时不看你项目叫什么名字也不看你代码写了多少它只认一个标准结构。一个合法Web应用的目录应当是这样的mywebapp/ ├── index.html ├── WEB-INF/ │ ├── web.xml │ ├── classes/ │ │ └── com/example/servlet/ │ │ └── HelloServlet.class │ └── lib/ │ └── mysql-connector-java-xxx.jar这个结构有几个关键点WEB-INF目录不参与静态资源直接访问浏览器无法通过URL直接访问其内部文件这是Servlet容器对待配置文件的保护机制。classes目录存放编译后的.class文件必须带完整包结构比如com.example.servlet.HelloServlet就对应classes/com/example/servlet/HelloServlet.class。lib目录存放依赖jar包比如数据库驱动、第三方工具库。web.xml是部署描述符Servlet映射、过滤器配置都在这里。很多新手把.java源码丢进webapps目录Tomcat自然不会理会还有人把.class文件直接丢在webapps/mywebapp/根目录下没有放进WEB-INF/classes结果访问时永远404。要理解Tomcat的处理逻辑WEB-INF/classes是classloader的根路径只有在这个目录下包名才能和文件路径对应起来。2.2 不用IDE也能编译Servletjavac加servlet-api.jar是底线刚入门的人如果还没用IDE完全可以用命令行方式编译Servlet。关键点在于Servlet接口不在JDK里它随Tomcat发行放在lib/servlet-api.jar里面所以javac编译时必须通过-cp参数把这个包加进来。假设Tomcat安装在/opt/tomcat项目的源码在/workspace/mywebapp/src/com/example/servlet/HelloServlet.java那么编译命令是这样mkdir -p /workspace/mywebapp/WEB-INF/classes javac -encoding UTF-8 \ -cp /opt/tomcat/lib/servlet-api.jar \ -d /workspace/mywebapp/WEB-INF/classes \ /workspace/mywebapp/src/com/example/servlet/HelloServlet.java-d参数指定编译输出的根目录javac会自动根据包名帮你生成多层子目录。如果还有第三方依赖比如用了JSON库、数据库驱动也要把这些jar加入-cp用冒号分隔。这是一个很容易被忽略的操作细节编译时类路径要包含servlet-api.jar和项目自身依赖但运行时这些依赖不能放在classes下散落必须放进WEB-INF/lib。我个人的习惯是哪怕IDE已经装了也会偶尔用命令行编译来做最小验证这样可以排除掉IDE偷偷帮我做的所有隐式操作比如自动编译、自动拷贝资源、自动管理classpath一旦出现IDE里能跑、命令行部署就404的问题用命令行编译反而更容易定位。2.3 web.xml中的Servlet声明与URL映射URL走到哪个类上由这里决定很多部署问题不是Tomcat找不到类而是Tomcat找到了类却不知道哪个URL应该调它这个对应关系就在web.xml里声明。一份最简web.xml长这样?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 servlet servlet-namehello/servlet-name servlet-classcom.example.servlet.HelloServlet/servlet-class /servlet servlet-mapping servlet-namehello/servlet-name url-pattern/hello/url-pattern /servlet-mapping /web-appservlet-class必须和编译后的全限定类名完全一致比如类上有package com.example.servlet这里就不能只写HelloServlet。url-pattern定义的是上下文路径之后的访问路径如果应用上下文是mywebapp那么完整访问地址应该是http://localhost:8080/mywebapp/hello。另外注意web.xml的存放位置是WEB-INF/web.xml不是项目根目录下。在Servlet 3.0之后也支持通过注解WebServlet(/hello)替代XML配置但如果初学阶段对配置还不熟建议老老实实用XML声明排查问题时能对应上更多显式信息。3. 三种落地部署方式及选型直接从文件夹到WAR包再到自定义Context3.1 方式一webapps目录下直接部署编译后的文件夹这是最直观的部署方式适合快速验证和学习。操作步骤分几步第一步把上一节编译好并配好web.xml的应用目录完整复制到Tomcat的webapps目录下。目录名是什么上下文路径就是什么。比如复制为webapps/mywebapp那么应用访问路径就是/mywebappcp -r /workspace/mywebapp /opt/tomcat/webapps/第二步启动或刷新Tomcat。如果Tomcat已经在运行大多数情况下它会自动检测到新放入webapps目录的应用并完成部署这就是我们常说的自动部署机制。如果没生效重启Tomcat是最稳妥的操作/opt/tomcat/bin/shutdown.sh /opt/tomcat/bin/startup.sh第三步访问验证。假设Servlet映射成/hello访问http://localhost:8080/mywebapp/hello看到Servlet输出的内容就说明部署成功。这种方式有个很舒服的地方改动class文件后只需重新编译并覆盖WEB-INF/classes对应的文件把新版文件放进去Tomcat默认每间隔一段时间就会检查一次class文件并做热部署对本机开发非常友好。生产环境不建议这么干因为文件覆盖没有原子性保障线上请求可能一半时间打到旧类、一半时间打到新类出了问题很难解释。我实际遇到过一次赶时间直接线上覆盖class结果Tomcat reload到一半抛了NoClassDefFoundError从那以后我养成了线上只部署war包的习惯。3.2 方式二打包成WAR部署可迁移、可备份、可回滚WAR包本质上就是按标准目录结构打成的ZIP压缩包后缀改成.war里面必须保持WEB-INF/classes这样的结构。打WAR包的方法很多这里说两种最常用的。如果在命令行环境直接在应用目录的上一层执行打包cd /workspace jar cvf mywebapp.war -C mywebapp .注意末尾的-C mywebapp .表示切到应用目录内部把整个内容压进war根目录。如果弄错了路径打出war后解压会发现多了一层mywebapp外层目录Tomcat部署后上下文路径会变成/mywebapp/mywebapp这种低级错误很常见。打成war后把它复制到Tomcat的webapps目录cp mywebapp.war /opt/tomcat/webapps/Tomcat检测到war包后会自动解压部署解压出的目录名就是war文件名。比如mywebapp.war会解压成mywebapp目录上下文路径为/mywebapp。这里分享一个非常实用的技巧把war文件重命名为ROOT.war部署后的上下文路径会直接变成http://localhost:8080/不需要带任何前缀。这个特性在做单应用服务器时特别方便访问根路径就能打开应用。我在多个项目里都这么干过简单有效。WAR部署最大的优势在于过程可控旧文件不会和新的打包结构混在一起出现半新半旧的状态。发布时把新的mywebapp.war放进去Tomcat自动解压并覆盖旧目录回滚时把旧版本war文件放回去就行配合备份机制操作非常直接。3.3 方式三通过Context配置自定义上下文路径和位置让应用不必待在webapps里把应用放在webapps之外也完全可以这时候就要用到Tomcat的Context配置。最常见的做法有两种。第一种是在conf/Catalina/localhost/目录下新建一个.xml上下文描述文件文件名就是你的上下文路径。比如要配置一个名称为customapp的应用就在conf/Catalina/localhost/下建customapp.xml内容如下Context docBase/data/apps/myapp reloadabletrue/这样Tomcat就会把磁盘上的/data/apps/myapp目录自动映射为/customapp上下文路径应用本身可以放在任意位置不占webapps的空间。这种文件方式比直接修改server.xml更好因为单独一个配置文件管理一个应用后续迁移、备份都清晰Tomcat重启时改动还少。第二种是在conf/server.xml的Host节点内添加Context元素。这种方式也能用但修改server.xml风险更大一个小语法错误就可能导致整个Tomcat启动失败我已经不止一次见过同事改server.xml漏了结尾标签结果Tomcat起都起不来只能靠备份文件救回来。所以能不动server.xml就不动优先用独立上下文描述文件。3.4 部署方式对比什么场景选哪种部署方式适用场景优点缺点webapps目录直接放文件夹本机开发、快速验证修改后可直接覆盖实现热部署结构容易散乱不易回滚webapps放WAR包测试、生产发布结构完整、可备份、可回滚每次更新需重新打包上下文描述文件Context多应用隔离、自定义位置应用位置灵活配置独立需要额外维护xml配置如果你的应用还没到使用容器化部署的阶段WAR包加Tomcat这种方式已经足够应付大多数传统发布场景。学习阶段建议三种方式都亲手试一遍理解它们之间的关联它们干的事都是一样的区别只在Tomcat通过什么路径找到你的应用目录。4. 部署后的验证与高频故障排查链路4.1 验证部署到底成没成从日志到页面的一整套确认流程部署完不能光看鼠标点击有反应我的验证顺序是这样的第一步看Tomcat日志。logs/catalina.out里会输出部署结果成功时能看到类似Deployment of web application archive或者Deployment of web application directory的字样如果应用启动报错也会在日志里看到SEVERE级别的异常堆栈。localhost.log则专门记录应用部署时的详细日志很多404问题都先在这里露出端倪。第二步访问应用的实际URL。先访问http://localhost:8080/应用名/的根路径看看是否返回页面内容如果返回500说明应用可能部署了但启动过程出错了如果返回404就需要按下面的链路继续排查。第三步用curl看响应头curl -I http://localhost:8080/mywebapp/hello返回200 OK是正常的返回404、500就要继续看下文。这种验证方式不带任何浏览器缓存结果比浏览器更真实。4.2 访问404问题出在路径、映射还是应用根本没加载404在Servlet部署里是最常见的报错很多人一看到404就开始怀疑代码其实应该按顺序排查。先确认Tomcat本身是启动的访问http://localhost:8080/如果能看到Tomcat默认首页说明Tomcat进程正常问题在你的应用上如果连首页都打不开先去查日志。再确认应用有没有被加载访问http://localhost:8080/mywebapp/看看是否出现目录列表或者欢迎文件。如果这个路径也404说明应用名的上下文压根没生效检查你是不是把目录名打错了或者war包名写错了。然后确认Servlet映射路径如果应用根路径能访问/hello却404那问题多半在web.xml的url-pattern或者类上注解的URL值不对。这里有个调试经验把url-pattern临时改成/或者/*类似能通配的路径如果访问变了说明映射配置有问题而不是类加载失败。最后还要确认WEB-INF目录下是不是真的存在classes/com/example/servlet/HelloServlet.class。有时候IDE编译输出目录是target/classes手动复制时漏了一层Tomcat找不到类就只能返回404。查这个最简单的方式是用find命令find /opt/tomcat/webapps/mywebapp/ -name HelloServlet.class4.3 访问500或ClassNotFoundException类路径、包名、jar包三个关卡如果访问时页面报500错误Tomcat一般会把异常堆栈打到logs/localhost.log里这是最容易定位的信息来源。最常见的两个500错误和一个高频异常分别说一下。第一个是java.lang.ClassNotFoundException: com.example.servlet.HelloServlet。这说明Tomcat在加载Servlet类时根本没在classpath里找到这个类。排查方向有三个从高概率到低概率排WEB-INF/classes目录是否缺失、class文件包名路径是否正确、web.xml里的servlet-class是否写错了全限定类名。第二个是java.lang.NoClassDefFoundError或者ClassNotFoundException指向第三方库类比如某个数据库驱动类、某个工具类。这个基本是依赖jar包没进WEB-INF/lib导致的。我们容易有一个习惯打war时把依赖jar放在项目根目录的lib下或者直接用IDE的classpath跑但Tomcat不认这些位置它只认WEB-INF/lib。部署前记得把项目所有运行时依赖统一放进这个目录。第三个是Servlet 3.0之后很经典的javax.servlet与jakarta.servlet包名问题。如果你在Tomcat 10上部署一个用javax.servlet.http.HttpServlet编写的旧项目启动时会直接报找不到类。这种问题不是配置错了而是版本兼容问题要么换Tomcat 9要么把源码里所有javax.servlet替换为jakarta.servlet后重新编译。4.4 端口占用、无法热更新、work目录缓存三个“小毛病”背后的原因部署久了还会遇到几个频率极高的环境类问题简单记一笔。端口占用是最常见的一类。启动时日志提示Address already in use: JVM_Bind基本就是8080被其他进程占了。用lsof -i :8080macOS/Linux或者netstat -ano | findstr 8080Windows查到占用进程确定不是业务进程后kill掉即可。千万不要一上来就改Tomcat端口如果你改了8080自己记得住别人可不一定记得住。热更新时间不一致的问题经常出现在直接覆盖class的场景。Tomcat默认有reloadable配置但发现自己改完class后浏览器里看到的还是旧行为建议先清理work目录再重启rm -rf /opt/tomcat/work/*work目录里存着Tomcat编译后的中间文件特别是你用了JSP时JSP的编译结果都缓存在这里。我遇到过不止一次改了JSP文件没生效最后发现是work缓存的问题。清掉后重启基本都能恢复。还有一个很多人忽视的时区问题服务器时区不对会导致日志时间戳和本地时间对不上排查线上问题时误判先后顺序。可以在setenv.sh脚本中加一行JVM参数JAVA_OPTS-Duser.timezoneAsia/Shanghai $JAVA_OPTS这类小问题不影响部署成败但会严重拖慢排查问题的速度提前处理掉能省很多时间。5. 生产环境里我习惯坚持的发布规范和几个实用习惯5.1 约定大于配置能不动server.xml就不动在我接触过的项目中一个常见误区和我在3.3里面提到的类似很多人部署应用第一反应就是改server.xml配Context、改端口结果一个不小心就让整个Tomcat起不来。实际上Tomcat的默认配置已经覆盖了绝大多数场景端口默认8080正常不用改上下文默认使用应用目录名命名清楚就可以了自动部署默认开启war包放入webapps后会自动发布上下文描述文件可以替代server.xml内的配置。如果只是想让应用通过根路径访问把war包命名成ROOT.war就行不需要去改server.xml。真正需要改server.xml的场景很少比如一台机器上要跑多个Tomcat实例且端口冲突。在其余情况下尽量保持Tomcat默认配置后续接手的人维护起来会轻松很多。5.2 发布时先停再换还是直接覆盖技术方案上经常有人争论线上应用更新时要不要先停Tomcat。我的经验是小团队、低峰期的应用没必要动不动就重启整个Tomcat尤其是在多个应用共用一个Tomcat的情况下重启一个会连累其他应用。更稳妥的做法是上传新的war包到webapps时先改个名比如mywebapp.war.new确保文件完整后再删除旧的解压目录与旧war包把新war改成正式名。Tomcat检测到war变化后会重新解压部署。这个过程里虽然应用可能会有几秒钟不可用但比直接线上覆盖class文件安全得多。如果对可用性要求更严格那就把tomcat部署拆成两步先备份旧war和旧的webapps目录再停Tomcat替换文件、启动。虽然停机时间更长但过程完全可控。二选一即可我给你的建议是承受不了五分钟没服务就别折腾热部署老老实实停了再换。5.3 日志、备份与回滚这些事不紧急但迟早要用到每次发布前我习惯先备份当前的war文件和当前解压目录。命令很简单比如cp -r /opt/tomcat/webapps/mywebapp /opt/tomcat/backup/mywebapp_$(date %Y%m%d_%H%M%S)发布出问题时回滚无非是删掉新目录把备份目录改回原名字或者用旧war替换新war再重启不用慌。另外日志文件的保留策略也不难配置Linux下用logrotate针对Tomcat的logs/catalina.out做按天切割就行配置示例大概是/opt/tomcat/logs/catalina.out { daily rotate 15 compress missingok copytruncate }这样日志不会无限膨胀占满磁盘。回头想想早期我踩过最痛的坑就是磁盘满导致Tomcat启动缓慢最后才想起是catalina.out几个月没管涨到几十GB。这个教训比任何部署手法都深刻。5.4 快速诊断的通用思路日志先行、逐层剥离最后分享一套我排查Tomcat问题的通用顺序适合各种疑难杂症先看logs/catalina.out有没有致命错误再看logs/localhost.log有没有应用部署阶段的异常堆栈用curl访问确认问题属于404、500还是连接失败排查目录结构重点确认WEB-INF/classes下class文件是否存在、路径对不对排查依赖jar包重点确认WEB-INF/lib有没有缺失清理work缓存重启Tomcat再做最后一次验证。这套流程看起来简单但实测下来能覆盖八成以上部署问题。大多数情况下问题不在代码本身而是文件系统结构和Tomcat的约定不一致。只要理解了Tomcat从哪里加载class、如何确定映射关系、日志又记录了什么部署Servlet这件事就变成了一道流水线操作不需要背任何魔法步骤。根据我个人的使用体会期间最值得养成的习惯还是“发布前备份、发布后验证”无论目录部署还是war包部署都适用。自己机器上折腾无所谓一旦对接到别人也在用的环境规范一点能少挨很多骂。另外一个小技巧存着随时准备一个最简单的HelloServlet环境出问题时拿来验证Tomcat本身是否健康比翻半天配置快得多。