1. 为什么会有这份第三次记录:一次推倒重来的JavaWeb复盘
先说背景。前前后后写JavaWeb相关的内容,陆陆续续也有过几轮笔记,但每次过几个月回看,总觉得上次写的要么太碎,要么漏掉了关键的环境配置细节,要么就是代码示例和实际跑通的结果对不上。于是有了这份“第三次记录”。它不是什么从零开始的教程,更像是我把自己踩过的坑、试过可行的方法、以及最终跑通完整案例的过程重新整理一遍,方便以后自己翻,也方便有同样需求的人直接照做。
第三次记录和之前的笔记最大的区别,是我不再只追求“代码能跑”,而是把注意力放在了三件事上:环境配置的版本匹配、完整项目的结构设计、以及运行时报错时的排查链路。很多JavaWeb初学者其实不缺教程,也不缺案例代码,真正卡住他们的往往是第一步环境搭不好,或者代码照着敲完,一运行就报ClassNotFoundException,然后整个人就懵了。这篇记录就是围绕这些问题展开的,适合正在学JavaWeb、准备做课程设计或毕设、以及想把手里的教学案例真正跑起来的读者。
这一版记录里用到的技术栈是经典的Servlet + JSP + MySQL + Tomcat,没有引入Spring之类的框架。这么选是有意的:先把底层的请求响应、数据库连接、页面跳转这些基本功吃透,再上框架会轻松很多。如果你已经在用框架开发,这篇记录可以帮助你反推底层原理;如果你刚开始学JavaWeb,这套案例完全可以作为起步模板。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境配置是第一个分水岭:IDEA创建JavaWeb项目之前必须想清楚的事
很多人的JavaWeb学习之旅,不是死在代码上,而是死在了环境配置上。IDEA版本、JDK版本、Tomcat版本、MySQL驱动版本,这四个东西任何一个不匹配,都会让你觉得明明代码没问题,可项目就是起不来。我第一次记录时就在Tomcat版本上吃过亏,第二次记录时又被JDK和Tomcat的兼容性坑了一回,所以这次先把环境选型说清楚。
2.1 JDK、Tomcat、IDEA三者之间的版本匹配关系
先说结论:Tomcat 9.x 对应 JDK 8 到 JDK 11 都可以,Tomcat 10.x 之后把包名从 javax.servlet 改成了 jakarta.servlet,这是一个巨大的分水岭。如果你照着网上老教程写代码,import 的都是 javax.servlet.http.HttpServlet,那请老老实实用 Tomcat 9.x;如果你非要用 Tomcat 10.x,那么所有 Servlet 相关的 import 都要改成 jakarta.servlet,否则编译都过不去。
很多教学案例和网上的笔记默认都是 Tomcat 9 + JDK 8 的组合,这也是目前最稳的组合。我这次第三次记录用的就是 JDK 1.8 + Tomcat 9.0.85 + IDEA 2026。IDEA 的版本对 JavaWeb 开发影响不大,它对 Tomcat 的支持比较稳定,真正需要留意的是 IDEA 2026 中创建项目时的模板变化。新版本 IDEA 在新建项目时默认会引导你使用 Maven 或 Gradle,但纯 Web 项目手动创建的方式依然保留着,只是入口藏得比以前深了一点。
有个容易踩的坑是:安装多个 JDK 版本时,IDEA 里的 Project Structure 设置和系统环境变量 JAVA_HOME 不一致。比如系统 JAVA_HOME 指向 JDK 17,而项目 Module 里选的是 JDK 8,Tomcat 运行时会优先读取 JAVA_HOME,然后你发现项目编译没问题,一启动 Tomcat 就报 UnsupportedClassVersionError。我的建议是:开发 JavaWeb 项目时,统一把 JAVA_HOME、IDEA 的 Project SDK、Module SDK 三处全部指到同一个 JDK 8 目录,不要嫌麻烦。
2.2 手动创建Web项目还是用Maven原型
IDEA 里创建 JavaWeb 项目有两条路线:一条是手动创建普通 Java 项目,然后手动添加 Web Facet 和 Artifact;另一条是直接用 Maven 的 maven-archetype-webapp 原型创建。对于学习阶段,我更推荐手动创建一次,原因很直接:它能让你真正理解一个 Web 项目的目录结构和部署逻辑。
手动创建的核心操作是:新建一个空 Java 项目,然后在 Project Structure 里的 Facets 中点加号,选择 Web,把 src/main/webapp 目录(或者你自定义的 web 目录)标记为 Web 资源目录;接着在 Artifacts 中新建 Web Application Exploded 或 Archive,把项目打包成可部署的结构。这一步很多人漏掉,结果就是 IDEA 里 Tomcat 配置能看到,但运行时报错说找不到 Artifact。
Maven 路线的好处是省事,webapp 目录结构自动生成,依赖也只需要在 pom.xml 里写几行。但坏处是如果镜像源没有配置好,下载依赖会卡半天;而且一旦依赖冲突,排查起来比手动管理 jar 包更麻烦。我的建议是:第一次做 JavaWeb 项目,走手动流程;后面项目复杂了需要管理大量依赖,再切 Maven。
2.3 Facet 和 Artifact:这两个概念不搞懂,部署必出问题
Facet 和 Artifact 是 IDEA 里两个特别容易混淆的概念。简单类比:Facet 是“项目能力的声明”,告诉 IDEA 这个模块具备了 Web 能力,能识别 JSP/Servlet;Artifact 是“打包与部署的蓝图”,决定了最终以什么形态输出到 Tomcat。如果你只加了 Facet 没配 Artifact,代码能编译,但 Tomcat 运行列表里会找不到可部署的包。
配置 Artifact 时,最常见的错误是 Output Layout 里缺少依赖。手动创建的项目默认不会把 MySQL 驱动等 jar 包自动带进部署包,你需要把 External Libraries 里的依赖右键加入 Artifact。如果不加,代码编译期不报错,运行时才报 ClassNotFoundException: com.mysql.cj.jdbc.Driver。这个错误我见过太多次了,后面会专门写一节排查过程。
涉及版本匹配的具体建议,我整理成一张表方便对照:
| 组合方案 | JDK 版本 | Tomcat 版本 | Servlet 包名 | 适用场景 |
|---|---|---|---|---|
| 方案A(最稳) | JDK 8 | Tomcat 9.x | javax.servlet | 教学案例、课程设计、老教程 |
| 方案B(新版) | JDK 11+ | Tomcat 10.1.x | jakarta.servlet | 新项目、想体验新规范 |
| 方案C(不推荐) | JDK 17 | Tomcat 8.5 | javax.servlet | 容易遇到版本兼容警告 |
如果你跟着网上的 JavaWeb 教学案例走,遇到代码里 import 的是 javax.servlet 开头,就按方案A来配环境,这是目前兼容性最好的组合。
3. 把零散的教学案例拼成一个完整项目:数据库设计和后端分层思路
环境配好了,接下来面临的问题是:网上能搜到的 JavaWeb 完整案例很多,但大多要么代码不全,要么数据库脚本缺失。这次第三次记录,我不打算贴一堆碎片代码,而是构建一个能跑通的完整小系统:用户登录 + 数据列表展示 + 新增记录。功能不多,但覆盖了 JavaWeb 开发的主干线。
3.1 数据表设计:从简单教学案例到能落地的结构
很多教学案例为了演示方便,会设计一张特别简单的表,比如用户表只有 id、username、password 三个字段。跑通没问题,但离真实项目差距很大。这次的案例我设计了两张表:用户表 t_user 和记录表 t_record。用户表负责登录认证,记录表负责业务数据展示。
sql复制CREATE DATABASE javaweb_demo DEFAULT CHARACTER SET utf8mb4;
CREATE TABLE t_user (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(64) NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE t_record (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
title VARCHAR(100) NOT NULL,
content TEXT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_record_user FOREIGN KEY (user_id) REFERENCES t_user(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这里有两个细节值得解释一下。第一,为什么用 utf8mb4 而不是 utf8?因为 utf8 在 MySQL 里最多存 3 字节,像 emoji 这样的字符会报错,而 utf8mb4 是完整的 UTF-8 编码,兼容性更好,这也是现代 JavaWeb 项目的默认选择。第二,为什么密码字段用 VARCHAR(64)?因为常见做法是存 MD5 或 SHA-256 加密后的十六进制字符串,长度正好是 32 或 64。明文存储密码是不安全的行为,哪怕是学习项目,也要培养这个习惯。
插入几条测试数据时,用户密码可以用 MySQL 内置函数 MD5('123456') 生成,这样 Java 端不需要额外写加密逻辑就能对接测试。列表页联合查询 t_user 和 t_record,可以顺便练习一下 JOIN 的写法。
3.2 Servlet 分层:为什么不能把所有逻辑都堆在 doGet 里
网上很多 JavaWeb 教学案例,会把数据库查询、业务判断、页面跳转全部写在 Servlet 的 doGet 或 doPost 方法里。代码量少的时候看着挺直观,但一旦功能多起来,一个 Servlet 动辄几百行,后续维护就是灾难。这第三次记录里,我按了三层结构来写:Servlet 层只负责接收请求和返回响应,Service 层处理业务逻辑,DAO 层操作数据库。
DAO 层用一个 BaseDAO 封装通用的增删改查,具体类继承它。Service 层把用户登录校验、记录查询这些业务方法独立出来。Servlet 层尽量精简,像用户的登录逻辑,一个 LoginServlet 只做四件事:接收参数、调用 Service、根据结果跳转、处理异常。
这样做的好处是:如果以后要把项目升级成 Spring MVC 或 Spring Boot,Controller 层对应原来的 Servlet 层,Service 层和 DAO 层几乎可以原封不动迁移过去。这也是我在第三次记录里特意强调的——你在学习阶段写的分层结构,其实就是框架思想的雏形。
3.3 数据库连接与连接池:DriverManager 直连只能算入门
最基础的数据库连接方式是 DriverManager.getConnection(),每个请求都创建一个连接,用完关闭。这种方式在教学案例里没问题,但放到真实场景下,频繁创建和销毁连接的开销很大。这一版记录里,我依然用 DriverManager 来演示核心原理,但代码里已经预留了换成连接池的接口。
java复制public class DBUtil {
private static final String URL = "jdbc:mysql://localhost:3306/javaweb_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4";
private static final String USER = "root";
private static final String PASSWORD = "123456";
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);
}
}
URL 上那串参数不是随便写的。serverTimezone=Asia/Shanghai 是因为 MySQL 8.x 驱动对时区敏感,不设置会报 The server time zone value '?й?ʱ?' is unrecognized 之类的错误;characterEncoding=utf8mb4 是为了避免中文乱码;useSSL=false 是禁用 SSL 警告。
还要注意驱动类的包名:MySQL 5.x 驱动是 com.mysql.jdbc.Driver,MySQL 8.x 驱动是 com.mysql.cj.jdbc.Driver。如果你下载的是 mysql-connector-java-8.0.x.jar,却写旧包名,运行时会直接报 ClassNotFoundException。这个坑在无数次求助帖里出现,值得单独拿出来说一遍。
4. 完整案例拆解:登录功能与列表查询的实现路径
这次记录的核心案例是一个带登录验证的记录管理系统,麻雀虽小五脏俱全。我按照实际开发顺序来拆解,先做登录,再做登录后的记录列表,最后做新增记录的表单提交。每部分都会说明关键代码的位置和为什么这么写。
4.1 项目目录结构:从源头避免“找不到类”的尴尬
手动创建的项目目录结构建议按下面这么安排:
text复制javaweb-demo/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/demo/
│ │ ├── servlet/
│ │ │ ├── LoginServlet.java
│ │ │ ├── ListRecordServlet.java
│ │ │ └── AddRecordServlet.java
│ │ ├── service/
│ │ │ ├── UserService.java
│ │ │ └── RecordService.java
│ │ ├── dao/
│ │ │ ├── BaseDAO.java
│ │ │ ├── UserDAO.java
│ │ │ └── RecordDAO.java
│ │ ├── entity/
│ │ │ ├── User.java
│ │ │ └── Record.java
│ │ └── util/
│ │ └── DBUtil.java
│ └── webapp/
│ ├── WEB-INF/
│ │ └── web.xml
│ ├── login.jsp
│ ├── list.jsp
│ ├── add.jsp
│ └── index.jsp
└── lib/
└── mysql-connector-java-8.0.30.jar
注意几个关键点:WEB-INF 目录下的文件不能通过浏览器直接访问,因此登录页面 login.jsp 放在 webapp 根目录,而需要权限控制的后台页面可以放 WEB-INF 下面,由 Servlet 转发访问。lib 目录存放外部 jar 包,如果在 IDEA 里配置 Artifact,需要把它加入部署包。
4.2 登录实现:Session 处理与请求转发/重定向的选择
登录的流程再普通不过:前端表单提交用户名密码,LoginServlet 接收后查询数据库,成功则将用户信息放入 Session,重定向到列表页;失败则回登录页并显示错误提示。
java复制@WebServlet("/login")
public class LoginServlet extends HttpServlet {
private UserService userService = new UserService();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
String username = request.getParameter("username");
String password = request.getParameter("password");
User user = userService.login(username, password);
if (user != null) {
HttpSession session = request.getSession();
session.setAttribute("loginUser", user);
response.sendRedirect(request.getContextPath() + "/list");
} else {
request.setAttribute("errorMsg", "用户名或密码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
}
这里有个经典问题:什么时候用 sendRedirect,什么时候用 forward?简单记为:登录成功后用重定向,因为要避免用户刷新页面时重复提交表单;登录失败后用转发,因为要携带错误信息给 JSP 页面展示,并且转发过程中 Request 作用域内的数据不会丢。
加载该 Servlet 时使用了 @WebServlet 注解,这要求项目不低于 Servlet 3.0 规范。Tomcat 9 默认支持,所以不需要在 web.xml 里再手动注册映射。如果你想兼容老版本的 Tomcat,那就得改成在 web.xml 里配置 <servlet> 和 <servlet-mapping>。现在的教学案例普遍用注解,因为代码更简洁。
4.3 记录列表查询:JOIN 联表查询在 DAO 中的写法
列表页需要展示每条记录的标题、内容、创建时间和所属用户名。数据来自 t_record 和 t_user 两张表,SQL 写法是:
sql复制SELECT r.id, r.title, r.content, r.create_time, u.username
FROM t_record r
JOIN t_user u ON r.user_id = u.id
ORDER BY r.create_time DESC;
DAO 层执行查询后,把每一行封装成 Record 对象。这里注意封装时的习惯:Record 实体类里除了自己的属性外,还需要一个额外的 username 字段来接收联表查询的结果。虽然这个字段在 t_record 表里不存在,但在展示层它是必需的。这不算破坏实体定义,而是 VO(视图对象)的雏形。
封装 ResultSet 时我习惯这样写:
java复制List<Record> list = new ArrayList<>();
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
Record r = new Record();
r.setId(rs.getInt("id"));
r.setTitle(rs.getString("title"));
r.setContent(rs.getString("content"));
r.setCreateTime(rs.getTimestamp("create_time"));
r.setUsername(rs.getString("username"));
list.add(r);
}
}
JDK 7 引入的 try-with-resources 语法会自动关闭资源,比手动在 finally 里关闭更安全。这个细节很多人一开始没注意,等遇到数据库连接泄漏、启动几百次后 Tomcat 崩了才会回头找原因。第三次记录里我明确要求自己所有 DAO 层代码都采用这种写法。
4.4 JSP 页面与后端交互:EL 表达式和 JSTL 的应用
JSP 页面中尽量不要再写 Java 脚本片段(<% ... %>),这是老式教学案例常用但不利于维护的写法。这一版记录里的列表页使用 EL 表达式 + JSTL 标签来渲染:
jsp复制<c:forEach items="${recordList}" var="r">
<tr>
<td>${r.id}</td>
<td>${r.title}</td>
<td>${r.content}</td>
<td>${r.username}</td>
<td><fmt:formatDate value="${r.createTime}" pattern="yyyy-MM-dd HH:mm:ss" /></td>
</tr>
</c:forEach>
如果页面出现 $ {recordList} 原样输出,通常意味着两个问题:一是没有在 JSP 页面头部引入 JSTL 标签库的 taglib 指令,二是缺少 jstl.jar 和 standard.jar 依赖。前者是代码问题,后者是依赖问题,报错时留意控制台的提示就能区分。
5. 部署运行实录:IDEA配置Tomcat的完整链路与两次实战排查
案例代码写完了,接下来就是部署运行。这是 JavaWeb 开发中最容易出问题的环节,也是大量求助帖的集中区。我在第三次记录中把这个环节写成了单独的章节,因为环境配置和排错经验比代码本身更值钱。
5.1 在IDEA 2026中配置Tomcat的最小可用步骤
按下面的顺序操作,基本能避免九成以上的部署问题:
- 点击工具栏的 Add Configuration,选择 Tomcat Server > Local。
- 在 Application server 中选择本机 Tomcat 9 的安装目录。
- 切到 Deployment 标签页,点加号选 Artifact。
- 把 Application context 设置成
/javaweb-demo或直接/。 - 启动前检查环境的 JAVA_HOME 与项目 SDK 是否一致。
第4步的 Application context 其实就是一个项目的访问前缀。如果设置成 /javaweb-demo,浏览器访问路径就是 http://localhost:8080/javaweb-demo/login.jsp;如果设置成 /,就能直接以根路径访问。很多头像教程没讲清这个,导致明明部署成功了,但打开网址是 404,然后开始怀疑人生。
启动时如果 IDEA 提示端口被占用,最可能是本地 8080 端口被之前残留的 Tomcat 实例占用了。直接找到那个进程结束掉,比在 IDEA 里反复改端口更靠谱。
5.2 排查实录一:ClassNotFoundException: com.mysql.cj.jdbc.Driver
这个报错信息我帮别人诊断过太多次。现象是项目启动正常,但打开页面一触发数据库操作就报这个错。根本原因通常是 MySQL 驱动 jar 包没有被部署到 Tomcat 的 WEB-INF/lib 目录下。
在 IDEA 里,单纯把 jar 包放到项目的 lib 目录是不够的,还需要在 Project Structure > Artifacts 的 Output Layout 里确认这个 jar 是否出现在 WEB-INF/lib 节点下。如果没有,右键点击 Output Layout 里的空目录区域,选择 Add Copy of > Module 依赖或直接添加 jar 文件。这一步操作完,重新构建 Artifact 再启动,问题就解决了。
我遇到过有人明明把 jar 放进去了还是报错,后来发现是因为他把 jar 放在了 JDK 的扩展目录里,而并不在项目的部署清单中。排查时建议先看 Tomcat 启动日志,或者 直接解压 Tomcat 工作目录下生成的 war/exploded 目录,看 WEB-INF/lib 里到底有没有这个 jar。
5.3 排查实录二:页面正常但访问Servlet报404
还有一种特别容易蒙圈的 404:JSP 页面能打开,但点击按钮跳到 Servlet 时 404。这种情况通常是注解路径不对。检查点有三个:
@WebServlet注解里的 URL 路径是否以/开头,比如/login,而不是login。- 前端表单或链接的 action 是否少了项目上下文路径,直接用
response.sendRedirect(request.getContextPath() + "/list")可以规避这个问题。 - 是否使用了
@WebServlet但项目被当成 Servlet 2.5 程序运行。如果web.xml里没有把版本声明为 3.0+,某些老容器不会扫描注解。
第三种情况比较隐蔽。Tomcat 9 默认支持注解扫描,但如果你的 web.xml 头文件写法还是旧版 Servlet 2.3 的格式,Tomcat 会以兼容模式运行,注解自动失效。解决办法是保证 web.xml 的根标签版本不低于 3.0,甚至直接删掉 web.xml,让容器按默认的 4.0 规范运行。
5.4 中文乱码问题:从请求参数到页面响应的完整链路
中文乱码是 JavaWeb 项目里最顽固的问题之一。这次记录我把自己的处理方式写成一个固定套路:
- 数据库连接 URL 加
characterEncoding=utf8mb4。 - 数据库建库使用
utf8mb4字符集。 - 所有涉及请求参数读取的 Servlet 开头加
request.setCharacterEncoding("UTF-8")。 - 响应输出设置
response.setCharacterEncoding("UTF-8"),JSP 页面头部确认contentType="text/html; charset=UTF-8"。 - Tomcat 的
server.xml中,HTTP Connector 可以加一行URIEncoding="UTF-8"。
如果 GET 请求参数乱码,问题多半出在 Tomcat 的 URI 编码上,因为 Tomcat 8 之后默认才是 UTF-8,老版本默认是 ISO-8859-1。如果 POST 请求乱码,多半是没设置 setCharacterEncoding,而且必须放在读取第一个参数之前。
乱码问题的排查顺序建议是:先看页面显示是否乱码,再看数据库存储是否乱码,最后看请求参数是否乱码。确定好是哪一段坏掉,处理起来就有方向了。
6. 从第三次记录里提炼出来的几条实战经验
写到这里,我想把这次记录中最有价值的经验沉淀下来。它们不是从任何教程里抄来的,而是在反复搭建、运行、报错、修复的过程中逐渐形成的习惯。
6.1 学习项目也要有“留痕”意识
我之前两次记录的教训是:只记成功路径,不记失败过程。结果第二次搭环境时,同一个 Tomcat 版本坑又踩了一遍。这次我花时间记录了每一步操作、报错信息、解决方法的组合。建议你也维护一份自己的排错笔记,不需要很正式,哪怕是备忘录里的一条“报错 xxx 是因为 yyy”,下次遇到就能快速定位。
6.2 代码注释里留下“为什么”
我看过很多教学案例的代码,注释写的大多是“查询数据库”或“获取参数”这种废话。这第三次记录里,我给关键代码加的是原因型注释,比如“这里必须用重定向,否则用户刷新会重复提交表单”。对自己写代码有要求,以后回看时能快速理解当初的决策逻辑。对看笔记的读者来说,这种注释才是真正有帮助的。
6.3 后端开发的第一课:不要相信用户输入
虽然这只是一个学习项目,但我在登录逻辑里还是加了基本的参数非空校验和密码加密存储。原因很简单:如果你从一开始就习惯写裸 SQL 和明文密码,后面进入项目实战时会非常痛苦。学习阶段多走一步,正式工作就少挨一次批评。
最后说点实在的:JavaWeb 这个技术栈在当下看起来有点“传统”,但它是整个 Java 后端知识体系的地基。框架看得再多,如果 servlet 请求周期、 session 机制、数据库连接这些底子不扎实,用框架时遇到问题依然无从下手。我这份第三次记录,希望能帮你少走一些弯路,把地基打牢一点。如果你按照这套流程把案例跑通,再把每个报错都亲手修一遍,相信我,下次换个框架、换个数据库,你也能从容应对。
