作为常年和 Servlet 打交道的老手,我见过太多人在 IDEA 里建 Servlet 时被各种细枝末节卡住——依赖版本对不上、找不到新建类的入口、启动 Tomcat 后访问 404、端口被占用报错……这些坑单个看不难,但每一个都足够耗掉一个下午。这篇文章就把我从零开始用 IDEA 创建 Servlet 的完整流程整理出来,从新建工程到配置 Tomcat,再到写一个能真正响应请求的 Servlet,按步骤照着走就能跑通。
适用人群很明确:刚接触 Java Web 的同学、被各种教程讲懵的转行者,以及那些想从 Eclipse 切到 IDEA 的旧派玩家。这篇文章不铺垫过多理论,重点在“能做出来”和“知道为什么这么做”,你跟着敲一遍,跑出第一个页面,后面再学 Spring MVC、Spring Boot 都能少走不少弯路。
1. 动手之前,先把 Servlet 和这套开发链路想明白
1.1 Servlet 到底是个什么角色
Servlet 从字面上看是“服务器端小程序”,本质就是一个运行在 Java 环境里的特殊类。它不像普通 Java 程序那样自己开端口、自己监听请求,而是寄生在一个叫做“Servlet 容器”的宿主程序里——最常见的宿主就是 Tomcat。
用餐厅来类比会直观很多:浏览器是顾客,Tomcat 是餐厅大堂,Servlet 是服务员。顾客来点菜,大堂经理(Tomcat)收下点单信息,把需求转给对应的服务员(Servlet),服务员处理后把菜品(HTML 响应内容)端出来,再由大堂交回给顾客。整个过程中,Servlet 自身不直接面对浏览器,它只做“接收参数、处理逻辑、输出响应”这三件事。
理解这层关系后,你就明白为什么教程里总说“Servlet 依赖于 Tomcat 才能运行”了。也正因如此,你在 IDEA 里写 Servlet 时,真正要做的事情其实只有三件:写好这个特殊类的代码、告诉容器“哪个 URL 对应哪个 Servlet”、把工程打包后丢进容器跑起来。
1.2 为什么用 IDEA + Maven + Tomcat 这套组合
现在 Java Web 开发环境有很多种组合方式,但我在教学和实际项目里最推荐 IDEA + Maven + Tomcat 三件套,理由很简单:
IDEA 是当前 Java 开发者的主流主力,它的智能提示、代码跳转、断点调试都相当顺手,当你从原生 Servlet 阶段开始用 IDEA,后面切到 Spring 系列框架时项目结构基本不用重新适应。Maven 解决的是依赖管理问题——Servlet API 不是一个你在电脑上手动下载 jar 包就能安心用的东西,它最好通过 Maven 声明式引入,由 Maven 自动下载和管理版本,省去手工搬 jar 的麻烦。Tomcat 则是业界最常用的 Servlet 容器,它体积小、配置直观、社区资料多,对新手最友好。
这套组合还有一个核心优势:它贴近真实项目开发流程。许多企业的老旧系统至今仍是“SSH 重壳”或“SSM 框架 + 外部 Tomcat 部署”的模式,你在 IDEA 里练熟这套工程结构和部署流程,直接能迁移到工作场景中,不会出现“教程跑得很顺,进公司一脸懵”的情况。
1.3 版本选型:照着这套配,少踩一半的坑
版本不匹配是新人翻车的第一大原因。我建议按这个组合来配环境:
- JDK 8 或更高版本:Servlet 4.0 规范最低要求 Java 8,JDK 8 本身的长期支持特性也足够日常使用。
- IDEA 2021.1 之后的版本:新版对 Maven 和 Tomcat 集成都更完善,操作界面和本文描述更接近。
- Maven 3.6+:Maven 3.8 以上在使用上没什么障碍,老版本也不影响。
- Tomcat 8.5 或 9.0:这两个版本分别对应 Servlet 3.1 和 4.0 规范,兼容性最稳。
注意:虽然 Servlet 5.0/6.0 规范已经发布,对应的是 Jakarta EE 命名空间,但大量教材和公司老项目仍在使用 javax.servlet 包。初次学习阶段,先在 Tomcat 8.5 + javax 环境下跑通最有价值,等基础牢固后再迁移到新规范也不迟。
2. 环境准备:先花十分钟检查和补齐工具
2.1 你需要准备的东西
开始写代码前,先把下面四样东西装好:
- JDK:建议 8 或 11,安装后配置
JAVA_HOME环境变量。 - IDEA:社区版(Community)和旗舰版(Ultimate)都行,但对 Tomcat 集成的支持有差异,这点我下面会专门说明。
- Maven:不用单独装也行,IDEA 自带了一个 Maven,但建议自己装一个,并把 IDEA 的 Maven 路径指到本地安装目录,这样可控性更高。
- Tomcat:去官网下载
zip版解压即可,无需安装。解压后目录里能看到bin、conf、webapps等文件夹,这就是一个完整的容器了。
2.2 确认环境可用的三个动作
打开终端,依次执行下面三条命令,确认环境无误:
java -version mvn -vn echo %CATALINA_HOME%java -version能看到 java 版本;mvn -vn能看到 Maven 版本;CATALINA_HOME是 Tomcat 的环境变量,某些老版本教程会要求配置它,但用 IDEA 集成启动时其实不强制。你只要保证前两条命令有输出即可。
2.3 IDEA 社区版和旗舰版的最大差异
这里必须多说一句。IDEA 社区版(免费版)默认不带 Tomcat 集成功能,你在运行配置里加号下拉根本找不到 “Tomcat Server” 选项。很多新手在这个位置放弃,或者以为是电脑问题,其实只是用错了版本。
旗舰版则原生支持 Tomcat 集成,直接在配置界面里指定 Tomcat 目录就能启动。社区版使用者也不用慌,装一个叫Smart Tomcat的插件(IDEA 插件市场直接搜)就能实现同等的启动、停止、部署功能。后面第 3 章实操部分我将以旗舰版的方式为例讲述,社区版用户装好插件后操作路径也大同小异。
3. 创建 Servlet 的完整实操流程:跟着做就能跑通
3.1 第一步:新建 Maven Web 工程
打开 IDEA 后,按File -> New -> Project,左侧选择Maven,然后勾选Create from archetype。这里的 archetype 可以理解为“项目模板”,我们选择maven-archetype-webapp,它就是 Maven 官方提供的 Web 工程骨架。
如果下拉列表里找不到这个 archetype,点击旁边的Add Archetype,手动填入三要素:GroupId 填org.apache.maven.archetypes,ArtifactId 填maven-archetype-webapp,Version 填1.4。填完点 OK,再选中它创建项目。
接着填写项目基本信息:
- GroupId:组织名,一般用域名反写,比如
com.example。 - ArtifactId:项目名,比如
servlet-demo。 - Name:一般保持和 ArtifactId 一致。
点 Next 直到 Finish,IDEA 会联网下载 archetype 模板,新建出的工程目录大概长这样:
servlet-demo/ ├── pom.xml └── src/ └── main/ ├── resources/ └── webapp/ ├── WEB-INF/ │ └── web.xml └── index.jsp这里有一个必须手动处理的坑:这个模板默认不会帮你建java源码目录。你需要右键src/main,选择New -> Directory,命名为java。创建完成后,右键java目录,选Mark Directory as -> Sources Root。这一步决定了你后面能不能在里面创建 Java 类,忘了标记的话,IDEA 不会把该目录当源码根,类会显示成普通文件夹。
3.2 第二步:补齐 pom.xml 中的依赖和编译配置
打开工程根目录下的pom.xml,在<dependencies>标签中加入 Servlet API 依赖。
<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies>为什么要加<scope>provided</scope>?因为 Tomcat 容器里已经自带了 Servlet API 的实现类,如果我们把依赖以默认的compile方式打包进war包,部署时就会出现类冲突,甚至直接抛LinkageError之类的异常。provided的意思是“编译期我需要你,运行期容器提供你”,这样打包时不会重复打入。
顺手建议在pom.xml里配置一下编译版本,避免默认编译级别过低导致语法识别异常:
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties>右键工程名,选择Maven -> Reload Project,IDEA 就会根据 pom 文件下载依赖。第一次下载会慢些,尽量在网络稳定的时间段操作。
3.3 第三步:编写第一个 Servlet 类
在src/main/java下新建包,比如com.example.servlet,然后在包下新建一个 Java 类,命名HelloServlet。类的内容可以直接参考下面这段:
package com.example.servlet; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @WebServlet("/hello") public class HelloServlet extends HttpServlet { @Override public void init() throws ServletException { System.out.println("Servlet 初始化方法执行了"); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); out.println("<html>"); out.println("<head><title>第一个 Servlet</title></head>"); out.println("<body>"); out.println("<h1>Hello, Servlet 欢迎你</h1>"); out.println("<p>当前时间:" + new java.util.Date() + "</p>"); out.println("</body>"); out.println("</html>"); out.close(); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }这段代码里的几个关键点:
继承 HttpServlet:所有 Servlet 类要么实现 Servlet 接口,要么继承 HttpServlet 这个便捷基类。日常开发 99% 都是继承 HttpServlet,因为它把 GET、POST、PUT 等请求方法帮你分好了,你只需要重写对应的方法即可。
重写 doGet 而不是 service:容器收到 GET 请求后,会调用 service 方法,而 HttpServlet 的 service 会自动根据请求方法路由到对应的 doXxx 方法。我们不直接重写 service,而是重写 doGet,这样代码更清晰,也符合开发习惯。
doGet 里的读取请求与写入响应:HttpServletRequest request封装了浏览器传来的所有信息,比如参数、请求头、路径;HttpServletResponse response是用来写返回内容的对象。setContentType声明返回的文本类型和编码,PrintWriter则是把 HTML 字符串写回浏览器的通道。
doPost 调 doGet:表单提交常用 POST,我们这里简化处理,让 POST 和 GET 走同一套逻辑,方便后面测试。
3.4 第四步:配置 URL 映射的两种方式
Servlet 不像普通 Java 类那样有main方法可以直接运行,它必须和某个 URL 地址“绑定”,容器才知道访问哪个路径时调用哪个类。这个绑定叫映射,有两种实现方式。
方式一:注解配置(推荐)
在上面代码中,@WebServlet("/hello")这行注解就是映射配置。/hello就是访问路径,注意必须用/开头。使用注解方式时,可以完全不管 web.xml 里的 servlet 配置,代码更简洁,也是 Servlet 3.0 规范之后的推荐做法。
方式二:web.xml 配置(传统方式)
如果你查看src/main/webapp/WEB-INF/web.xml,里面是一个模板骨架。传统方式是在这个文件里手动声明 Servlet 和映射关系:
<servlet> <servlet-name>HelloServlet</servlet-name> <servlet-class>com.example.servlet.HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping>这里必须提醒一个关键坑:如果你在类上加了@WebServlet("/hello"),又在 web.xml 里配了同样的映射,启动时 Tomcat 会直接报错,提示重复映射。两者选其一即可,初学者我强烈建议用注解,少一处配置就少一处出错。
3.5 第五步:配置 Tomcat 并部署运行
现在到了最容易让人兴奋也最容易翻车的环节。以 IDEA 旗舰版为例:
- 点击运行配置下拉框,选
Edit Configurations。 - 点左上角
+,选择Tomcat Server -> Local。 - 在
Application server处点击Configure,选择你本地 Tomcat 的解压目录。 - 切到
Deployment标签页,点+,选Artifact,会看到类似servlet-demo:war exploded的选项,选中添加。 Application context默认是servlet-demo,你可以改成/。这个值代表项目访问的根路径,改成/后访问时就不用再带项目名。- 点击
OK保存配置,然后点运行按钮启动 Tomcat。
启动后,IDEA 下方的控制台会刷出 Tomcat 的启动日志,耐心等几秒,看到Server startup in [xxx] milliseconds就说明启动成功了。此时打开浏览器,访问:
http://localhost:8080/hello如果一切正常,你会看到自己写的 HTML 内容,页面上显示当前时间。如果你用的是社区版,则先安装 Smart Tomcat 插件,在Edit Configurations里添加Smart Tomcat,指定 Tomcat Server 路径、部署的工程路径和 Context Path,启动效果与旗舰版基本一致。
4. 核心原理拆解:为什么请求能到达你的 Servlet
4.1 一次完整的请求之旅
浏览器地址栏输入http://localhost:8080/hello后,到底发生了什么?把这个链路想清楚,后面所有 Web 框架你都能举一反三。
- 浏览器向本机
8080端口发起 HTTP 请求,请求行中带有路径/hello。 - Tomcat 作为容器,一直在监听
8080端口,收到这个请求后,会解析出 URL 中的路径部分。 - Tomcat 根据
@WebServlet("/hello")的映射关系,在已加载的 Servlet 中定位到HelloServlet这个类。 - 容器创建本次请求专属的
HttpServletRequest对象和HttpServletResponse对象,并调用HelloServlet的service方法。 HttpServlet父类的service方法根据请求方法(GET/POST)自动调用我们重写的doGet或doPost。- 我们在
doGet里通过response.getWriter()写出的 HTML 字符串,会由 Tomcat 从响应对象中取走,封装成 HTTP 响应报文。 - 浏览器拿到响应报文后解析 HTML,渲染成页面展示给用户。
这就是为什么你在 Servlet 代码里能直接访问request参数、能随手把内容写进response——这两个对象不是你自己 new 出来的,而是容器为每一次请求临时创建的。它们的生命周期和周而复始,构成了整个 Web 交互的基础。
4.2 Servlet 的生命周期:三个阶段一次看完
在一个 Servlet 从出生到销毁的过程中,有三个方法扮演着关键角色:
- init():第一次访问该 Servlet 时,容器会创建它的实例,并调用
init进行初始化。init在整个生命周期里只执行一次。适合做数据源连接、配置加载等一次性操作。 - service()/doGet()/doPost():每次请求进来,容器都会调用对应的方法。这个方法会被并发调用,因此不要在 Servlet 成员变量中随意存请求相关的可变数据。
- destroy():容器关闭或卸载该应用时,会调用
destroy进行资源清理,整个生命周期结束。
你可以把init理解成餐厅开业前的后厨准备——只做一次;doGet是每一次被叫到号时的工作过程——反复执行;destroy是打烊后的清理工作——最后做一次。
为了直观验证这个流程,把刚才代码里的System.out.println打开,启动 Tomcat 后第一次访问/hello,控制台会打印出初始化方法执行的字样;刷新页面不会再次打印初始化;停止 Tomcat 时则可能看到 destroy 相关输出(需要在类里额外重写 destroy 方法才会看到)。这个实验强烈建议做一次,做完你对 Servlet 生命周期就彻底有底了。
4.3 URL 映射规则与路径组合的细节
访问 Servlet 的完整路径由两部分组成:项目上下文路径 + Servlet 映射路径。
如果Application context设为/,那么访问路径直接就是http://localhost:8080/hello。如果Application context保持默认的servlet-demo,那么完整访问路径就是http://localhost:8080/servlet-demo/hello。
很多人在这个环节 404,就是搞混了这两段路径的拼接方式。记住一个口诀:上下文路径配在容器部署里,映射路径写在注解或 web.xml 里,浏览器访问时把两者顺序拼起来。
另外,映射路径也支持模糊匹配,比如@WebServlet("/user/*")可以同时匹配/user/list、/user/detail等地址,但初学者先用精确匹配就好,模糊匹配容易把访问范围搞大,反而不利于排查问题。
5. 常见问题与排查技巧实录
5.1 快速对照表:遇到问题先看这张表
下面这些是我在教学和实战中遇到频率最高的几个问题,整理成速查表,遇到报错先对照排查,能省大量时间。
| 现象 | 最可能的原因 | 解决办法 |
|---|---|---|
访问http://localhost:8080/hello显示 404 | URL 路径拼错或映射路径不一致 | 核对Application context和@WebServlet值是否拼对 |
| Tomcat 启动时端口被占用 | 8080 端口已被其他进程占用 | 关闭占用进程,或修改 Tomcat 的server.xml端口号 |
| IDEA 控制台中文乱码 | JDK 或 Tomcat 默认编码不是 UTF-8 | 在运行配置的 VM options 中加-Dfile.encoding=UTF-8 |
启动时报A child container failed during start() | Tomcat 版本和 Servlet 依赖版本冲突 | 换成 Tomcat 8.5/9.0 + javax.servlet-api 4.0.1 |
| Servlet 类写了但运行时报 404 | 类没有编译进部署产物 | 确认java目录已标记为 Sources Root,重新部署 |
| 页面中文内容乱码 | 响应编码未设置 | 在代码里调用response.setContentType("text/html;charset=UTF-8") |
5.2 三个必看的排查细节
排查一:区分 404 和 500
404 表示找不到资源,问题大多出在路径上——映射错了、上下文路径不对、部署产物没生成;500 表示服务器内部异常,问题大多出在代码上——空指针、类找不到、依赖冲突。看到 500 别急着改路径,去 Tomcat 日志里翻异常堆栈,一找一个准。
排查二:部署列表里没有可用 Artifact
在配置 Deployment 时,如果Artifact列表是空的,说明工程缺少 Web 构件定义。右键工程名,选择Open Module Settings,在Facets里点击+,先添加Web,再在Artifacts里点击+,选择Web Application: Exploded,把 Web 目录指向src/main/webapp,重复上面的添加步骤即可解决。
排查三:Tomcat 启动后访问返回 404,但日志显示 Started
这种情况最迷惑人。其实 “Started” 只表示容器启动成功,不代表你的应用一定部署成功。你需要观察启动日志中是否有Deploying web application directory、Deployment of web application archive之类的行。如果没有部署记录,多半是部署配置不正确,重新检查 Deployment 里的 Artifact 是否添加成功、Application context 是否设置合理。
5.3 独家避坑:开发阶段的小技巧
我个人的习惯是,开发阶段一定用war exploded部署,而不是打 war 包。exploded 部署相当于把解压后的目录直接扔给 Tomcat,修改类或静态资源后,容器的热重载机制能更快地感知变化,省去反复打包的沉闷等待。配合 IDEA 的运行配置里的 “Build project automatically” 选项,能让开发节奏顺畅很多。
另一个小技巧是:在 IDEA 中给 Tomcat 运行配置的VM options里加上一句:
-Dfile.encoding=UTF-8这能解决控制台输出、日志和编码相关的一大部分乱问题。很多人以为乱码是代码问题,折腾半天,其实只是 JVM 启动参数没配 UTF-8。
最后分享一点我的体会
说句心里话,Servlet 技术本身已经是 Java Web 体系里最古老的环节之一了,现在新项目基本都直接用 Spring Boot 内嵌容器开发。但我依然建议所有入行 Java Web 的人,老老实实亲手建一次 Servlet 工程、写一遍 doGet、配一次 Tomcat。因为后面框架里那些看似神奇的功能——请求路由、参数绑定、响应封装,本质都是在 Servlet 规范之上做的封装。你把 Servlet 的请求、响应、映射、生命周期这四件事弄明白了,学 Spring MVC 时你会发现新世界的大门是敞开的。
我个人在实际操作中最受益的一个习惯,是在跑通第一个 Servlet 后,主动做一个小扩展:写一个LoginServlet,模拟表单提交,用request.getParameter("username")读取参数,再让页面显示“欢迎 xxx”。这个小练习能把doPost、表单提交、请求参数读取这几个核心点全部串起来,做一次比看十遍教程都有用。
最后再留一个延伸方向:在你已经跑通的servlet-demo项目里,试着通过 JSP 页面发送表单请求,路径指向刚才的HelloServlet,再用request.getParameter("name")动态拼接 HTML。这一步做完,你的 Java Web 基础就算真正筑基成功了。