用 IDEA 2023 创建一个 Servlet 项目,这事看着简单,但真正动手时你就会发现,卡点往往不在 Servlet 代码本身,而在“项目怎么建、Tomcat 怎么配、依赖怎么引”这一串前置流程上。很多新手照着老教程走,结果 IDEA 版本不同、Tomcat 版本不同,下一步就报错,然后整个人直接懵掉。
这篇文章我按自己的实际操作路径,给你一条从零到一跑通 Servlet 项目的完整流程。包括环境怎么选型、IDEA 2023 里怎么建 Maven Web 工程、Tomcat 怎么接进来、第一个 Servlet 怎么写、怎么返回 JSON,最后还会把请求参数处理、中文乱码、404/500 这类高频问题一起讲清楚。适合刚学 Java Web 的初学者,也适合被 IDEA 折腾过、想系统梳理一遍的同学参考。
1. 环境准备:版本选对,后面少踩一半坑
1.1 我的开发环境清单
先说结论,再解释为什么这么选。我这次使用的组合是:
- IDEA 2023.2(Ultimate 版)
- JDK 1.8(实际用 8 还是 17 都可以,建议按你们公司/学校的项目基线来)
- Maven 3.8(IDEA 内置的 Maven 也够用)
- Tomcat 9.0.8x(重点:不是 Tomcat 10)
- Servlet API 4.0(javax.servlet 坐标)
这套组合看起来“老”,但它是目前网上绝大多数教程、教材能直接对得上的版本。如果你用 Tomcat 10 或 11,Servlet 包名从javax.servlet变成了jakarta.servlet,很多老代码、老教程里的 import 全部要改。新手阶段没必要给自己加这个负担,先用 Tomcat 9 把原理跑通,后面要升级再升级。
1.2 为什么选择 Maven 而不是手搭目录
有些朋友学 Servlet 的时候,用的是最原始的方式:手动创建WEB-INF/classes目录、手动扔 jar 包、手动编译。这套流程在 2010 年前后是主流,但现在再用就有点折磨自己了。
用 Maven 的好处有三个:
- 依赖管理省心。
javax.servlet-api、jstl、jackson这些库,在pom.xml里写个坐标就自动下载,不用满世界找 jar 包。 - 目录结构约定俗成。
src/main/java、src/main/resources、src/main/webapp是 IDEA 和 Maven 都认的标准布局,项目一创建就是规范的。 - 打包部署方便。
mvn package直接出 war 包,往 Tomcat 的webapps目录一扔就能跑。
IDEA 2023 内置了对 Maven 的完整支持,你不用额外装什么插件,直接用它自带的 Maven 就行。
1.3 关于 IDEA 版本与授权的一点建议
IDEA 2023 分为 Community(社区版)和 Ultimate(旗舰版)。社区版免费,但早期版本不支持 Tomcat 集成和 Java EE/Web 开发,只能当普通 Java 编辑器用。Ultimate 版功能完整,但需要付费订阅。
网上有很多关于“激活”“破解”的内容,我的态度很明确:不要碰。一个是安全风险,破解工具里面夹带什么你根本不知道;另一个是稳定性问题,IDEA 更新后破解很容易失效,到时候项目正写着突然打不开,哭都来不及。想省钱就用社区版配合外部 Tomcat 手动部署,想省心就用官方正版或试用期,JetBrains 对学生和开源开发者还有免费授权计划,走正规渠道最稳妥。
2. 创建 Servlet 项目:IDEA 2023 里的完整操作流程
2.1 用 Maven 骨架快速创建 Web 工程
IDEA 2023 创建 Servlet 项目,说到底是一件事:创建一个“带 Web 目录的 Maven 项目”。操作路径如下。
打开 IDEA,选择New Project。在左侧选择Maven,然后勾选Create from archetype,在列出的骨架里找到maven-archetype-webapp。
提示:如果列表里没有这个骨架,点一下
Add Archetype,手动填上org.apache.maven.archetypes:maven-archetype-webapp:1.4,IDEA 会自动从中央仓库拉取。
接下来填写GroupId和ArtifactId。GroupId一般用公司域名倒写,比如com.example;ArtifactId是项目名,比如servlet-demo。这俩会决定你后面package路径的根目录,建议一次想好,避免后面大量改包名。
填完之后 IDEA 会开始下载依赖,第一次可能比较慢。等右下角进度条跑完,项目结构就出来了。
2.2 认识生成的项目结构
创建完成后,src/main/webapp目录下会自动生成一个index.jsp和一个WEB-INF/web.xml。pom.xml在最外层。这和我们平时写普通 Java 项目不太一样,多出来的webapp目录就是将来部署到 Tomcat 里的 Web 根目录。
我自己习惯把webapp类比成“一个网站的根文件夹”。你放在webapp下的index.jsp、静态图片、CSS、JS,用户通过浏览器访问项目路径时能看到;而WEB-INF是一个受保护的区域,浏览器直接访问不到,只有 Servlet 通过forward或include才能跳转进去。
先把自动生成的web.xml打开看一下,里面通常是一个 Servlet 3.0 或 4.0 的配置文件头。记一下版本号,后面写映射时可能要用。
2.3 pom.xml 添加 Servlet 依赖
在 IDEA 里打开pom.xml,在<dependencies>标签内加上 Servlet API 和 JSP 的依赖。
<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> </dependencies>注意这里的scope是provided,意思是“编译和测试时需要,但打包时不要打进去”。因为 Tomcat 自己就带了一套 Servlet 实现,如果打进去了反而会和 Tomcat 本身的类冲突。这个细节很多人第一次不注意,后面部署到服务器上各种类重复、版本冲突,排查半天。
2.4 配置 Tomcat 运行环境
这是新人最容易卡住的一步。IDEA 里写完代码不能直接双击运行,你得把项目挂到一个 Servlet 容器上。这里我用的是本地 Tomcat,流程如下。
先下载 Tomcat 9,解压到一个没有中文和空格的路径,比如D:\apache-tomcat-9.0.87。然后在 IDEA 里点右上角的Add Configuration,选择Tomcat Server下的Local。
在Tomcat Server Settings里选中你刚才解压的目录,IDEA 会自动识别。接下来切到Deployment页签,点加号,选择Artifact,选中项目名后面带war exploded的那一项。war exploded是“展开的 war 包”,意思是用目录方式部署,好处是修改代码后可以热更新,不用每次重启 Tomcat。
Application context我一般改成/或者/servlet-demo。这决定了你访问时的根路径。如果设成/servlet-demo,那么访问 Servlet 的 URL 就是http://localhost:8080/servlet-demo/YourServlet。
最后在Server页签把On frame deactivation改成Update classes and resources,这样切到浏览器时自动更新资源,开发体验会顺滑很多。
3. 第一个 Servlet:生命周期、映射方式与代码实测
3.1 Servlet 生命周期到底在说什么
写代码之前,先花两分钟理解 Servlet 的生命周期。你可以把它类比成“开一家小吃店”:
init():店面装修好,开门营业前的准备。整个生命周期只执行一次,适合做初始化工作,比如读取配置、建立数据库连接。service():客人点餐环节。每次 HTTP 请求进来都会经过它,它会根据请求方法是 GET 还是 POST,再分发到doGet()或doPost()。destroy():店面关门清算,整个生命周期只执行一次,适合释放资源。
明白了这个流程,你写代码时心里就有数了:初始化逻辑放init,主业务逻辑放doGet/doPost,清理逻辑放destroy。不要什么都往doGet里塞。
3.2 写一个最简单的 HelloServlet
在src/main/java下新建一个包,比如com.example.servlet,然后创建HelloServlet类,继承HttpServlet,重写doGet方法。
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 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8"); PrintWriter writer = resp.getWriter(); writer.write("<h1>Hello Servlet, IDEA 2023</h1>"); } }这里用到了@WebServlet注解,直接把/hello这个路径映射到当前类。IDEA 2023 会对注解做代码提示,写起来比较方便。
写完这一步,如果你用的是前面配置的 Tomcat,直接点右上角运行按钮。浏览器访问http://localhost:8080/hello(如果 Application context 是/),或者http://localhost:8080/servlet-demo/hello,就能看到输出内容。
注意:
resp.setContentType("text/html;charset=utf-8")这行一定不要省。不设置字符集的话,浏览器会按照默认编码解析,中文字符大概率乱码。
3.3 注解映射和 web.xml 映射的区别
很多教材还用web.xml配置 Servlet 映射。在 Servlet 3.0 之前,没有@WebServlet注解,必须在web.xml里写<servlet>和<servlet-mapping>标签。到了 Servlet 3.0 以后,注解方式成了主流。
两者各有适用场景:
- 注解方式:代码量少,类和映射关系一目了然,适合小型项目。
web.xml方式:集中管理映射,方便运维人员和架构师统一查看,适合需要统一控制、或者不能随意改代码的场景。
如果使用web.xml配置,写法是这样的:
<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>使用web.xml时,要把HelloServlet类上的@WebServlet注解去掉,否则两处都配置,启动时会因为重复映射报java.lang.IllegalArgumentException。
3.4 URL 匹配规则:斜杠里的学问
@WebServlet里的路径不是随便写的,它有一套匹配优先级规则,新手经常在这里踩坑。
- 精确匹配:
/hello,完全相等才匹配。 - 目录匹配:
/api/*,匹配/api开头的所有路径。 - 扩展名匹配:
*.do,匹配所有以.do结尾的路径。 - 默认匹配:
/,匹配所有未命中的请求。
优先级从高到低是:精确匹配 > 目录匹配 > 扩展名匹配 > 默认匹配。你写/的时候要特别小心,它会拦截所有静态资源。比如你放了一张a.jpg,如果没有单独的静态资源配置,直接访问/a.jpg会被这个 Servlet 接管,然后 404 或者报错。
4. 进阶实操:请求参数、JSON 返回与外部 API 调用
4.1 获取 GET 和 POST 请求参数
写 Servlet 的意义不只是输出一段 HTML,更多时候是接收前端传过来的参数,做业务处理,再返回结果。获取参数的核心方法是request.getParameter()。
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("utf-8"); resp.setContentType("text/html;charset=utf-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); PrintWriter writer = resp.getWriter(); if ("admin".equals(username) && "123456".equals(password)) { writer.write("登录成功"); } else { writer.write("用户名或密码错误"); } } }这里有一个关键细节:req.setCharacterEncoding("utf-8")必须放在getParameter之前调用,否则 POST 请求里带的中文参数一样会乱码。GET 请求的乱码问题主要在 Tomcat 的server.xml里的URIEncoding配置,不过 Tomcat 8.0 之后默认就是 UTF-8,一般不用改。
4.2 返回 JSON 而不是 HTML
现在前后端分离很常见,Servlet 更多是用来做接口,返回 JSON。两步就能搞定:设置Content-Type为application/json,然后把字符串按 JSON 格式输出。
@WebServlet("/api/user") public class UserServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("application/json;charset=utf-8"); String json = "{\"name\":\"张三\",\"age\":25}"; resp.getWriter().write(json); } }如果数据结构简单,手拼 JSON 没问题。但字段一多,手拼会非常容易出错。推荐引入 Jackson 库,在pom.xml里加依赖。
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency>然后定义一个普通的 Java 类,用ObjectMapper转成 JSON 字符串。
ObjectMapper mapper = new ObjectMapper(); User user = new User("张三", 25); String json = mapper.writeValueAsString(user); resp.getWriter().write(json);writeValueAsString可以直接把对象序列化成 JSON 字符串,返回给前端。Jackson 是 Fastjson 之外比较主流的选择,稳定性和社区活跃度都靠谱。
4.3 在 Servlet 里调用大模型 API 接口
前面说到的“体验 servlet 调用大模型 api 接口”,其实就是让 Servlet 充当后端中转层。浏览器不能直接拿着密钥去调第三方大模型接口,那样会把API Key暴露在前端代码里,非常危险。正确做法是 Servlet 接收前端请求,再由后端去请求大模型服务,拿到结果后返回给前端。
代码写起来不复杂,用 JDK 自带的HttpURLConnection就能实现。这里我以一个通用的第三方大模型接口为例。
@WebServlet("/api/chat") public class ChatServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("utf-8"); resp.setContentType("application/json;charset=utf-8"); String prompt = req.getParameter("prompt"); String apiKey = System.getenv("LLM_API_KEY"); // 建议从环境变量读取 // 构造请求体 String body = "{\"prompt\":\"" + prompt + "\",\"max_tokens\":100}"; URL url = new URL("https://your-llm-api.example.com/v1/completions"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("Authorization", "Bearer " + apiKey); conn.setDoOutput(true); conn.getOutputStream().write(body.getBytes("utf-8")); // 读取响应 BufferedReader reader = new BufferedReader( new InputStreamReader(conn.getInputStream(), "utf-8")); StringBuilder result = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { result.append(line); } reader.close(); conn.disconnect(); resp.getWriter().write(result.toString()); } }有几点提醒你:API Key不要硬编码在代码里,更不要提交到 Git 仓库,推荐用环境变量或配置文件管理;外部接口的connectTimeout和readTimeout一定要设置,否则接口卡住时你的 Servlet 线程也会一直挂着;生产环境建议用线程池 + 连接池,直接HttpURLConnection在高并发下性能不够看。
5. 常见问题与排查技巧实录
5.1 高频报错速查表
我在带新人和自己学习的过程中,遇到的典型报错基本集中在下面几类。整理成一张表,方便你直接对照排查。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 浏览器 404 | URL 路径写错,或Application context起头不对 | 检查访问路径是否包含/项目名,再核对@WebServlet里的映射值 |
| 浏览器 404 且 Tomcat 报错 | web.xml 和注解重复配置映射 | 二选一,不要同时配 |
后端 500,控制台ClassNotFoundException: javax.servlet | pom 里没引javax.servlet-api,或 scope 写错 | 添加依赖,scope 用provided |
| 启动时端口占用 | 8080 被其他程序占用 | 改 Tomcat 端口,或杀掉占用进程 |
| 中文变成问号/乱码 | 请求或响应编码未设置 | req.setCharacterEncoding("utf-8")和resp.setContentType("...charset=utf-8") |
java.lang.NoClassDefFoundError | jar 包冲突或缺少依赖 | 检查 pom,mvn dependency:tree查看依赖树 |
| Tomcat 闪退 | JAVA_HOME 未配置,或 JDK 版本不匹配 | 确认JAVA_HOME指向正确 JDK 路径 |
5.2 依赖作用域:provided 和 compile 的坑
这个坑值得单独拿出来说。javax.servlet-api的scope如果写成默认的compile,打包生成的 war 包里会带上servlet-api.jar。部署到 Tomcat 后,Tomcat 自身的lib目录里也有一份servlet-api.jar,两边的类就冲突了。
常见的表现是:
- Tomcat 启动时打印
Duplicate jar警告。 - Servlet 类加载异常,抛出
ClassCastException。 - 某些时候能跑,但行为莫名其妙。
如果你是跟着模板创建的 pom,一定要检查javax.servlet-api的scope是否被设置成了provided。这个provided的含义就是“我编译时需要,但运行时容器已经提供了,不要再打包”。
5.3 修改代码后不生效:热部署设置
IDEA 里运行 Tomcat 时,默认并不会每次自动编译并更新到服务器。很多人改了代码,刷新浏览器没有变化,就以为是代码写错了。
解决方法是把 Run Configuration 里的On frame deactivation设置为Update classes and resources。这样你从 IDEA 切到浏览器时,IDEA 会自动把变更的 class 和资源推送到 Tomcat 的部署目录。注意,如果新增了方法签名、改了类结构这种大改动,还是需要重启 Tomcat,不然会出现NoSuchMethodError之类的情况。
如果你改了web.xml、pom.xml,或者新增了依赖,也建议手动重启,别撑着热部署硬跑。
5.4 浏览器直接访问 WEB-INF 下的页面
这个问题我当年也踩过。把 JSP 放到WEB-INF下面,然后访问http://localhost:8080/项目名/WEB-INF/index.jsp,结果是 404。
其实这是 Servlet 规范里的一种保护机制。WEB-INF目录对浏览器是封闭的,外部请求永远无法直接拿到里面的文件。如果你想访问WEB-INF下的 JSP,必须通过 Servlet 转发:
req.getRequestDispatcher("/WEB-INF/index.jsp").forward(req, resp);这样做的好处是:页面文件对外不可见,所有访问都强制走 Servlet,你做登录校验、权限控制就非常方便。这是一套经典的 MVC 思路,Servlet 当 Controller,JSP 当 View。
几个我后来才想明白的经验
写 Servlet 项目这件事,难点真的不在于 API 本身,而在于项目构建工具和容器之间的配合。IDEA 2023 相比之前的版本,对 Java Web 的支持已经算是非常顺滑了,只要你把 Maven 工程创建、Tomcat 配置、依赖声明这三件事理顺,后面写代码就是水到渠成。
我自己的感受是,刚接触时要刻意练习“从浏览器地址栏到 Servlet 代码”的映射思维:输入一个 URL,请求怎么被 Tomcat 接收,怎么根据web.xml或注解找到对应类,怎么进入doGet/doPost,方法里怎么处理参数、返回响应。这条链路每想通一个节点,你就离真正理解 Java Web 更进一步。
之后你再学 Spring MVC、Spring Boot,会发现它们的底层就是对这套 Servlet 机制的封装和增强。把 Servlet 项目亲手从零建一遍,绝对不亏。