1. 先把404这事儿看明白:它到底在说什么
作为一个常年跟Tomcat打交道的人,404应该是我见过最多的错误之一,没有“之一”我甚至想说就是它。很多新手一看到浏览器里那个硕大的 “HTTP Status 404 – Not Found” 就慌了,以为是Tomcat坏了,赶紧重启、重装、换端口,折腾半天问题还在。这其实是你跟Tomcat之间在沟通上出现了偏差——你要的资源它那里没有,或者它压根没收到你的请求。
先说一个最容易被忽略的点:404的英文全称是“Not Found”,它背后有两层含义。第一层是网络层面的,请求根本没到Tomcat这里来,比如端口错了、IP地址错了,这种情况返回的可能不是Tomcat的404页面,而是浏览器的“无法访问此网站”。第二层是应用层面的,请求到了Tomcat,但是Tomcat在自己的部署目录里、在所有已部署的应用里、在每个应用的URL映射表里,都没能找到跟你请求路径匹配的资源。我们这篇文章讨论的所有内容,几乎都集中在第二层。
还有一个经典场景,就是标题里强调的“Tomcat可以正常运行但是报404”。这种情况尤其迷惑人,因为你访问Tomcat首页能打开,Tomcat的管理界面也能打开,说明服务器本身没毛病,但一访问你的项目就404。这种时候问题基本就锁定在“项目部署”和“项目内部路径配置”这两个环节了。
我需要先给这套排查方案定个基调:别一上来就乱试,先把问题分类,再按顺序排查。404问题的根源五花八门,但归纳起来无非就是几大类——项目部署层面的、路径映射层面的、配置文件层面的、静态资源处理层面的。你只要把这几个方向都搞清楚,绝大多数404都能在五分钟之内定位到原因。
这篇文章我会把多年的排查经验整理成一套可复用的方法论,从最常见的部署问题讲到最刁钻的路径映射问题,每一步都会告诉你原理是什么、为什么要这么做、踩过的坑有哪些。适合刚接触Java Web开发、被404折磨到怀疑人生的新手,也适合那些遇到过“Tomcat明明好好的但项目就是404”的进阶开发者拿来当速查手册。
2. 最常见的404原因排查:先从部署和路径下手
2.1 项目压根没部署成功——这是最冤枉的一种404
我见过太多人排错排了两小时,最后发现war包根本没拷进去,或者项目目录根本不在webapps下面。Tomcat启动后会自动扫描webapps目录下的应用,如果你把一个名为“myapp.war”的文件放在别的地方,Tomcat自然找不到它。这时候你访问 http://localhost:8080/myapp/ 得到的必然是404,而且Tomcat日志里不会有任何报错,因为Tomcat根本不认为这个应用存在。
判断项目到底有没有部署成功,最直接的办法是看Tomcat的启动日志。启动Tomcat时,在控制台或者logs/catalina.out里会有一段类似这样的输出:
INFO [main] org.apache.catalina.startup.HostConfig.deployWAR - Deploying web application archive [myapp.war] INFO [main] org.apache.catalina.startup.HostConfig.deployWAR - Deployment of web application archive [myapp.war] has finished in [1,234] ms如果你在日志里完全找不到自己的项目名,说明Tomcat根本没扫描到它。这时候检查三件事:webapps目录路径是否正确、war包或解压后的应用目录权限是否足够、Tomcat是否配置了别的deploy目录。
还要注意一种很容易踩的坑:Tomcat默认只扫描webapps目录,但有些IDE或者脚本会把项目部署到wtpwebapps、tomcat-tmp之类的目录。你可能在IDE里点了一个“Run on Server”,看起来部署成功了,但实际上项目被放到了一个单独的部署位置。如果这时候你手动去访问webapps下的路径,照样是404。这种情况下,最好借助IDE的控制台输出确认实际部署路径,别想当然。
2.2 访问路径与上下文路径不匹配——这个坑是真的深
就算项目部署成功了,404还是可能出现,而且这一类的坑对新手极其不友好。每个Web应用在Tomcat里都有一个“上下文路径”,俗称Context Path。默认情况下,如果你把一个项目直接放在webapps下并命名为“myapp”,那上下文路径就是/myapp,你必须访问 http://localhost:8080/myapp/xxx 才能找到资源。
但如果你把这个项目改名为“ROOT.war”部署,或者配置了<Context path="" docBase="...">,上下文路径就变成了空字符串,这时候访问 http://localhost:8080/xxx 才能生效。很多人拿到的项目代码可能配置了server.xml里的Context节点,或者项目里带了META-INF/context.xml,这些配置都会影响实际的上下文路径。
判断项目真实的上下文路径有一个非常可靠的方法:看Tomcat的webapps目录下解压出来的文件夹名字。Tomcat默认以文件夹名作为上下文路径。注意还有一个细节:如果webapps下同时存在“myapp.war”和“myapp”这个文件夹,Tomcat优先使用文件夹里的内容,war包反而不会被解压,这时候文件夹里的旧代码可能跟你想象的不一致。
所以当你要访问的URL是http://localhost:8080/myapp/pages/index.jsp,而你的项目文件夹叫“demo”,那Tomcat找不到/myapp这个上下文就自然会404。搞清楚这个逻辑之后,你需要做的就是把访问路径里的项目名改成实际部署名称,或者干脆把项目部署成ROOT应用。
2.3 欢迎页面配置与默认首页的误导
还有一种特别常见的404,发生在你访问http://localhost:8080/myapp/(根路径)的时候。你期望的是进入首页,结果Tomcat给你返回一个404。这种情况往往是欢迎页面没配好。Tomcat查找欢迎页面的顺序是:先找web.xml里的<welcome-file-list>配置,如果项目里没有这个配置,Tomcat会尝试找index.html、index.htm、index.jsp这几个默认文件。
如果你的项目首页叫login.jsp,而web.xml里什么都没配,Tomcat就不知道要展示哪个文件作为默认页。更气人的是,有时候index.jsp存在,但因为它内部又引用了别的资源路径(比如CSS、JS、图片),而这些资源路径写错了,你访问根路径时页面加载出来的效果就像404一样,打开浏览器开发者工具一看,CSS和JS全部标红报404。
因此排查根路径404时,别只盯着有没有index.jsp,还要看看这个页面里引用的静态资源路径是否是相对路径、是否带了错误的上下文前缀。
3. Servlet与映射配置导致的404:老生常谈但永远有人栽
3.1 web.xml中servlet映射的坑
在Spring Boot还没全面普及的年代,web.xml是Java Web应用的标配。只要web.xml里有一处URL映射写错,你访问对应的Servlet路径就是404。这个错误极其隐蔽,因为Tomcat本身能正常启动,项目也能正常加载,甚至日志里都看不出异常,只是你请求的路径没有任何Servlet响应。
web.xml里的Servlet配置长这样:
<servlet> <servlet-name>loginServlet</servlet-name> <servlet-class>com.example.controller.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>loginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>两个<servlet-name>必须完全一致,大小写都不能错。但更坑的是<url-pattern>的写法,/login表示只有访问http://localhost:8080/myapp/login这个精确路径才会触发这个Servlet。如果你是访问/login.do或者/login/,直接就404了。还有种写法是*.do,这种通配符路径要求你访问的URL必须以.do结尾,比如/user.do。很多从Struts项目转过来的同学已经习惯访问xxx.action,如果web.xml里写的是*.do而不是*.action,那404就是板上钉钉的事。
还有一个容易忽略的点:<url-pattern>里的斜杠是绝对不能省略的。写login不写/login,Tomcat启动时就会报错或直接忽略这个映射,之后你访问/login自然是404。
3.2 注解方式配置Servlet的常见问题
现在用纯Servlet的项目已经多半改用注解了,比如@WebServlet("/login")。这种方式省事,但也带来了新问题。最典型的就是注解里写的路径忘了带前导斜杠,或者用了Tomcat不支持的路径匹配规则。
@WebServlet的urlPatterns参数支持/login、/login/*、*.do这几种写法,但如果你写login——没有斜杠——多数情况下Tomcat会直接报部署异常,整个应用都启动不了。这反而不是404了,是500或者直接就访问不了。
还有一点,某些时候你加了注解,但项目里还残留着旧的web.xml,而且web.xml里配置了metadata-complete="true"。这个属性一旦设为true,Tomcat在部署时会完全忽略注解扫描,直接把你所有@WebServlet全部无视。这时候你访问任何Servlet路径都会404,而且表面上看起来一切正常。这个坑在从老项目迁移到新版本Tomcat时特别容易遇到。
我的建议是,如果项目用了注解方式,就检查web.xml的根元素里是否有metadata-complete属性,有的话优先删掉或改为false。这招曾经帮我解决过一个排查了整整一个下午的“全站404”问题,那感觉真是柳暗花明。
3.3 配置了Filter但路径写错导致404
这个属于进阶一点的坑了。Filter本身不直接产生404,但如果你写的Filter把请求拦截下来并跳转到了另一个不存在的路径,那你看到的就是404。比如Filter里写了request.getRequestDispatcher("/nonexistent.jsp").forward(...),而这个JSP压根不存在,那响应就会变成404。更隐蔽的是Filter映射的<url-pattern>是/*,它拦截了一切请求,如果Filter代码里做了路径判断但逻辑写反了,就会把本该放行的静态资源也拦下来,再forward到一个错误页面,最后表现成整个应用“到处都是404”。
排查这种问题的方法很简单:在浏览器里访问某个404的URL,同时在Tomcat日志里看有没有Filter相关的打印信息。如果你用了自定义Filter且有日志输出,但日志里只看到拦截没看到放行,那问题八成就在Filter的转发逻辑里。
4. “Tomcat能运行但404”深度排查清单
4.1 一步一步确认Tomcat到底在不在正常工作
当Tomcat能启动、能访问首页,但访问你的项目却404的时候,你要按下面的顺序一步步排除。这套顺序不是我随便排的,是根据“从服务器到应用再到代码”的排查逻辑来设计的,能帮你少走很多弯路。
第一步,确认Tomcat真的正常工作。访问 http://localhost:8080/ 能看到Tomcat的默认首页。如果这一步都过不了,那问题就大了,但不属于本次讨论的404范围。如果这一步没问题,说明Tomcat进程、端口、网络都是正常的。
第二步,确认你的项目是否被部署。去webapps目录下看,有没有你的项目文件夹或者war包。有war包的话,看看有没有对应的解压文件夹。如果没有,把war包拷进去,或者在IDE里重新部署。这一步能解决大约四成“Tomcat正常但404”的问题。
第三步,确认访问URL的上下文路径。访问 http://localhost:8080/项目文件夹名/ 看能否打开应用首页。如果你访问的URL和项目文件夹名不一致,就改URL。如果改URL还是404,继续往下一步。
第四步,确认应用内部的路径映射。用最简单的方式验证——直接访问一个明确的静态资源,比如http://localhost:8080/myapp/css/style.css。如果这个文件存在却还是404,说明整个应用内部路径都没被Tomcat识别,问题多半在配置层面。如果这个文件能访问而Servlet不能访问,那问题就锁定在Servlet映射层面了。
第五步,翻日志。Tomcat的logs目录下有各种日志文件,catalina.out是主日志,localhost.log记录具体应用相关的异常。404不一定有异常信息,但如果你部署失败、映射配置错误,大概率会在这里留下线索。这一步是排查利器,很多新手的问题就在于不看日志,全靠瞎猜。
4.2 静态资源404的特殊处理思路
很多人搭了Spring MVC、Spring Boot的项目之后,发现自己写的静态资源全部404。这种情况在纯Servlet的Tomcat项目里也时有发生,但处理逻辑差不多。你需要搞清楚Tomcat处理静态资源的默认Servlet是谁——在Tomcat的conf/web.xml里配置了一个名为default的Servlet,它映射了/、*.html、*.css、*.js等常见的静态资源后缀。
如果你的应用里的web.xml覆盖或者干扰了defaultServlet的映射,静态资源就可能全部404。比如你给某个Servlet配置了<url-pattern>/</url-pattern>,就把根路径的映射全占了,default Servlet对根路径的默认处理全部失效,所有静态资源直接404。这是Struts或者部分MVC框架配置时的一个经典坑。
还有一种情况:项目里用了Spring Security或者拦截器,把静态资源路径拦截了,导致请求根本没到达default Servlet。你自己写Filter或者Interceptor的时候,一定要把/css/*、/js/*、/images/*这类路径排除掉。不然表面上看起来是“静态资源404”,实际上是被你亲手写的代码拦了。
4.3 虚拟目录与多应用部署的404
当你部署了多个应用,或者配置了虚拟目录,404的判断会更加混乱。Tomcat支持在server.xml的<Host>节点下配置多个<Context>,每个Context有独立的path属性。举个例子:
<Host name="localhost" appBase="webapps"> <Context path="/demo" docBase="/opt/myapps/demo" /> </Host>这个配置表示把物理路径/opt/myapps/demo映射到URL路径/demo。如果你访问http://localhost:8080/demo/返回404,要检查的是/opt/myapps/demo这个目录是否存在、里面有没有可用的Web应用结构(WEB-INF目录必不可少)。还有个常见问题是:docBase指向的目录里套了一层子目录,比如/opt/myapps/demo/build/libs/demo.war,那Tomcat加载的时候可能找不到上下文根。
虚拟目录的好处是能把应用放到webapps之外,但这也意味着你不能用“看一眼webapps”来判断应用是否部署。遇到这种部署方式,第一件事就是先看server.xml里的Context配置到底指向了哪里,别对着webapps干瞪眼。
5. 常见问题速查表与一套顺手的问题排查顺序
5.1 高频404问题对照表
我把这些年见过最多、最典型的404问题整理成一张速查表,方便你遇到问题直接对照。这张表不追求全,但追求“好使”,覆盖了80%以上的日常404场景:
| 现象特征 | 根本原因 | 快速验证方法 | 解决办法 |
|---|---|---|---|
| Tomcat首页能开,访问项目404 | 项目未部署或部署目录错误 | 看webapps下有没有对应项目文件夹 | 重新部署项目,或调整URL上下文路径 |
访问根路径/myapp/直接404 | 欢迎页面未配置或缺失 | 看web.xml是否有welcome-file-list配置 | 添加欢迎页配置,或访问具体页面确认 |
| Servlet路径全部404,静态资源正常 | web.xml映射错误或注解失效 | 检查url-pattern写法与metadata-complete | 修正映射路径,删除metadata-complete |
| 静态资源CSS/JS/图片全部404 | default Servlet被抢占 | 查看有没有Servlet映射了/路径 | 改用/*保留静态资源处理,或添加资源映射 |
| 单独一个页面404,其他页面正常 | 对应文件不存在或路径大小写不对 | 直接确认文件系统里文件名 | 修正文件名或访问路径,注意Linux大小写敏感 |
| 某个跳转后的页面404 | Filter或控制器转发路径错误 | 查看转发路径目标是否存在 | 修正forward路径或添加对应页面 |
| 应用刚部署时偶尔404,刷新后恢复 | 目录扫描延迟或war包还没解压完 | 等几秒再刷新 | 耐心等部署完成,或提前解压war包 |
| 换服务器后全站404 | 编译版本、目录结构或路径配置不一致 | 对比原服务器部署目录结构 | 调整上下文路径或重新打包部署 |
这张表你可以截图存起来,或者打印一份贴在工位上。排查404的时候不用背逻辑,直接对照现象查原因,大多数情况一眼就能定位。
5.2 一套顺手的问题排查顺序
如果你不想用速查表,也可以按照我下面给这套顺序来走,它本质上是个决策树,每一步都指向下一步,不会让你在排查中迷路。
第一步,先确认Tomcat本身没问题。访问首页,能打开就继续,打不开就去查端口和启动日志。第二步,确认部署目录里有没有你的项目。没有就部署,有就继续。第三步,确认URL的上下文路径跟项目实际名称一致。不一致就改URL。一致还404就继续。第四步,访问一个静态资源,比如/css/style.css。静态资源能访问,说明应用部署OK,问题在动态映射;静态资源也404,说明整个应用的请求都没被正确处理,重点查web.xml和Filter。第五步,翻日志。查localhost.log、catalina.out,用关键词搜索“Deploying”“Exception”“Error”。
这套流程走完,你大概率已经能解决问题了。如果还搞不定,那肯定是你遇到了某些极冷门的配置——比如Tomcat的conf/server.xml里的全局配置影响了项目部署、权限问题导致war包无法解压、磁盘满了导致写入失败之类的。这时候就不要瞎试了,老老实实看日志,日志里一定会有线索。
6. 实际项目里那些让我印象深刻的404教训
6.1 一个改了3小时结果是个大小写问题的案例
有一回,开发环境一切正常,代码部署到测试服务器后就全站404。排查了很久,最后发现是Linux服务器上目录大小写敏感的问题——本地Windows不区分大小写,代码里访问的是/Login.jsp,文件名实际是login.jsp。Windows上一切正常,一到Linux就全部404。
这个案例想说明的是:404不一定就是配置问题,也可能是运行环境差异。尤其是从Windows部署到Linux的Java Web项目,大小写敏感、文件编码、路径分隔符、权限设置,这些都可能是404的幕后黑手。我个人的习惯是,凡是涉及文件名和URL路径,一律全小写,让线上和本地保持一致,能省掉非常多不必要的麻烦。
6.2 千万别小看日志里的那几行信息
我见过太多人了,遇到404就先重启,重启完没用就开始搜“Tomcat 404怎么解决”,然后在网上看到的答案五花八门,挨个试了一遍还是不行。其实Tomcat的日志里面写得很清楚了,只是你没去读它。
catalina.out里如果出现类似SEVERE: Error deploying web application archive,那就是部署失败。localhost.log里如果出现org.apache.catalina.core.StandardWrapperValve.invoke相关的异常,那就是应用里处理请求时出了错。哪怕日志里没有任何报错,它至少能告诉你Tomcat有没有尝试去加载你的应用。没有加载,就是部署问题;加载了还是404,就是映射问题;映射没错还404,那就是资源本身问题。
顺便分享一个小技巧:启动Tomcat的时候加一个-verbose:class参数,能看到JVM都加载了哪些类。对于排查类路径不对导致的404或500,有奇效。不过要注意,这个参数会让日志膨胀得非常快,定位到问题之后赶紧去掉。
6.3 最后再分享一个小技巧
排查404时,别老在浏览器里凭肉眼观察。用命令行工具curl直接看HTTP状态码和响应头,比浏览器干净得多,不会被缓存、代理、浏览器插件干扰。比如:
curl -I http://localhost:8080/myapp/login返回的是404还是200,一目了然。如果你想看完整的内容,用curl -v还能看到请求头和服务器的响应头。这些信息对于定位问题非常有帮助。
另外,如果你用的是Spring Boot内嵌Tomcat,很多排查思路其实也能平移过来,只是部署路径变成了spring-boot-autoconfigure自动配置的逻辑;但“检查路径映射、检查静态资源处理、看日志”这套方法论完全一致。技术框架会变,排查思路永远通用。
做开发这些年,我慢慢意识到:404并不是一个“错误”,它更像是一种提示——你告诉服务器你要找什么,服务器在它自己的世界里没找到,仅此而已。理解了这一点,你就能平静地面对它,然后用正确的方法,一步步解决问题。