很多学完 Java SE 的朋友,一进入 JavaWeb 就卡住了。教材翻了几页,视频刷了几个,代码也照着敲了,结果项目一运行就报错:404、端口被占、lib 目录空、访问不到 Controller……这些场景我当年全都经历过。说实话,JavaWeb 这个阶段学习曲线的陡峭程度,很大一部分不是来自语言本身,而是来自"环境配置 + 项目结构 + 部署方式"这套东西,它跟之前写命令行程序完全是两码事。
这篇博文是 JavaWeb 系列的第一篇,我在里面把我现在带新人时最常遇到的坑、最基础的配置逻辑、以及怎么从零把一个能跑的 JavaWeb 项目在 IDEA 里跑起来,完整梳理了一遍。只要你能把这篇里的项目跑通,后面学 Servlet、Filter、事务、Spring,就都有了一个可依赖的基础。
1. 开发环境选型:为什么说"版本组合错了,项目都跑不起来"
先说环境,因为 JavaWeb 入门的第一个噩梦就是版本。很多教程直接说"装 JDK、装 Tomcat、装 IDEA",但没人告诉你这些组件之间是有匹配关系的。我见过太多例子:JDK 17 + 老版本 Tomcat 9,启动直接报UnsupportedClassVersionError;IDEA 版本太老,识别不了新版本的 Maven 结构。这不是你能力问题,是版本组合给你埋了雷。
1.1 一套稳定到"随手就能跑"的版本搭配
我现在带团队新人,统一推荐下面这套组合。它不是最新的,但胜在稳定、教程覆盖率高、网上搜报错基本都有答案:
| 组件 | 推荐版本 | 选它的理由 |
|---|---|---|
| JDK | 1.8(8u202) | JavaWeb 传统项目最通用的版本,所有框架都能兼容 |
| IDEA | 2022.x 及以上 | 对 Maven、Tomcat 集成支持成熟稳定 |
| Tomcat | 8.5.x / 9.0.x | 支持 Servlet 3.1/4.0,覆盖绝大多数教学场景 |
| Maven | 3.6.3 | 稳定不折腾,换 3.8+ 后部分内网镜像会踩 TLS 坑 |
| MySQL | 5.7 / 8.0 | 5.7 老项目多,8.0 新项目多,选哪个都行 |
| mysql-connector-java | 5.1.49(配 5.7)/ 8.0.x(配 8.0) | JDBC 驱动版本必须配合数据库大版本 |
这里有个细节想多说一句:JDK 版本真的不用追新。有人觉得 JDK 17 都出了,我学 JavaWeb 干嘛还用 8?原因很简单,你现在学的 Servlet、JSP、Tomcat 部署这套东西,大量历史项目和教材都是基于 JDK 8 写的。等你用 JDK 8 把这套流程跑通了,理解了底层原理,再往 Spring Boot 这种自带 Tomcat 的框架走,JDK 17 天然支持,完全没有冲突。反过来,如果你一上来就用 JDK 17 配老教程,光是javax.servlet和jakarta.servlet的包名差异就够你怀疑人生。
1.2 一个我现在看来"白折腾"的教训
我自己第一次搭环境的时候,非要去装最新版 Tomcat 10,因为觉得新。结果 Servlet 的包名从javax.servlet改成了jakarta.servlet,网上所有老教程的import javax.servlet.*全部要改,连 IDEA 自动生成的 web.xml 头都变了。那时候我不懂什么叫 API 迁移,就在那儿改代码,改到崩溃。
所以第一篇文章里我特别想把这句话说出来:学 JavaWeb 的第一阶段,选主流偏旧的环境,不是保守,是明智。你要折腾新特性,等理解了核心原理之后再折腾完全不迟。工具是帮助你理解概念的,不是拿来攀比的。
2. IDEA 里新建 JavaWeb 项目的完整配置逻辑
接下来是重头戏:怎么在 IDEA 里把一个 JavaWeb 项目从无到有地建出来,并且成功部署到 Tomcat 上。
2.1 项目创建方式的选型:Maven 还是手动建目录
现在创建 JavaWeb 项目主要有两条路:一条是 IDEA 自带的"Java Enterprise"模板或者直接建普通项目再手动加 web 目录,另一条是 Maven 项目改造成 Web 项目。我强烈推荐走 Maven,原因有三个:
- 依赖管理:Servlet、JSP、数据库驱动这些 Jar 包不需要你手动下载、手动复制到 WEB-INF/lib 下面,Maven 一行坐标搞定。
- 目录规范:Maven 强制的
src/main/java、src/main/resources、src/main/webapp结构,从源头养成良好分层习惯。 - 后续衔接:你现在用 Maven 建的 JavaWeb 项目,跟以后学 Spring Boot 的项目结构逻辑是一样的,不需要二次适应。
具体操作路径是:File → New → Project → Maven(不要选 archetype,空项目即可),如果你用的是 IDEA 2022+,注意选Maven而不是Spring Initializr(那是 Spring Boot 的,以后再用)。
建完之后,手动补上 Web 模块的目录和配置:
项目根目录 ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ ├── resources │ │ └── webapp │ │ └── WEB-INF │ │ └── web.xml注意一点:web.xml 不一定非要在WEB-INF下手动创建。更快的做法是在 IDEA 里给项目添加Web Facet。操作路径:右键项目 →Add Framework Support→ 勾选Web Application。IDEA 会自动给你生成webapp/WEB-INF/web.xml。
2.2 Maven 坐标:最基本的三个依赖
在pom.xml里加上这几个依赖。我给你贴的是最小可用集合:
<dependencies> <!-- Servlet 核心 API --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- JSP 支持 --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.11</version> </dependency> <!-- JSTL 标签库(后面 JSP 页面要用) --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>关于scope=provided这个概念,我用一句话解释你就能懂:Servlet API 这个 Jar 其实 Tomcat 自己就带着,你写代码的时候需要它来编译,但打包部署的时候不应该把它打进你的项目里,否则会和 Tomcat 自带的产生冲突。provided就是告诉 Maven:编译时用我,打包时别带我。
提示:如果你用的 Tomcat 是 10+,Servlet 的包名会变成
jakarta.servlet,上面的坐标也得换成jakarta.servlet:jakarta.servlet-api。再次强调,所以我推荐 9.0.x。
2.3 Tomcat 配置与 Artifact 部署:最容易出错的一步
依赖加完之后,需要把项目跑起来。这里要理解几个概念,不然你在 IDEA 配置界面会看得一头雾水:
- Facet:项目的"侧面能力"。一个项目可以有 Web Facet(能处理网页请求)、Spring Facet(能支持 Spring 注解)等。你可以把 Facet 理解成"给项目装插件"。
- Artifact:项目的"打包产物"。JavaWeb 项目的产物形式通常是
war,你可以理解成"交给 Tomcat 运行的文件包"。 - Tomcat:运行环境。它把 war 抠出来放到自己的 webapps 目录下执行。
具体配置步骤:
- 点击 IDEA 右上角Add Configuration→ 点“+”→ 选择Tomcat Server → Local。
- 在Server选项卡里,Tomcat 需要选择你本地解压的 Tomcat 目录。
- 切到Deployment选项卡,点“+”→Artifact→ 选你的项目名(
war exploded那个)。这里我建议选war exploded,原因是这种模式会让 IDEA 直接把编译后的资源发布到 Tomcat,修改 Java 代码后配合热部署,刷页面就能看到效果,调试效率高很多。war模式则要重新打包,慢,适合发布。 - 指定Application context,比如填
/javaweb01,那你访问项目的根路径就是http://localhost:8080/javaweb01/。
这里有个我在实际教学中反复强调的细节:如果你的 Deployment 列表里是空的,说明项目没有被识别成 Web Artifact。解决办法是回到File → Project Structure → Artifacts,手动创建"Web Application Exploded"Artifact,然后再去配置里添加。
很多新手在 Tomcat 那里配置了半天,一运行发现 IDEA 提示没有可用 Artifact,原因就是漏掉了这一步。
3. 从零实现一个完整的用户管理模块:请求、数据与页面
环境搞定了,下面我用一个最常见的"用户管理"模块,把 JavaWeb 的技术点串起来讲。这个案例麻雀虽小,但包含了浏览器请求、Servlet 处理、JDBC 查库、页面渲染整条链路。
3.1 数据库准备:建表与基础数据
先在 MySQL 里创建一个javaweb_demo数据库,并建一张用户表:
CREATE DATABASE javaweb_demo DEFAULT CHARACTER SET utf8mb4; USE javaweb_demo; CREATE TABLE t_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), created_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO t_user(username, password, nickname) VALUES ('admin', '123456', '管理员'), ('zhangsan', 'abc123', '张三');注意创建数据库时我特意指定了utf8mb4。字符集这个问题,新手阶段不重视,后面写到 JSP 页面时一遇到中文就乱码,再回头溯因,基本都是这里埋下的根源。
3.2 标准三层结构:实体、连接工具、DAO
实体类 User.java(放在com.example.entity包下):
package com.example.entity; import java.time.LocalDateTime; public class User { private Integer id; private String username; private String password; private String nickname; private LocalDateTime createdTime; // 省略 Getter/Setter,生产环境建议直接用 Lombok }JDBC 工具类 JdbcUtil.java(放在com.example.util包下):
package com.example.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class JdbcUtil { private static final String URL = "jdbc:mysql://localhost:3306/javaweb_demo?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "你的数据库密码"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }serverTimezone=Asia/Shanghai这个参数是很多新手第一次连接 MySQL 8.0 时报错The server time zone value ...的罪魁祸首。在 JDBC 连接串里,时区必须明确指定,否则驱动会拿本地时区去对比服务器时区,两边不一致就抛异常。characterEncoding=utf8则是保证你向数据库写入中文时按 UTF-8 编码传输。
UserDao.java(放在com.example.dao包下)——只负责跟数据库打交道:
package com.example.dao; import com.example.entity.User; import com.example.util.JdbcUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; public class UserDao { public List<User> findAll() { String sql = "SELECT id, username, password, nickname, created_time FROM t_user ORDER BY id"; List<User> list = new ArrayList<>(); try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { User u = new User(); u.setId(rs.getInt("id")); u.setUsername(rs.getString("username")); u.setPassword(rs.getString("password")); u.setNickname(rs.getString("nickname")); u.setCreatedTime(rs.getTimestamp("created_time").toLocalDateTime()); list.add(u); } } catch (SQLException e) { e.printStackTrace(); } return list; } }3.3 控制器 Servlet:接收请求、调用业务、转发视图
Servlet 是 JavaWeb 里最前端的 Java 代码入口。我用一个UserListServlet来演示它的三件事:
package com.example.servlet; import com.example.dao.UserDao; import com.example.entity.User; 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.util.List; @WebServlet("/user/list") public class UserListServlet extends HttpServlet { private final UserDao userDao = new UserDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 设置编码,防止中文响应乱码 req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); resp.setContentType("text/html; charset=utf-8"); // 1. 调用 DAO 查询数据 List<User> userList = userDao.findAll(); // 2. 把数据塞进 request 作用域 req.setAttribute("userList", userList); // 3. 服务器内部转发到 JSP 页面渲染 req.getRequestDispatcher("/WEB-INF/pages/user-list.jsp").forward(req, resp); } }这段代码里最关键的是req.getRequestDispatcher(...).forward(...)。它做的事情是:Servlet 在服务器内部替浏览器把请求转发给 JSP,浏览器地址栏不变,但页面展示的是 JSP 渲染后的结果。这里要理解"转发"和"重定向"的区别:转发是服务端行为,重定向是浏览器行为;转发后 request 里的数据还在,重定向后就没有了。JavaWeb 初学者容易在这两个概念上栽跟头。
3.4 JSP 页面:用 JSTL 渲染数据
在src/main/webapp/WEB-INF/pages/user-list.jsp下创建页面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>用户列表</title> </head> <body> <h2>用户列表</h2> <table border="1" cellpadding="8" cellspacing="0"> <tr> <th>ID</th> <th>用户名</th> <th>密码</th> <th>昵称</th> <th>创建时间</th> </tr> <c:forEach items="${userList}" var="user"> <tr> <td>${user.id}</td> <td>${user.username}</td> <td>${user.password}</td> <td>${user.nickname}</td> <td>${user.createdTime}</td> </tr> </c:forEach> </table> </body> </html>把页面文件放在WEB-INF目录下,是为了避免用户直接通过浏览器地址访问到这个 JSP 文件。因为WEB-INF下的资源对浏览器不可见,只有通过 Servlet 内部转发才能展示。这是 JavaWeb 一个很重要的安全习惯:视图文件不能直接暴露给外部访问,必须由 Servlet 控制入口。
到这一步,启动 Tomcat,浏览器输入http://localhost:8080/javaweb01/user/list,你应该能看到页面上的两条用户数据。这就是一个 JavaWeb 项目的最小闭环。
4. 一次完整请求的流转链路与几个必须讲清楚的原理
项目跑通了,别急着往下学新东西。把刚才这次请求的完整链路在脑子里过一遍,比多做十个练习都管用。我把这条链路拆成人话版本,再配上几个你后面一定会遇到的技术原理。
4.1 从浏览器地址到 JSP 页面,中间发生了什么
你在浏览器输入http://localhost:8080/javaweb01/user/list,按回车。接下来发生的事,我逐步拆给你看:
- 浏览器向 Tomcat 的 8080 端口发起一次 HTTP 请求,路径是
/javaweb01/user/list。 - Tomcat 根据 URL 里的
/javaweb01匹配到你的 Web 应用,剩下的/user/list进入应用内部的路由匹配。 - 容器(Tomcat 是 Servlet 容器)根据注解
@WebServlet("/user/list")找到对应的UserListServlet,创建一个新实例(或者复用池里的实例)去执行doGet方法。 - Servlet 里
userDao.findAll()通过 JDBC 驱动跟 MySQL 完成一次查询。 - 查询结果以
List<User>的形式被塞进request.setAttribute("userList", ...)里。 request.getRequestDispatcher(...).forward()把 request 对象连同里面的数据一起转交给 JSP。- JSP 把自己翻译成 Servlet 再编译成 Java 类,执行后生成 HTML 字符串。
- Tomcat 把这段 HTML 作为 HTTP 响应返回给浏览器,你看到了表格页面。
这里面有一个所有新手都会好奇的问题:Servlet 实例到底有几个?在默认情况下,对同一个 Servlet 类,Tomcat 只会创建一个实例,所有请求都走这一个实例。如果你的 Servlet 里有成员变量,且你在这个变量上做了写操作,那么并发请求就会产生数据竞争。这就是为什么 Servlet 里我们通常只用局部变量,或者像上面代码里那样把 DAO 作为不可变引用使用。
4.2 转发和重定向到底怎么选
这个知识点在面试里经常问到,在实际写项目时也经常要选。我给你一个非常实操的判断标准:
- 你想让浏览器地址栏保持不动,request 里的属性还能接着用,选转发(forward)。比如:查完用户数据后去渲染列表页。
- 你想让浏览器地址栏发生变化,或者要防止表单重复提交,选重定向(sendRedirect)。比如:用户登录成功后跳转到首页。
新手容易犯的错误是在登录成功后用了转发,然后一刷新浏览器,又把表单提交了一遍,用户数据重复写入。这时候应该用resp.sendRedirect(req.getContextPath() + "/user/list")来重定向。注意,这里我用了req.getContextPath(),拿到的是/javaweb01,这就是前面配置的 Application context。代码里写死/javaweb01也可以跑,但一旦你换了 context 路径,代码就全崩了。所以永远用getContextPath()动态拼接,这是我从一开始就想让你养成的好习惯。
4.3 为什么一定要用 PreparedStatement
上面UserDao里我写的都是PreparedStatement,没有用Statement。原因不只是防 SQL 注入这么简单。用Statement去拼接 SQL 需要你把用户输入直接揉进字符串里,改起来麻烦且不可读;而PreparedStatement用占位符?传参,数据库驱动会自动做转义,同时执行计划还能被预编译复用。
我还见过有人把PreparedStatement当成普通Statement的替代品之后,反而不会写了,常见错误是:
// 错误写法:在 SQL 字符串里直接拼参数 String sql = "SELECT * FROM t_user WHERE username = '" + username + "'"; ps = conn.prepareStatement(sql);这个写法本质还是拼接,PreparedStatement 的防注入优势全被浪费了。正确做法是:
// 正确写法 String sql = "SELECT * FROM t_user WHERE username = ?"; ps = conn.prepareStatement(sql); ps.setString(1, username); rs = ps.executeQuery();这两段代码的差异,表面上是风格的差异,实际上是思维方式的差异:你有没有把"数据"和"结构"分开对待。数据库 SQL 是结构,用户输入是数据,两者一旦混在一起,既有安全问题又有可维护性问题。
4.4 中文乱码问题的完整防线
中文乱码是 JavaWeb 新手崩溃率最高的问题,没有之一。乱码的本质只有一个:编码和解码用的字符集不一致。数据从浏览器到数据库,要走好几道"门",每一道门的字符集都得对齐,才能保证不出乱码。我总结了一套防线:
| 环节 | 要求 | 原因 |
|---|---|---|
| HTML 页面声明 | <meta charset="UTF-8"> | 让浏览器用 UTF-8 解析表单内容 |
| 请求解析 | req.setCharacterEncoding("UTF-8")放在 doGet/doPost 第一行 | 让 Tomcat 用 UTF-8 解码请求体 |
| 响应输出 | resp.setContentType("text/html; charset=utf-8") | 让浏览器用 UTF-8 渲染返回的 HTML |
| JDBC 连接 | characterEncoding=utf8 | 让 MySQL 驱动按 UTF-8 传输数据 |
| 数据库表 | DEFAULT CHARSET=utf8mb4 | 让 MySQL 存储和读取按 UTF-8 |
| Tomcat 配置(GET 请求) | server.xml里 Connector 增加URIEncoding="UTF-8" | GET 请求参数是写在 URL 里的,需单独指定解码集 |
这里有个小细节:req.setCharacterEncoding("UTF-8")只对 POST 请求的请求体有效。GET 请求的参数在 URL 上,Tomcat 默认用 ISO-8859-1 解码,你要在server.xml里对 Connector 设置URIEncoding="UTF-8"才能解决。我在带新人时经常看到他们只设置了 POST 的编码,然后 GET 请求带中文参数还是乱,排查很久找不到原因,十有八九就是漏了这一处。
5. 新手第一次运行 JavaWeb 项目的高频报错排查手册
这一部分我直接给你一份"症状 → 原因 → 解决"的排查手册,都是我在实际教学中遇到过的高频问题。
5.1 症状:Tomcat 启动失败,提示端口被占用
启动时如果看到:
Port 8080 is required by Tomcat v9.0 Server at localhost which has already been bound to a port.意思是 8080 端口已经被其他进程占用了。可能是你之前启动的 Tomcat 没关掉,也可能是别的软件占了端口。
排查步骤:
- 在命令行执行
netstat -ano | findstr 8080,看 8080 是谁在监听。 - 看到 PID 之后,去任务管理器确认这个进程是什么。如果是残留的 java 进程,结束掉即可。
- 如果你就是频繁遇到残留进程,建议在 IDEA 的 Tomcat 配置里,Server面板取消勾选 "After launch",这样 IDEA 在关闭项目时不会自动关掉控制台线程,能减少一部分残留进程问题。或者更简单,下次停止服务时只用停止按钮,别直接点 × 关窗口。
5.2 症状:页面 404,资源找不到
404 的原因就两类:要么路径不对,要么 Web 应用根本没有部署成功。
先分清是应用级 404 还是路径级 404。如果访问http://localhost:8080/都能看到 Tomcat 默认首页,说明 Tomcat 正常;访问/javaweb01/user/list报 404,那问题就在你的应用里。
常见的几个原因:
- Application context 配错了。你配置的是
/javaweb01,访问时却写成了/JavaWeb_01,大小写对不上。 - 注解没生效。如果你写的是
@WebServlet,确认 Tomcat 8.5+ 且项目根目录下没放旧版的web.xml来替代注解扫描。 - 页面文件放在了
src/main/webapp外面。IDEA 只把webapp目录当 Web 根目录,文件放错了地方,Tomcat 根本找不到。 - 部署列表里根本没添加 Artifact。你只是启动了 Tomcat,但没有选择 Deployment,Tomcat 跑的是空实例,自然会 404。
排查时最好的办法是看 IDEA 的 Server 启动日志。如果日志里出现Deployment of web application directory ... has finished之类的语句,说明应用部署成功了,404 大概率是路径写错了;如果压根没有这句,说明应用没部署上,去检查 Artifact。
5.3 症状:NoClassDefFoundError / ClassNotFoundException
一启动就报找不到某个类,说明依赖没打进去。新手最常遇到的是两种情况:
一是mysql-connector-java没打进去。检查方式:项目External Libraries里有没有这个 Jar,以及你在 Tomcat 配置里选的 Artifact 是不是exploded模式。如果 Artifact 是普通打包模式,依赖没跟着进去,运行就会报找不到 MySQL 驱动。IDEA 里有一项设置叫做"添加到构建路径时添加依赖到 Artifact",默认是开的,但如果你手工改动过 Artifact,可能会导致依赖列表偏移。
二是javax.servlet相关的包冲突。如果你把javax.servlet-api的 scope 写漏了,比如没设provided,那么 Maven 会把 Servlet API 打包进 WAR,和 Tomcat 自带的 Servlet 实现打架。这种冲突表现各异,有时是LinkageError,有时是ClassCastException,真的排查起来很费时间。所以记住那句话:Servlet API 依赖,scope 永远是 provided。
5.4 症状:数据库连接失败 / 时区报错
如果你连接的是 MySQL 8.0,出现下面这种报错:
The server time zone value '����ʱ��' is unrecognized or represents more than one time zone.解决方案就是我在JdbcUtil里写的连接串增加serverTimezone=Asia/Shanghai。这个报错太经典了,几乎每一个从 MySQL 5.7 升到 8.0 的新手都会碰到一次。还有一个常见问题是:
Public Key Retrieval is not allowed这是 MySQL 8.0 的 caching_sha2_password 插件导致的,解决办法是在连接串后面加allowPublicKeyRetrieval=true。如果你用的是 MySQL 5.x,连接驱动也要换成com.mysql.jdbc.Driver,而不是 8.x 的com.mysql.cj.jdbc.Driver,包名不一样,千万别搞混。
5.5 症状:JSP 里取不到后端传过来的数据,页面空白
如果你在 JSP 里用了${userList}取不到数据,检查顺序如下:
- Servlet 里有没有真的
setAttribute?变量名对不对? forward了没?如果是sendRedirect,request 里的数据会全部丢失。- JSP 路径对不对?
WEB-INF下的页面只能通过req.getRequestDispatcher访问,你要是直接访问了这个路径,Tomcat 会报 404。 - 项目是不是热部署成功了?改了 Java 代码后,在 exploded 模式下 IDEA 会默认热部署,但偶尔你没等重启完成就刷新页面,看到的还是旧数据。
还有一种情况我见了太多次:Servlet 里 set 到 request 的变量名是userList,JSP 里取的却是users。这种错误排查非常费眼神,所以我建议在给 Attribute 命名时,尽量和实体类、数据库字段保持一致的语义,这样在 JSP 里看代码就能猜到 Java 端的代码。
6. 我对 JavaWeb 学习顺序的一点个人体会
环境配好了,项目跑通了,报错也会查了。到这里,你的 JavaWeb 其实已经"入门"了。
以我带新人的经验来看,下一步的正确顺序是:先去理解 Servlet 生命周期(init、service、destroy)和 Filter 过滤器,然后用 Filter 做登录校验,用 Session 做用户状态保持,再学 MVC 思想,最后再进入 Spring 和 Spring Boot。很多人想直接跳过传统 Servlet 学 Spring Boot,结果连"为什么 Spring Boot 不用部署 Tomcat"、"为什么它有自动配置"都搞不清楚。你连 JavaWeb 的请求是咋走的都没见过,就直接上了封装好的框架,遇到问题你根本不知道去哪一层排查。
JavaWeb 这条线,说学起来难,主要是环境和技术栈多,要说学的本质,其实核心就一件事:理解浏览器和服务器之间的一次请求-响应闭环里,每一层分别做了什么。把这条链路吃透,你就拥有了 JavaWeb 最值钱的底层认知。这个认知一旦建立,以后学什么框架都学得快。
下一篇我会接着写 Servlet 生命周期和请求响应对象(Request/Response),争取把你脑子里的"知道"变成"熟练"——毕竟在 JavaWeb 这条路上,动手跑通永远比收藏吃灰有用。
(全文完)