第一次接触 Tomcat 和 Servlet,很多人都会在 IDEA 里兜圈子:代码明明照着写了,点运行却跳出 404;或者明明用的是老教程,放进新版 IDEA 却直接编译失败。 这类事情我前前后后折腾过很多回,每次帮朋友排查最后几乎都会落到同一个原因:IDEA 2024 这个版本,默认环境和老的 Tomcat 9 + javax.servlet 教程完全是两套逻辑。 如果你也卡在“部署完访问不到”“Servlet 找不到类”这一步,这篇应该能帮你把从建项目到配服务器的整条路走通一遍。
这篇内容适合刚学 Java Web、第一次用 IDEA 建动态 Web 项目、或者以前用过 Eclipse / 老版本 IDEA 想切到最新版的人。我会把版本选型、建项目、写 Servlet、连 Tomcat、部署运行、报错排查这几个环节串在一篇文章里写好,重点说清楚为什么这么做,而不只是给一串点击步骤。
1. 环境准备与版本选型,这一步错了后面全白搭
1.1 为什么 IDEA 2024 里的坑和以前不一样
用 IDEA 2024 和网上大量旧教程碰到的最大的差异,出在 Servlet API 的命名空间上。 Tomcat 9 及更早版本用的是javax.servlet,但从 Tomcat 10 开始,官方把包名改成了jakarta.servlet。IDEA 2024 新建项目时默认引用的组件、以及你手工下载当前版 Tomcat 时拿到的几乎都是 Tomcat 10.1 或更高版本,于是老代码里的import javax.servlet.http.HttpServlet就会直接标红,或者在部署时抛NoClassDefFoundError。
我实际推荐两种最稳妥的组合,二选一:
| 组合 | JDK | Tomcat 版本 | Servlet 依赖包名 | 适用场景 |
|---|---|---|---|---|
| 保守方案 | JDK 8 或 JDK 11 | Tomcat 9.x | javax.servlet-api | 跟着老教材学,公司老项目维护 |
| 主流方案 | JDK 17 | Tomcat 10.1.x | jakarta.servlet-api | IDEA 2024 默认环境,推荐新手 |
我自己现在默认走的是第二种,也就是 JDK 17 + Tomcat 10.1 +jakarta.servlet-api。不是因为它比老的先进多少,而是 IDEA 2024 对这套组合识别得最顺,Maven 创建项目时不会因为包名不一致报一堆红线。如果你手头已经有 Tomcat 9,那把依赖换成javax.servlet-api也完全没问题,下面的步骤全部通用,只有 import 的包名前缀不一样。
1.2 JDK 与 Tomcat 到底要不要配环境变量
很多教程第一步就让你去系统里设置JAVA_HOME和CATALINA_HOME。如果你只是在 IDEA 里跑本地开发,这一步我建议跳过,不要配。 IDEA 自己会识别它内置的 JBR 运行时,也可以直接用你在 Project Structure 里指定的 JDK,不需要在系统层面做任何干预。 反倒是你手动改了全局环境变量,很可能和 IDEA 自带的 JDK 版本冲突,出现“IDEA 里能编译,命令行启动 Tomcat 却报版本错误”的怪问题。
真正要用到的,是你在创建项目时,Project SDK 明确选择成你安装的 JDK,然后在 Tomcat 的配置界面里确认所选 JDK 和项目一致。这比设什么环境变量都实在。
如果你一定要在命令行里验证一下环境,可以执行下面两条命令,看到版本号就说明没问题:
java -versionTomcat 本身也不需要手动设置 CATALINA_HOME,IDEA 配置 Tomcat Server 的时候,只需要指定 Tomcat 安装目录,它自己会去找里面的bin/catalina.bat。
2. 在 IDEA 2024 里创建 Web 工程,别用错了模板
2.1 为什么优先用 Maven 生成项目
我见过不少朋友建 Java Web 项目是直接在 IDEA 里选Java Enterprise或者手动创建目录再补web文件夹。不是说不能用,而是 IDEA 2024 里新建 Maven 项目最省事,依赖管理清晰,打包 war 也简单,后面部署的时候能少踩一堆坑。 Maven 会帮你自动建出标准的src/main/java、src/main/resources和src/main/webapp结构,你不用自己记哪里该放代码、哪里该放页面。
实际操作流程是:
- 打开 IDEA,选择
New Project,左侧选Generators里的Maven。 - 设置好项目名。Project SDK 选 JDK 17。
- 不要勾选模板里的任何骨架(比如
maven-archetype-webapp),IDEA 2024 生成的普通 Maven 项目反而够干净。 - 创建完成后,手工在
src/main下新建webapp目录,这就是之后放 JSP、HTML、静态资源的地方。
这样得到的项目结构如下:
项目根目录 ├── pom.xml └── src └── main ├── java ├── resources └── webapp如果你已经建好了一个 Maven 项目,也不用推翻重来,直接在 Project Structure 里给模块添加一个 Web Facet,再把 Web Resource Directory 指向src/main/webapp,效果一样。只是对新手而言,直接在创建时规划好更不容易漏。
2.2 pom.xml 里的 Servlet 依赖,照着抄也得看版本
建好 Maven 项目后,第一件事是把pom.xml里的依赖加上。下面这段是 Tomcat 10.1 对应的 Servlet 6.0 依赖:
<dependencies> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>6.0.0</version> <scope>provided</scope> </dependency> </dependencies>如果你用的是 Tomcat 9,把这段换成:
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency>这里的<scope>provided</scope>很关键。它的意思是编译的时候用这份依赖,但最后打成 war 包时不要把它打进去。因为 Tomcat 自己本来就带了一套 Servlet 实现,你再塞一份进去,启动时反而可能因为 jar 包冲突报各种奇怪错误。 不少新手就是漏了这一步,部署后 Tomcat 一启动就报ClassCastException或者LinkageError,原因就是 lib 里放了多份 Servlet API。
2.3 约定优于配置:用 @WebServlet 注解代替 web.xml
以前写 Servlet 都要在web.xml里手动配置<servlet>和<servlet-mapping>,一个类一行配置,类一多就长到没法看。Servlet 3.0 之后引入了注解@WebServlet,可以直接写在类上,省掉 web.xml 的重复劳动。
IDEA 2024 里新建项目默认是支持注解扫描的,所以你只需要在类上写好 URL 映射,不用再手动维护 web.xml。我这里明确一点:只要你没在项目中创建web.xml,Tomcat 就会自动扫描@WebServlet; 如果你保留了 web.xml,而且里面连 content 版本都没写对,反而可能拦截扫描,导致你的注解不生效。 这个细节我栽过一次:项目里有个历史遗留的空 web.xml,结果加了注解的新 Servlet 怎么访问都是 404,最后把 web.xml 删掉就好了。
3. 第一个 Servlet:从写代码到请求被正确处理
3.1 用代码理解 Servlet 生命周期
Servlet 说白了就是一个能处理 HTTP 请求的 Java 类。它本身没有 main 方法,Tomcat 负责创建它的实例,并在合适的时机调用它的init、service、destroy这些方法。 你可以把它理解成 Tomcat 给你提供的服务员模板,你负责写清楚“收到请求后该干什么”,Tomcat 负责把客人领到你面前。
一个最小可用的 Servlet 长这样:
import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; @WebServlet("/hello") 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!</h1>"); } }新建类的时候,注意jakarta.servlet开头,确认和 pom 里的依赖一致。写完这个类之后,Maven 会自动编译,IDEA 里如果没有红线,就可以进入部署环节了。
3.2 GET 和 POST 请求的分流处理
HttpServlet父类已经在底层帮你做好了service方法的路由:根据 HTTP 请求的 method 自动调doGet或doPost。你在子类里重写哪个方法,就代表处理哪种请求。
一个最典型的场景,浏览器直接在地址栏输入 URL 访问是 GET,表单直接提交也是 GET。而用 POST 提交一般是为了传数据,比如登录、注册、上传。 新手容易犯的错是 HTML 表单设置了method="post",后端却只写了doGet,结果点击提交后收到的要么是 405 错误,要么是空白页。
建议你一上来就把两个方法都写上,哪怕是先写在同一套处理逻辑里:
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { process(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { process(req, resp); } private void process(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType("text/html;charset=UTF-8"); String name = req.getParameter("name"); resp.getWriter().write("收到的 name 是:" + name); }这样不管 GET 还是 POST 都能跑到同一个业务方法,排查问题范围更小,也不容易因为方法漏写出现 405。
3.3 请求参数怎么拿,响应中文怎么不乱码
拿到 URL 里的参数,通常用req.getParameter("参数名"),这个方法同时兼容 GET 的 query string 和 POST 的 form body。 但如果你用 POST 提交中文,不设置编码,很容易出现乱码。正确做法是在读取任何参数之前先设置:
req.setCharacterEncoding("UTF-8");响应的中文乱码则是另一回事,必须设置响应类型,也就是resp.setContentType("text/html;charset=UTF-8")。我见过有人只在 resp 里设置了编码,却在 req 里忘了设置,导致“请求中文乱,响应中文正常”的情况。 两个地方都要设置,不要偷懒只写一处。
下表可以当成速查:
| 情况 | 设置位置 | 代码 |
|---|---|---|
| 表单提交中文变乱码 | 读取参数前 | req.setCharacterEncoding("UTF-8") |
| 页面输出中文变乱码 | 获取 Writer 前 | resp.setContentType("text/html;charset=UTF-8") |
4. 在 IDEA 2024 里连接 Tomcat 并启动部署
4.1 配置 Tomcat Server 的完整路径
写好了代码,接下来就是把项目“接”到 Tomcat 上跑起来。IDEA 里这一步叫Run/Debug Configurations,快捷键可以直接用右上角的服务器下拉框,选Edit Configurations。
点击左上角加号,找到Tomcat Server -> Local。如果你是第一次配置,IDEA 会要求你指定Tomcat Home或者Application server路径,直接选到你解压 Tomcat 的目录,比如D:\apache-tomcat-10.1.28。IDEA 会自动识别版本。
这里有一个重点:新添加的配置里面不会帮你自动部署项目。你必须在Deployment选项卡里加一个 Artifact,否则运行的时候 Tomcat 是起来了,但访问你的项目路径只会看到 Tomcat 的默认首页或者 404。
4.2 选择 war 还是 war exploded
在 Deployment 选项卡里点击加号,Artifact下通常会出现两个选择:项目名:war和项目名:war exploded。
war:把整个项目打包成一个 war 文件,再复制到 Tomcat 的 webapps 目录下解压运行。它更接近真实部署,但修改代码后需要重新打包,速度慢。war exploded:把项目按解压后的目录结构直接当作 Web 应用来跑,修改静态页面和 Java 代码后经常能实现热更新,开发时更方便。
我本地开发一律选war exploded,迭代速度快很多。 尤其是你改一个 JSP 或者静态资源,有时候连重启都不用,刷新页面就能看到效果。
添加好后,下面会有一个Application context输入框,默认是/项目名。 这个路径决定了你访问应用的前缀,比如项目名是demo,那你访问的完整地址就是:
http://localhost:8080/demo/hello前缀路径经常是新手找不到 Servlet 的主要原因:只记得/hello,忘了前面还要带/项目名。 你把它改成/也可以,这样访问时就变成http://localhost:8080/hello,但一般不建议,因为一个 Tomcat 实例跑多个项目时容易冲突。
4.3 运行前检查端口和 provided 依赖
配置好之后,一般不建议直接点运行,先检查三件事:
- Tomcat 默认端口是不是 8080,是否已经被占用。 如果 8080 被别的进程占用了,有两个办法:换端口,或者杀掉占用进程。换端口最快,在
Server选项卡里找到HTTP port,改成8081。杀占用进程的办法我放在后面常见问题里写。 - 部署项目的 URL 路径里有没有 application context。
- 确认 pom 里的 Servlet 依赖 scope 还是 provided。 如果之前误写成了 compile,运行后很可能在 Tomcat 的 lib 下出现重复的 Servlet jar。
点运行之后,IDEA 下方会跳出一个 Tomcat 的日志窗口。看到类似下面的日志,就说明启动成功了:
Server startup in [xxx] milliseconds这时候打开浏览器,访问 http://localhost:8080/demo/hello ,如果页面出现“Hello, Servlet!”,第一个 Servlet 就跑通了。 如果没出来,也别急着改代码,先去看日志里的关键字,绝大部分问题都能在日志里找到答案。
5. 运行阶段的常见报错与排查思路
5.1 访问 404,先区分是哪个层级的 404
看到 404 是最容易慌的,但其实只要分清“Tomcat 都起来了,只是应用没找到”和“应用找到了,Servlet 路径没匹配上”这两种情况,处理起来就清晰了。
先访问http://localhost:8080,如果能看到 Tomcat 默认首页,说明服务器本身没问题。再访问http://localhost:8080/demo/,如果能出现项目下的 index.jsp 或目录列表,说明项目部署成功,问题在 Servlet 的路径映射上。 如果连/demo/都 404,那就是 Deployment 的 Application context 和你输入的路径不一致,或者 Artifact 没有正确添加。
常见对应关系如下:
| 访问结果 | 大概原因 | 处理方向 |
|---|---|---|
8080 正常,/demo404 | 应用没部署或 context 对不上 | 检查 Deployment 里的 Artifact 和 Application context |
/demo正常,/demo/hello404 | 注解映射路径不对 | 检查@WebServlet("/hello")是否写成/hello.do或漏了斜杠 |
| 所有路径都 404 且日志无异常 | web.xml 干扰了注解扫描 | 删除或清空 web.xml |
5.2 500 和 NoClassDefFoundError,多半是包名或依赖冲突
如果你启动时报的是NoClassDefFoundError: javax/servlet/...,那么原因基本可以锁定为:你用的 Tomcat 是 10 以上版本,但代码里引用的是javax.servlet。 反过来,如果你用的 Tomcat 9,但依赖引的是jakarta.servlet,同样地址找不到类。
遇到这个,别去改代码硬凑,先确认组合是不是一致。 简单判断方式就是看 import 语句的前缀:
- Tomcat 10.1 +
jakarta.servlet.http.HttpServlet - Tomcat 9 +
javax.servlet.http.HttpServlet
两个前缀混用,就会出现编译时好好的、运行时发现 Tomcat 的类加载器里根本没有对应类的情况。 这类错误日志通常会显示Caused by: java.lang.ClassNotFoundException,顺着 Caused by 看最快。
5.3 端口被占用,怎么快速定位和释放
IDEA 启动 Tomcat 时经常报这样的错误:
Port 8080 was already in use. Tomcat cannot be started.原因大概率是你之前启动过另一个 Tomcat 实例没关干净,或者别的程序占了 8080。 在命令行里执行下面两条命令,就能查到谁在用这个端口:
netstat -ano | findstr 8080找到 PID 后,用任务管理器结束对应进程,或者用命令:
taskkill /pid 进程号 /f如果你还在用 Mac 或 Linux,可以用lsof -i:8080查看,然后用kill -9 进程号结束。 要是你不想跟别人抢端口,省事的办法是在 Tomcat 配置里把 HTTP port 改成 8081,重启就行。 我自己的习惯是,如果这个端口以后要跑别的服务,就先找出进程杀掉;如果是自己本地练习,直接换端口也不丢人。
5.4 改了代码却看不到效果,多半是热部署没生效
IDEA 的war exploded虽然支持热部署,但默认设置不一定开全。 如果你改了 Servlet 代码后,重新请求还是旧结果,先看控制台有没有重新编译的日志。 如果没动静,可以手动在Build -> Rebuild Project强制重新编译一下,再访问页面。
如果你想尽量接近“改完就生效”,可以在这个运行配置的Server选项卡里,把On frame deactivation设置成Update classes and resources。 这样你从 IDEA 切回浏览器时,IDEA 会自动把更新的 class 和静态资源同步到 Tomcat 的部署目录。 不过改类名、改方法签名这类结构级改动,还是建议直接重启 Tomcat,因为热部署遇到类结构变化时容易出现内存里残留旧类,行为变得诡异。
个人经验是:页面样式、JSP、简单的返回值改动,交给热部署没问题;一旦要加变量、改函数签名,果断重启,省下的时间比等热部署出问题后再排查多得多。
写到这里,最基本的 Tomcat 部署和第一个 Servlet 应该能跑通了。再顺手补充一个我常用的习惯:每次启动前看日志的时候,别只盯着红色 ERROR 那几行,重点是Caused by后面跟的内容。 很多新手喜欢只看最上面的红色段落,结果问题根源在日志中间,一看日志长度就放弃了。 Tomcat 给的报错其实已经一步步把原因写在里面了,顺着Caused by逐层往下,基本都能找到真正的问题出在哪个 jar、哪个类、哪一行。
另外,后续如果你想接着往下扩展,可以顺手在同一个项目里加几个普通的 HTML 页面,再试着用表单 POST 到同一个 Servlet,体验一下 GET/POST 的分发逻辑。第一次可以慢点来,但把日志和请求路径看明白,后面加拦截器、过滤器、数据库连接的时候,就会顺很多。