很多刚接触 Java Web 开发的人,第一次遇到 Servlet 就懵了。倒不是 Servlet 本身有多难,而是这东西不像普通 Java 类那样写好之后右键 Run 就能看到效果——你得先有一个 Web 服务器,把项目打包好,部署上去,再通过浏览器发起请求,和 Servlet 真正打个照面。这一整套流程里,任何一个环节没对齐,页面就是 404 或者 500,连个像样的日志提示都没有,排查起来特别劝退。
这篇教程就专门解决“在 IDEA 里创建、部署、运行一个 Servlet 程序”这件事。我会把背后为什么需要 Tomcat、依赖该怎么引、路径该怎么配、部署有哪几种方式、报错了怎么定位这些内容全部捋一遍。内容围绕一个最典型的 Servlet Demo 展开,从环境准备到最后跑通浏览器访问,每一步都给出可以直接照着做的操作,并解释这么做的原因。适合刚学完 Java 基础、准备进入 Web 开发领域的读者,也适合那些第一次用 IDEA 集成 Tomcat、想搞明白“原理没变但工具怎么配”的开发者。
1. 准备工作:先搞清楚 Servlet 要跑起来,到底需要哪几样东西
很多同学上手就打开 IDEA,新建项目,写一个类,然后发现 Run 按钮根本没法直接跑——因为 Servlet 不是 Java SE 程序,它没有 main 方法,不能自己启动,必须由 Web 容器加载和调用。所以,在谈 IDE 操作之前,先把“零件”配齐,同时把运行逻辑想明白,后面才不会稀里糊涂。
1.1 环境清单:JDK、IDEA 和 Tomcat 缺一不可
做一个最基础的 Servlet 程序,环境上需要三样东西:
- JDK。至少 1.8 及以上,我这里用的是 JDK 8,最稳定,教程里所有 API 都兼容。
- IDEA。操作界面以 2023 及以上版本为例,但 2019 到 2024 的版本布局差异不大,核心步骤都能对应上。
- Tomcat。我建议直接下载 Tomcat 9 的 zip 压缩包,解压即用,不用安装。
Tomcat 和 Servlet 的版本关系很容易被忽视。很多新手下载了最新版 Tomcat 11,结果发现代码里 import 的 javax.servlet 包用不了了——因为 Tomcat 10 之后,Servlet 规范从 javax 改成了 jakarta 命名空间。这不是你写错,是版本换代。对新手来说,认准 Tomcat 9 就行,配套使用 javax.servlet,网上绝大部分旧教程的代码都能直接跑。
1.2 理解 Servlet 的运行逻辑:为什么非要有 Tomcat
打个比方。Servlet 就好比餐厅后厨的一个厨师,这个厨师不负责接客,也不负责开店,没有自己的店面。Tomcat 就是那个餐厅老板,负责开门、接待顾客、把顾客的“点餐需求”传达给后厨,再把做好的菜端到顾客桌上。
这里面的“点餐需求”是什么?在 Web 开发里,就是浏览器发过来的 HTTP 请求。Tomcat 作为 Servlet 容器:
- 监听着一个端口(默认 8080)
- 接收浏览器的 HTTP 请求
- 根据 URL 里的路径找到对应的 Servlet 类
- 调用这个 Servlet 的 service 方法(通常是 doGet 或 doPost)
- 把 Servlet 写好的响应内容返回给浏览器
所以你写 Servlet 时,其实只写了“后厨做菜”的那部分逻辑,而“谁来接客、谁把请求转过来、转给哪个厨师”这件事,统统由 Tomcat 完成。这也解释了为什么 IDEA 里不能像跑 main 方法一样跑 Servlet——你得先把 Tomcat 这个老板给请来,并且让 IDEA 跟你一起指挥它。
1.3 版本搭配记牢,避免“年份越新越折腾”
我见过不少同学,报错后不是先检查代码,而是先去升级最新版 Tomcat、最新版 JDK、最新版 IDEA,结果越升级越乱。就基础学习而言,稳定的搭配是:
- JDK 8 或者 JDK 11
- Tomcat 9.x
- Servlet API 4.0
这个组合的好处是:社区资料最全、IDE 支持最成熟、依赖导入最简单。等整个流程跑通、Servlet 和过滤器这些基础概念搞明白,再考虑迁移到新版,也更容易理解差异。
提示:Tomcat 本身不是必须安装的,IDEA 里也可以下载集成的 Tomcat,但学基础还是建议手动解压一个,这样你能看到 Tomcat 的目录结构,比如 webapps、lib、conf,知道部署到底部署到哪里了,遇到问题也有排查落脚的线索。
2. 创建项目并搭好依赖:把 Maven 工程先立起来
有了环境和逻辑上的准备,接下来动手在 IDEA 里创建工程。这一节我重点讲“为什么要这么建”和“建的时候注意什么”。
2.1 IDEA 新建 Maven 项目,选对骨架很重要
打开 IDEA,选择 New Project,注意左侧项目类型要选 Maven,不要选新建普通的 Java 项目——虽然也能通过手动添加依赖和框架支持做成 Web 项目,但对新手来说,直接建 Maven 工程省去很多 jar 包管理的麻烦。
在 New Project 界面里,需要注意三个地方:
- Name:给项目起个名字,比如 servlet-demo
- Language:选 Java
- Build system:选 Maven
有些版本的 IDEA 在新建项目时会直接提供 Web 选项,比如勾选一个 Web 模块或者选择 Web 骨架。如果界面里能看到 Web 相关的选项,可以直接选上;如果没看到,也不要慌,可以先把 Maven 项目建出来,后面再通过“添加框架支持”来补 Web 能力。
我常用的做法是:先建一个纯净的 Maven 项目,然后手动补 Web 目录结构。这样做的好处是每一步都掌握在自己手里,知道哪个目录是干什么的,未来用别人提供的代码时也不容易被隐藏的目录结构搞懵。
建好之后,项目的标准结构应该是这样一个雏形:
- src/main/java:写 Java 代码的地方
- src/main/resources:放配置文件的
- pom.xml:Maven 的依赖管理文件
2.2 在 pom.xml 里引入 Servlet 依赖,注意 scope 填 provided
Servlet 属于 J2EE 规范的一部分,它本身不是 JDK 自带的,所以要引入外部依赖。在 pom.xml 的<dependencies>标签里,加上这一段:
<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies>这里有一个细节值得单独说:scope 是 provided,不是默认的 compile。
什么是 provided?意思是“这个 jar 包在编译期存在,但在运行期不需要把它打包进你的应用”,因为 Tomcat 自己就带了一份 servlet-api.jar。如果你把它设成 compile,最终打包出来的 war 里会包含一份 servlet-api.jar,这时候 Tomcat 和你的应用里都有相同的类,很容易引发类冲突或版本覆盖问题。虽然基础 Demo 可能表现不出来,但这是一种必须养成的约定——凡是容器本身提供的 API,scope 一律用 provided。
引入之后,让 IDEA 重新加载 Maven 依赖。右下角会弹出 Maven 同步提示,点击“Reload All Maven Projects”就行。等右侧 Maven 面板里能看到 javax.servlet-api 的出现,就说明依赖装好了。
2.3 顺手补上 webapp 目录:不同 IDEA 版本用不同方式
刚才说过,如果新建项目时没有勾选 Web 框架,需要手动补一个 Web 应用目录。这个目录是 Servlet 项目的灵魂,里面放 web.xml 和一些静态资源。
一种方式是:在项目名上右键,选择 Add Framework Support,在弹出的列表里勾选 Web Application,IDEA 会自动生成 src/main/webapp 目录和一个 web.xml 文件。
另一种方式是手动创建。在 src/main 下新建一个目录叫 webapp,然后在 webapp 里新建一个 WEB-INF 目录,最后把 web.xml 放进去。web.xml 可以从其他项目复制,也可以手动写一个最简版本:
<?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"> </web-app>注意,如果 web.xml 文件不存在,容器启动时其实也可以运行注解版本的 Servlet(后面会讲),但很多老项目还是会约定保留这个文件,而且 IDE 在识别 Web 工程时也需要它。所以哪怕是空的,也建议留着。
实操心得:手动建 webapp 目录后,一定要确认一下 IDEA 是否把该目录标记为 Web Resource。在 Project Structure 里,Modules 页签下的 Web 设置里可以检查 Web Resource Directory 是否是 src/main/webapp。如果没指定对,后面部署时会出现“找不到页面路径”的问题。
3. 编写 Servlet 实现类:从 Hello World 到真正能跑通
依赖和目录都备好了,下面开始写代码。这一步是整个项目最核心、也最容易看出门道的地方。
3.1 继承 HttpServlet,重写 doGet 方法
在 src/main/java 下面新建一个包,比如 com.demo.servlet,然后在包里新建一个类,取名 HelloServlet。这个类要继承 HttpServlet 并重写 doGet 方法:
package com.demo.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置响应的内容类型和编码,告诉浏览器这是一段中文网页 response.setContentType("text/html;charset=UTF-8"); PrintWriter writer = response.getWriter(); writer.println("<html>"); writer.println("<h1>Hello, Servlet!</h1>"); writer.println("</html>"); } }解释一下这里面的关键点。HttpServletRequest 是请求对象,里面装的是浏览器发过来的信息,比如参数、请求头、Cookie;HttpServletResponse 是响应对象,写进去的内容会原封不动地变成浏览器接收到的内容。doGet 方法只在浏览器用 GET 方式请求时才会被调用。
设置 setContentType("text/html;charset=UTF-8") 这个动作初学容易漏,如果漏了,输出中文时浏览器会显示乱码,因为浏览器默认可能用 ISO-8859-1 或者 GBK 去解码。设置之后,等于告诉浏览器“请按 UTF-8 来读取这个响应”。
3.2 用 @WebServlet 注解配置 URL 映射,web.xml 可以省一半
Servlet 类写好了,Tomcat 还不知道哪个 URL 要调用它。这时候需要做 URL 映射,把一个路径和 Servlet 类绑定起来。
有两种方式,建议新手优先使用注解方式。直接在类名上方加一行:
@WebServlet("/hello") public class HelloServlet extends HttpServlet { // ... }注意路径必须以斜杠开头,而且可以包含层级,比如 @WebServlet("/demo/hello") 也是合法的。这个路径就是你在浏览器里访问的入口路径。
注解方式比 web.xml 配置简单直观。以前老项目是在 web.xml 里这么配的:
<servlet> <servlet-name>hello</servlet-name> <servlet-class>com.demo.servlet.HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>hello</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping>两行配置才能达到一个注解的效果,而且一旦 servlet-name 写错就会被绕晕。所以我现在带新人的时候,都建议直接用 @WebServlet 把前端时间花在理解映射逻辑上,等到需要配置初始化参数等复杂场景时,再回头学 web.xml 的完整语法。
3.3 编译确认 class 文件真的出来了
这一步是很多初学者想不到检查的,但却是排错时的关键。执行 Maven 的 compile 命令,或者直接 Build 一下项目,然后到 target/classes 目录里看有没有 com/demo/servlet/HelloServlet.class 这个文件。
为什么这一步重要?因为有时候 IDEA 的缓存问题或者部分模块没有勾选,会导致你改了代码但 class 文件没有重新生成,部署到 Tomcat 里还是老版本。养成先编译再看文件存在的习惯,能排除一大批“改了但没生效”的假问题。
4. 把项目部署到 Tomcat:IDEA 与容器的连接配置
代码写完了,接下来是大头:让 Tomcat 跑起来,让 IDEA 把项目部署进去。很多 Servlet 新手在这一步翻车,不是代码不对,而是部署方式不对。
4.1 在 IDEA 中配置 Tomcat:路径、端口和 JRE 一个都不能错
点击 IDEA 顶部工具栏的下拉框(通常是 Add Configuration),进入 Run/Debug Configurations 面板。点击左上角的加号,选择 Tomcat Server,然后选择 Local。
这一步会打开 Tomcat 配置界面,需要填几个关键项:
- Application Server:点击 Configure,选择你解压出来的 Tomcat 根目录(比如 apache-tomcat-9.0.xx)。IDEA 会自动识别 Tomcat 版本。
- JRE:选择你当前用的 JDK。
- HTTP port:默认 8080,保持默认即可。
这里我要指出一种常见误区:有人以为配置完 Tomcat 之后项目就自动部署上去了,于是直接点运行,结果 Tomcat 倒是启动了,但访问 http://localhost:8080 时始终看不到自己的页面。原因在于你还没有把“项目”这个待部署的产物挂到 Tomcat 上,Tomcat 默认只会找自己 webapps 目录下已有的应用。
4.2 添加 Artifact:war exploded 模式更合适
上面的 Tomcat 配置里,还有一个最关键的页签叫 Deployment,这个就是决定“把什么挂上去”的地方。
点击 Deployment 页签里的加号,选择 Artifact,IDEA 会列出两个候选:一种是 xxx:war,另一种是 xxx:war exploded。
新手通常会困惑这俩词什么意思。war 是压缩包形态,部署的时候 Tomcat 会把 war 解压到 webapps 目录下;war exploded 则是把整个项目按解压后的目录结构直接作为部署目录,也就是说,不需要压缩和解压这一步。
我强烈建议初学阶段选 war exploded,因为开发模式下效率更高:修改 Java 代码或 JSP 后,配合热部署能迅速看到效果,而 war 模式每次都要重新打包。从学习角度讲,exploded 模式下,启动后你可以直接在 Tomcat 的工作目录里看到项目对应的目录结构,比如里面会有 WEB-INF/classes,你能亲眼看到编译后的 .class 文件被放到了哪,这对理解部署过程非常有帮助。
选好之后,Deployment 列表里会出现一条记录,Application context 默认是根路径,如 /servlet-demo_war_exploded。这个名字可以改成更友好的,比如直接改成 /demo,方便记忆。它对应的效果就是,浏览器里的访问路径前缀会变成 http://localhost:8080/demo/...。
4.3 启动运行,浏览器访问接口
配置完成之后,点击 Debug 或 Run 按钮启动。注意,Debug 模式更推荐,因为后续可以打断点调试。
启动过程中,Console 里会输出 Tomcat 的日志。看到类似 “Server startup in xxx ms” 的字样,说明 Tomcat 启动成功了。
现在在浏览器输入地址:
http://localhost:8080/demo/hello其中 /demo 是 Application context,/hello 是 @WebServlet 里配置的映射路径。如果一切正常,页面上会显示一行 Hello, Servlet!。
实操心得:如果点击运行后弹出错误提示 “Unable to ping server at localhost:8080”,不要慌,这不是项目代码问题,而是 Tomcat 没有正常起来。常见原因是上一个 Tomcat 实例没有完全退出、端口占用、或者 JDK 与 Tomcat 版本不匹配导致启动失败。先在 IDEA 里把运行中的 Tomcat 停止,再从命令行手动跑一下 Tomcat 看报错信息,往往能直接看到真凶。
5. 新手最容易踩的坑:几个高频报错和排查方法
就算整个流程走通了,接下来自己写程序、改程序时,还是难免遇到报错。既然本文的核心目标是“能独立跑通 Servlet”,排查问题的能力比写代码本身更值得花篇幅讲。
5.1 404:URL 明明输入了,页面就是找不到资源
404 是新手遇到最多的错误。这个错误的本质是:容器接收到了请求,但根据请求路径没有找到对应的 Servlet。可能的原因按照出现概率排序如下:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 路径少了一个斜杠 | 注解写成了@WebServlet("hello") | 看类上注解,必须写成/hello |
| Application context 写错 | 访问时把/demo漏了 | 查看 Run 配置里的 Application context,http://localhost:8080/context/urlPattern 一一对应 |
| class 没编译出来 | target/classes 里没有 com/demo/servlet 目录 | 执行 mvn compile,观察输出 |
| 部署失败 | Deployment 页签里没加 Artifact | 检查 Run Configuration 的 Deployment |
| 文件不对味 | 注解没生效,web.xml 里又没加配置 | 回想一下你用的 web.xml 版本,看是否存在旧版本与注解冲突 |
我遇到过一例特别典型的:同学把 @WebServlet("/hello") 写到了类名上方,但同时 web.xml 里残留了一份空模板的<web-app>标签,标签里没有做映射,结果重启后一直 404。后来把这个空 web.xml 删掉重启才正常。这个问题的根源是某些版本的 web.xml 存在时,容器只读取 XML 配置而忽略注解。所以在初学阶段,如果你选择用注解方式,就尽量别在 web.xml 里写任何与 Servlet 映射相关的内容,避免冲突。
5.2 500:容器找到类了,但方法内部出了问题
500 表示“服务器内部错误”,也就是说,Servlet 是被找到了,但在执行过程中抛了异常。这时候看控制台或 Tomcat 的日志,才能定位是哪一行出的问题。
最典型的有这么几类:
- 空指针异常。比如从 request.getParameter() 返回值直接调用方法,而参数本身是 null。
- 类转换异常。在一些错误示范里,把 request.getSession() 的结果强转成自定义类型,结果类型不匹配。
- Response 写入后继续操作。代码已经调用了 response.getWriter() 并 flush 了输出,后面又尝试改动响应内容,在复杂场景下会抛 IllegalStateException。
初学者排查 500 最容易犯的错是:只看浏览器上的错误页面,不看 IDEA 的 Console。其实异常堆栈信息第二行就会告诉你完整的类名和行号,顺着行号点进去,一目了然。
5.3 端口占用:启动的瞬间就失败
Tomcat 默认端口是 8080,如果你开过其他 Web 服务,比如某个前后端联调工具、另一个 Tomcat 实例、或者别的开发服务占用了 8080,IDEA 控制台会报:
Port 8080 was already in use.解决办法有两种:一种是找到占用进程并把它结束;另一种是直接给 Tomcat 换个端口,比如 8081。在 Run Configuration 里 HTTP port 改成 8081 就行。注意,改完后访问地址要同步改成 http://localhost:8081/...。
5.4 依赖冲突和包缺失:jar 太多太杂也会出问题
有的同学会把依赖里的 scope 写成 compile 甚至从网上下载 servlet-api.jar 手动放到 WEB-INF/lib 里,这会造成类重复加载。最典型的诡异报错是 ClassCastException,明明代码编译通过、运行期却报“类型转换失败”,还可能伴随 NoSuchMethodError。
这类问题的通用排查思路是:先检查 pom.xml 里 servlet-api 的 scope 是不是 provided,再看 WEB-INF/lib 目录里有没有多余的 servlet-api.jar。如果有,删掉,重启。
避坑技巧:不同来源的 jar 包版本不一致时,优先在 pom.xml 里统一管理版本。只要项目里出现跟“重复 jar 包”相关的异常,先把 WEB-INF/lib 手动的 jar 全部清掉,再让 Maven 统一拉依赖。
6. 进阶一点:让 Servlet 真正参与业务
很多学生学到 Servlet 后,只会输出一个固定 HTML,然后就觉得 Servlet 很“废”,其实 Servlet 是 Web 开发的地基,真正的业务交互从“接收参数”开始。
6.1 接收前端参数并返回结果
把 HelloServlet 改造一下,让它接收 URL 里的参数:
@WebServlet("/hello") public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); String name = request.getParameter("name"); String defaultName = (name == null || name.isEmpty()) ? "陌生人" : name; PrintWriter writer = response.getWriter(); writer.println("<h1>你好, " + defaultName + "</h1>"); writer.println("<p>收到请求方式: " + request.getMethod() + "</p>"); } }然后浏览器访问:
http://localhost:8080/demo/hello?name=张三页面就会显示“你好, 张三”。这个例子虽然简单,但体现了一个 Web 程序的基本链路:浏览器发请求携带参数 → Tomcat 封装请求对象 → Servlet 读取参数 → 渲染响应内容。
如果你想让表单 POST 提交,可以再加一个 doPost 方法,逻辑和 doGet 一样。实际上很多新手总把 doGet 和 doPost 分开写两遍,有经验的人通常会写一个私有方法,让两者都去调它,减少重复。
6.2 转发和重定向:别再全靠 response.getWriter()
Servlet 中除了直接向浏览器输出内容,另一个核心能力是跳转。跳转有两种方式:
- 请求转发:request.getRequestDispatcher("/other").forward(request, response)
- 重定向:response.sendRedirect("/demo/other")
它们最大的区别是:转发发生在服务器内部,相当于“后厨内部把菜转给另一个厨师做”,浏览器地址栏不会变,请求对象和响应对象还是同一个;重定向则是服务器告诉浏览器“你找错人了,去另一个地址重新请求”,浏览器地址栏会变成新地址。
初学阶段不用背太深,但至少要知道有这么两种跳转方式。因为它们后面学 MVC、学 Spring MVC 时会频繁出现,而且面试也常被问到。搞明白转发和重定向,比背 10 个框架 API 有用得多。
6.3 热部署和调试技巧:开发效率的临门一脚
如果每次改一行代码都要重启 Tomcat,那种体验极其折磨。幸好 IDEA 对 Tomcat 有热部署支持。在 Run 配置里有一个 On frame deactivation(失去焦点时执行)的选项,可以选择 Rerun Profile 或 Update classes and resources。
我更推荐手动实时更新:当代码修改后,切到 IDEA 外(比如切到浏览器按一下),IDEA 会检测到代码变更,自动把变更的内容热同步到运行中的 Tomcat 中,Console 会输出一个 update 成功的提示,然后刷新浏览器页面即可看到效果。不过要注意,如果是修改了 web.xml 或者新增了 Servlet 类,热部署有时不生效,此时还是需要重启 Tomcat。
调试方面,解决 500 最有效的方式就是在 Servlet 代码行号左侧打一个断点,然后用 Debug 模式启动,浏览器发出请求后,线程会停在断点处,这时候你可以在 Variables 面板里查看 request 对象里拿到的参数值,要比反复看日志高效得多。
写在最后
我觉得 Servlet 是 Java Web 里少有的“先苦后甜”的知识点。在 IDEA 里把它跑通,确实要比跑一个 Java 控制台程序多花不少功夫,但只要亲手走一遍“写类、配 Tomcat、部署、访问”这条流程,后续学过滤器、监听器、Spring MVC 时的很多疑惑都会迎刃而解。因为你会发现,再花哨的框架,底层都离不开那套“容器如何处理一个 HTTP 请求”的基础逻辑。
最后再分享一个小经验:遇到问题不要只盯着代码看,先看 IDEA 的 Console 日志,再从浏览器、部署目录一层层往上倒推。Servlet 程序的报错链是清晰且完整的,耐下心来看日志,绝大多数问题都能自己解决。希望你用这套步骤亲手跑通第一个 Servlet 程序之后,能体会到“原来 Web 开发不过如此”的踏实感。