☰
JavaWeb连接MySQL完整案例:从建表到增删改查全链路
2026/10/10 3:33:23 网站建设 项目流程

1. JavaWeb_07 学到这里,该让数据"落地"了

如果你是从 Servlet、JSP、请求响应一路跟上来的,走到 JavaWeb_07 基本就是一个分水岭:前几篇做的东西,数据都是活在内存里的——用户注册完,刷新一下服务,数据没了;输入框里的内容,只能通过request.getParameter()拿到一次,然后就没有然后了。这个阶段再继续往后学,最自然、也最绕不开的一件事,就是接数据库。

JavaWeb 项目连接 MySQL,几乎是每个学 JavaWeb 的人都要过的一关。不管是黑马的笔记数据、学校课设里的用户管理系统,还是你自己想做一个能真正跑起来的完整案例,最终都要落到"把数据存进 MySQL,再通过页面查出来展示"这条链路上。热词里能刷到"javaweb连接mysql数据库""idea运行javaweb项目配置""javaweb项目完整案例mysql",说明大家卡住的位置高度集中:不是代码本身难,而是"环境怎么配、依赖怎么加、Tomcat 怎么部署、数据库连接串怎么写"这些杂事没理顺。

所以这篇 JavaWeb_07 我打算换一个讲法——不单独拆 JDBC 语法,也不单独讲 Servlet 生命周期,而是直接以一个"用户信息管理"的完整案例为主线,把数据库设计、IDEA 配置、JDBC 连接、三层架构、增删改查页面全串起来。你跟着做完这个案例,相当于一次性打通了"浏览器 → Servlet → Service → DAO → MySQL → 再返回页面渲染"的整条链路。适合人群很明确:已经会写基础 Servlet/JSP、但对数据库接入还比较模糊的人,以及准备做课设或毕设、需要一个完整项目参考的人。

这篇里所有代码和配置我都按当前主流环境给:IDEA 2023 + Tomcat 9 + MySQL 8.0 + Druid 连接池,完整体验下来大概需要两三个小时。过程中我会把容易踩的坑单独拿出来讲,这些坑你在搜索引擎里翻好几页都不一定找得到。

2. 建表先行的设计取舍:用户管理的数据模型

很多新手拿到项目需求,第一反应是先在 IDEA 里新建各种类,写一堆代码,然后再想数据库怎么弄。我的建议正好反过来:先建库建表,再写代码。

原因很简单——JavaWeb 项目的核心是数据流转,表结构定了,DAO 层的方法签名、Service 层的业务逻辑、JSP 页面上要显示哪些字段,全都跟着定下来了。表结构一改,代码全部要跟着动,所以建表这一步值得多花十分钟想清楚。

2.1 字段设计:别一口气塞十几个列

以一个最常见的"用户管理"模块为例,一个用户表至少要有这些字段:用户名、密码、真实姓名、邮箱、手机号、创建时间。再多就不建议了,比如"性别、年龄、头像、地址、个性签名"这些,不是不能加,而是第一版加越多,后面出错的概率越大。你是在学项目,不是在做生产系统,先保证链路跑通,再考虑字段丰富度。

建表 SQL 我建议直接用下面这个,注意几个细节:

CREATE DATABASE IF NOT EXISTS javaweb_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE javaweb_demo; DROP TABLE IF EXISTS t_user; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名,登录时用', password VARCHAR(64) NOT NULL COMMENT '密码(建议存加密摘要)', real_name VARCHAR(50) DEFAULT '' COMMENT '真实姓名', email VARCHAR(100) DEFAULT '' COMMENT '邮箱', phone VARCHAR(20) DEFAULT '' COMMENT '手机号', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

几个设计上的考虑,我说一下理由:

  • 字符集用 utf8mb4,不是 utf8。MySQL 的 utf8 实际上是 utf8mb3,存不了 emoji 和部分生僻字,一旦用户的昵称或备注里带了表情符号,插入时直接报Incorrect string value。这个问题非常隐蔽,你可能查半天代码发现没问题,最后才发现是字符集的事。
  • 用户名加 UNIQUE 约束,保证同一用户名不能重复注册。这个约束会让后续的"注册去重"逻辑变得简单——在 Service 层先查一次,再依赖数据库兜底。
  • 密码字段长度设 64,别设成 20。原因后面讲注册逻辑的时候会细说,和加密后的摘要长度有关。
  • id 用 AUTO_INCREMENT 自增主键,InnoDB 引擎。这是 MySQL 最常规、最好用的组合,不要在这个阶段去尝试 UUID 做主键,性能和写法都复杂,没必要。

2.2 初始数据:给列表页留点东西

表建好之后,顺手插入两条初始数据,方便后面测试列表页和管理员登录:

INSERT INTO t_user (username, password, real_name, email, phone) VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '系统管理员', 'admin@example.com', '13800000000'), ('zhangsan', 'e10adc3949ba59abbe56e057f20f883e', '张三', 'zhangsan@example.com', '13900000000');

这里密码字段存的是123456的 MD5 值。明文密码在生产系统里是绝对不能存的,但在这个学习阶段,至少做到"页面不直接显示明文、数据库不直接存明文",就算迈出第一步了。等后面学了 Shiro、Spring Security 或者 BCrypt,再换更安全的算法,思路都是一样的。

3. IDEA 里跑通 JavaWeb 连接 MySQL 的配置三件套

很多人在这一步就被卡住了:代码明明是从教程里抄的,为什么一运行就是找不到驱动、连不上数据库、Tomcat 启动报错?我可以负责任地说,90% 的 JavaWeb 连接数据库失败,不是代码问题,是项目配置问题。

3.1 Maven 依赖的两个关键选择

如果项目是用 Maven 管理的(建议你现在就开始用,别再手动往 WEB-INF/lib 里扔 jar 包了),pom.xml里最核心的依赖就两个:

<!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- Druid 连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency>

版本选择是个容易被忽视的坑。MySQL 8.x 的驱动包,坐标是mysql:mysql-connector-java,驱动类名是com.mysql.cj.jdbc.Driver(注意中间多了个 cj)。如果你用 MySQL 5.x 时代的老教程,驱动类写的是com.mysql.jdbc.Driver,放到 MySQL 8.0 上直接报ClassNotFoundException,或者报你完全看不懂的Loading class 'com.mysql.jdbc.Driver'警告。所以拿到一篇教程,先看对方用的 MySQL 版本和你是否一致,再决定要不要抄配置。

3.2 集成 Tomcat 时别忽略的两个细节

IDEA 里给项目配置 Tomcat 一般大家都熟:Run → Edit Configurations → 点 + 号 → 选 Tomcat Server Local。但有两个细节,90% 的新手都会踩:

第一,Deployment 标签页里,要把项目以war exploded的方式部署,而不是war。war exploded是"解压版"的 war 包,它直接以项目输出的目录结构运行,改动 Java 代码或 JSP 后,在 Debug 模式下可以通过热重载快速生效;而war方式每次改动都要重新打包,调试效率低到让人崩溃。选哪个在 Deployment 窗口里有下拉项,别忽略它。

第二,Application context 要设置成/,或者一个简短路径,比如/javaweb07。这个路径决定了你访问项目的 URL 前缀。如果设成/,那访问地址就是http://localhost:8080/userList,清爽直观。如果忘了设置,默认会是/项目名_war_exploded/,很长一串,而且后续页面跳转时容易因为路径不对出现 404。

完整配置好后,Tomcat 控制台能正常启动,浏览器访问http://localhost:8080/能看到你的页面,才说明基础环境没问题。这一步别急,配置一遍踩过的坑,比看十遍配置说明都记得牢。

3.3 项目运行时可能出现的经典报错

配置阶段最常见的报错,我直接列个表,你对照排查:

报错信息根本原因解决方案
ClassNotFoundException: com.mysql.jdbc.Driver驱动类名写错(MySQL 8 用 cj 版本)改为com.mysql.cj.jdbc.Driver
No suitable driver found驱动 jar 没打进来,或 URL 格式不对确认依赖已引入,URL 以jdbc:mysql://开头
Communications link failure数据库服务没启动,或端口不对检查 MySQL 是否运行,确认 3306 端口
Access denied for user用户名密码错误,或权限不足检查 db.properties 里的账号配置
Artifact 部署后 404Application context 路径不对访问路径与部署名称保持一致

这些错误几乎每个 JavaWeb 学习者都至少遇到过一次,不用慌,按表排查基本几分钟就能定位。

4. JDBC 连接层:从 DriverManager 到 Druid 连接池

配置做好之后,就该写真正连接数据库的代码了。JavaWeb 连接 MySQL,底层手段还是 JDBC,但实际项目里不会裸写DriverManager.getConnection()到处建立连接,那样性能太差。不过在不理解原理之前直接上连接池,又容易一头雾水。所以这一节我们先从裸 JDBC 讲清楚,再平滑过渡到连接池。

4.1 JDBC URL 里的三个隐藏参数:时区、编码和安全

连接数据库,核心就是一条连接字符串。以 MySQL 8.0 为例:

jdbc:mysql://localhost:3306/javaweb_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8&rewriteBatchedStatements=true
  • useSSL=false:本地开发用不到 SSL 加密,加上这个可以避免 MySQL 8 默认启用 SSL 导致的无谓警告或握手失败。
  • serverTimezone=Asia/Shanghai:这个必须有。MySQL 8 对时区校验非常严格,如果不指定时区,连接时会报The server time zone value ... is unrecognized或者Error while creating session。国内统一用Asia/Shanghai就行。
  • characterEncoding=utf8:保证 Java 和 MySQL 之间传输的数据按 UTF-8 编码,配合数据库的 utf8mb4 字符集,才能彻底避免中文乱码。

你可能觉得这些都是细节,少写一个又能怎样。我可以明确告诉你:少了serverTimezone,连接直接失败;少了characterEncoding,中文数据进库之后再查出来就变成???。这俩都是最典型的"环境问题伪装成代码问题"。

4.2 先写一个最基础的 JDBC 工具类

下面这个工具类很短,但是把注册驱动、拿连接、关资源都封装好了,适合理解原理。关键代码就这几行:

package com.demo.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class JdbcUtil { private static final String URL = "jdbc:mysql://localhost:3306/javaweb_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USERNAME = "root"; private static final String PASSWORD = "你自己的密码"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("MySQL驱动加载失败,请检查依赖"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

注意Class.forName()放在静态代码块里,保证类加载时只注册一次驱动。这个工具类的缺点也很明显:每次取连接都重新走 TCP 握手,高并发下性能极差,而且资源管理完全靠手动 close,一旦在业务代码里漏写,数据库连接就会耗尽。

4.3 换成 Druid 连接池:配置比想象中简单

学习项目可以直接上连接池,这不是过度设计,而是让你从一开始就养成好习惯。Druid 的配置非常简单,先建一个druid.properties文件放在src/main/resources下:

driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/javaweb_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 username=root password=你自己的密码 initialSize=5 maxActive=10 maxWait=3000

然后写一个工具类,通过Properties加载配置文件并创建连接池:

package com.demo.util; import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; public class JdbcUtil { private static DataSource dataSource; static { try (InputStream in = JdbcUtil.class.getClassLoader().getResourceAsStream("druid.properties")) { Properties props = new Properties(); props.load(in); dataSource = DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError("连接池初始化失败:" + e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 与上面一致 } }

用连接池之后,getConnection()拿到的不再是新建的物理连接,而是从池子里借出来的一条逻辑连接;调用conn.close()也不是真的关闭,而是把连接还回池子。这就是为什么你用连接池时更要养成"用完就 close"的习惯——操作的是复用资源,不还回去,池子很快会空,后面的人就排队等着。

5. 一个完整的增删改查项目:从 DAO 到 JSP 页面

环境通了,连接层有了,接下来就是把数据从数据库里"搬运"到网页上。这一步我强烈建议按照经典三层结构来做:DAO 层只管访问数据库,Service 层管业务逻辑,Servlet 层接收请求调用 Service,JSP 负责展示结果。这个结构学过,以后接触 Spring 的时候,你会发现 Srping 的三层架构就是在此基础上长出来的,现在把地基打好,后面会顺很多。

5.1 分层到底在分什么

很多人在学习时会有疑惑:代码量看起来变多了,一个简单的增删改查而已,为什么要拆这么多层?

我用一个场景来解释。假设用户注册时要求"用户名不能重复",如果所有逻辑都堆在一个 Servlet 里:先查数据库看看有没有同名用户,没有就插入,有就返回错误。功能是能跑通,但第二个需求来了——"注册时密码要加密存储",你又得改这个 Servlet。等做到第三个功能"用户列表要显示每个用户有多少条订单",这个 Servlet 就会膨胀到几百行,改一个功能影响一片。

分层之后,职责就清晰了:

  • DAO 层:只回答"数据库能不能做这个操作",比如insert(User user)、findByUsername(String username)。
  • Service 层:只回答"这个业务允不允许这样做",比如注册时先调用findByUsername判断用户是否已存在。
  • Servlet 层:只负责"把请求里的参数拿出来、组装好、调用 Service、根据结果决定跳哪个页面"。
  • JSP:只负责"把数据显示成 HTML"。

一旦养成分层的习惯,后期加功能、修 bug、换数据库,影响范围都被控制在某几层内,而不是全局推倒重来。

5.2 DAO 层:最核心的五个方法

以用户表为例,一个最小的 DAO 接口应该有下面五个方法。先写接口,再写实现类,这是一种"面向接口编程"的意识,一开始不要求完全理解,照着做就行:

package com.demo.dao; import com.demo.entity.User; import java.util.List; public interface UserDao { List<User> findAll(); User findById(Integer id); User findByUsername(String username); int insert(User user); int update(User user); int deleteById(Integer id); }

实现类里面就是标准的 JDBC 操作,以findAll()为例:

@Override public List<User> findAll() { List<User> list = new ArrayList<>(); String sql = "SELECT id, username, real_name, email, phone, create_time FROM t_user ORDER BY id DESC"; try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setRealName(rs.getString("real_name")); user.setEmail(rs.getString("email")); user.setPhone(rs.getString("phone")); user.setCreateTime(rs.getTimestamp("create_time")); list.add(user); } } catch (SQLException e) { throw new RuntimeException("查询用户列表失败", e); } return list; }

注意我用的是PreparedStatement,不是Statement。前者可以预编译 SQL,并且通过占位符传参,能有效防止 SQL 注入。新手很容易在这个地方图省事直接用字符串拼接,比如"SELECT * FROM t_user WHERE username='" + username + "'",一旦用户在用户名里输入' OR '1'='1,你的数据就全暴露了。用 PreparedStatement 不是为了炫技,是保命。

insert和update的区别在于,前者拿数据库生成的主键,后者靠传入 id 定位一条记录。这里有个细节:insert之后如果需要回显新用户的 id,可以通过Statement.RETURN_GENERATED_KEYS来取,适合扩展注册后自动登录的场景:

PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.setString(1, user.getUsername()); ps.executeUpdate(); ResultSet keys = ps.getGeneratedKeys(); if (keys.next()) { user.setId(keys.getInt(1)); }

5.3 Service 层:业务校验放这里

DAO 层写完,先别急着写 Servlet。中间加一层 Service,哪怕很简单,也有实际价值。

注册用户的核心逻辑是这样的:先检查用户名是否已存在,如果不存在,再对密码做 MD5 加密,然后插入数据库。这段逻辑放 Servlet 里也能实现,但放 Service 里才能被多个入口复用——比如以后加了"管理员后台手动创建用户"功能,它同样需要这套校验逻辑。

public class UserService { private UserDao userDao = new UserDaoImpl(); public boolean register(User user) { if (user.getUsername() == null || user.getUsername().trim().isEmpty()) { throw new IllegalArgumentException("用户名不能为空"); } if (userDao.findByUsername(user.getUsername()) != null) { return false; // 用户名已存在 } // 对密码做摘要存储,避免明文入库 String md5Pwd = MD5Util.md5(user.getPassword()); user.setPassword(md5Pwd); return userDao.insert(user) > 0; } public User login(String username, String password) { User user = userDao.findByUsername(username); if (user != null && user.getPassword().equals(MD5Util.md5(password))) { return user; } return null; } }

MD5 在这个阶段作为"练手级加密"够用。真要上线生产环境,建议换 BCrypt 或者至少加盐处理。这里提到 MD5,是为了让你理解"数据库不存明文"的实现思路,别在真实项目里直接用 MD5,它的碰撞风险是已经被攻破的。

5.4 Servlet 路由设计:一个 Servlet 处理一个模块

Servlet 的写法上,我推荐一个模块用一个 Servlet,用action参数区分不同操作。比如用户管理模块就叫UserServlet,通过?action=list、?action=add、?action=edit、?action=delete分别处理增删改查。这样比"一个功能一个 Servlet"好管理得多,也贴合 JavaWeb 后期框架的路由风格。

核心代码如下:

@WebServlet("/user/*") public class UserServlet extends HttpServlet { private UserService userService = new UserService(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if ("list".equals(action)) { List<User> userList = userService.findAll(); req.setAttribute("userList", userList); req.getRequestDispatcher("/jsp/userList.jsp").forward(req, resp); } else if ("add".equals(action)) { User user = new User(); user.setUsername(req.getParameter("username")); user.setPassword(req.getParameter("password")); user.setRealName(req.getParameter("realName")); user.setEmail(req.getParameter("email")); user.setPhone(req.getParameter("phone")); boolean success = userService.register(user); if (success) { resp.sendRedirect(req.getContextPath() + "/user?action=list"); } else { req.setAttribute("error", "用户名已存在"); req.getRequestDispatcher("/jsp/register.jsp").forward(req, resp); } } else if ("delete".equals(action)) { int id = Integer.parseInt(req.getParameter("id")); userService.deleteById(id); resp.sendRedirect(req.getContextPath() + "/user?action=list"); } // edit, view 按同样模式扩展 } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); doGet(req, resp); } }

这里两个细节值得说道:

  • req.setCharacterEncoding("UTF-8")必须在doPost里最先调用,否则 POST 请求带过来的中文会乱码。这句话要放在取第一个参数之前,因为参数是一进入请求就已经被解码了的,你事后设置就晚了。
  • 新增和删除成功之后,用sendRedirect()做重定向;失败时,用forward()把请求转发回原始页面。区别是:redirect 是浏览器重新发起一次新请求,地址栏会变,能避免表单重复提交;forward 是服务端内部跳转,地址栏不变,适合携带错误信息返回表单页。

5.5 JSP 页面:JSTL 和 EL 让代码更干净

到了 JSP 这里,一个最常见的坏习惯是:在页面里写一堆<% for (...) { %>的 Java 脚本片段。不是说不能运行,而是页面会被 Java 代码和 HTML 混在一起,阅读性极差。专业的做法是引入 JSTL 标签库 + EL 表达式,页面里只负责循环和展示。

在pom.xml加入 JSTL 依赖(Tomcat 9 建议用 JSTL 1.2):

<dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency>

用户列表页的核心结构大概是这样:

<%@ 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> <table border="1" cellspacing="0" cellpadding="6"> <tr> <th>ID</th> <th>用户名</th> <th>真实姓名</th> <th>邮箱</th> <th>手机号</th> <th>操作</th> </tr> <c:forEach items="${userList}" var="u"> <tr> <td>${u.id}</td> <td>${u.username}</td> <td>${u.realName}</td> <td>${u.email}</td> <td>${u.phone}</td> <td> <a href="${pageContext.request.contextPath}/user?action=edit&id=${u.id}">编辑</a> <a href="${pageContext.request.contextPath}/user?action=delete&id=${u.id}" onclick="return confirm('确定删除该用户吗?')">删除</a> </td> </tr> </c:forEach> </table> </body> </html>

注意页面顶部contentType="text/html;charset=UTF-8",这个是 JSP 页面的输出编码,漏掉它,浏览器显示中文就是乱码。整个 JavaWeb 链路里涉及编码的地方很多,我后面集中讲。

6. 运行链路与"经典坑":我实际跑通时踩过的摩擦点

代码写到这里,理论上已经可以跑起来了。但真实的开发过程从来不会这么顺利。下面这几个坑,是我自己在做这类项目时实际遇到过的,按出现的概率从高到低排,你照着先排查一遍,能省下一个小时起步。

6.1 时区错误:最神秘的启动失败

现象:Tomcat 启动正常,页面也正常出来,点"注册"按钮后控制台抛异常,信息很长,最前面是SQLException,中间夹杂一句The server time zone value '???ú±ê׼ʱ??' is unrecognized。

原因:MySQL 8 对连接时区要求严格,而连接串里没有指定serverTimezone。很多教程里的连接串是很老的那种jdbc:mysql://localhost:3306/xxx,拿到 MySQL 8 上就会踩中。

解决:把 URL 改成带serverTimezone=Asia/Shanghai的完整形式。如果还报错,检查 MySQL 服务端时区设置,执行SELECT NOW()看看返回时间是否正常。

6.2 中文乱码:一条链路上的三处关卡

中文乱码是 JavaWeb 里被问次数最多的问题,没有之一。它不是单一原因造成的,而是从浏览器到数据库整条链路都要统一编码。我遇到过的乱码场景有三种:

第一,页面显示乱码。JSP 顶部少了charset=UTF-8,或者<meta charset="UTF-8">没写。这个最简单,补上就行。

第二,POST 请求参数乱码。表单提交的中文到 Servlet 里变成???。原因是没有在取参数之前执行req.setCharacterEncoding("UTF-8")。这个方法只对 POST 请求生效,GET 请求的编码取决于 Tomcat 的 URIEncoding,Tomcat 8 以后默认就是 UTF-8,所以通常不用额外处理。

第三,数据库里的数据乱码。页面显示正常,但mysql命令行查出来是???。原因就是建库时没用 utf8mb4,或者连接串里少了characterEncoding=utf8。统一改成 utf8mb4 + UTF-8 连接串,这个问题就彻底消失了。

6.3 浏览器访问 404:八成是 Application context 没配对

项目部署了,Tomcat 启动了,但访问http://localhost:8080/user?action=list返回 404。这个问题的排查顺序是:

  1. 先看 IDEA 控制台,Tomcat 是否真的启动成功,没有报端口占用之类的错误。
  2. 再确认部署名称。IDEA 的 Deployment 标签页里,Application context 是你项目访问的基础路径。部署名是javaweb07_war_exploded,你访问路径里就要带这一串;如果嫌长,把 Application context 改成/,然后用http://localhost:8080/user?action=list直接访问。
  3. 如果确认路径没问题,再看有没有把 Servlet 映射写对。@WebServlet("/user/*")的写法会匹配/user以及/user/xxx,如果你访问的是/userServlet,那映射也得改成/userServlet。

404 的排查核心是"把路径和映射对齐",就这么简单。

6.4 连接池耗尽:用完不还资源的下场

我见过一个特别典型的案例:本地一测试列表页没问题,多刷新几次也还行,但连续操作十几分钟后,页面就卡死,控制台报wait millis 3000, active 10, maxActive 10。这就是 Druid 在喊救命——池子里 10 条连接全被借走且没有归还。

当时查到最后,发现是 DAO 层某处ResultSet没关。在还没用 JDK 7 的 try-with-resources 写法时,资源关闭只能靠开发者自觉。所以我在前面所有代码示例里,连接、语句、结果集都是用 try-with-resources 管理的,就是为了从源头避免这个问题。

如果你在用旧版写法,务必养成"谁打开谁关闭"的原则:Connection、Statement、ResultSet三个对象,用完之后按照"先 ResultSet,再 Statement,最后 Connection"的顺序关闭,缺一个都不行。

6.5 一个容易被忽略的细节:密码长度和摘要算法

如果你在注册功能里直接存入明文密码,或者数据库表字段长度只有 20,那将来改用 MD5 或 BCrypt 时会非常尴尬——MD5 的十六进制摘要固定 32 位,20 个字符根本存不下,又得去改表结构。

所以从一开始建表时就把password字段设计成 64 位,既给 MD5 留了空间,也给 SHA-256 留了余量。这个"看似多余"的设计,能在后面省掉一次字段变更。

7. JavaWeb_07 做完之后,下一步的扩展方向

按照上面这套流程走下来,你现在手里的东西已经不是一个"练习片段",而是一个麻雀虽小、五脏俱全的 JavaWeb 项目:有数据库设计、有 JDBC/连接池、有分层架构、有完整的增删改查页面。这个项目可以直接当成课设的底子,也可以作为自己练手的基础框架。

如果还有余力,建议在项目基础上做三个方向的扩展。第一个是登录状态管理——目前所有页面都是直接访问的,没有拦截逻辑。可以结合 Session 实现一个简单的登录判断,在doGet开头检查req.getSession().getAttribute("loginUser")是否为 null,为 null 就重定向到登录页。这是 JavaWeb 里最经典的需求之一。

第二个是分页查询——用户数据一旦变多,findAll()就会把全部记录查出来,页面会渲染得很漫长。标准做法是写一个分页查询的 SQL,配合LIMIT offset, size,再在前端做一个页码导航。建议你手动推一遍(pageNum - 1) * pageSize这个偏移量公式,分页逻辑必须自己亲手算过才会不会忘。

第三个是 Filter 的统一编码处理。我在第 6 节说的req.setCharacterEncoding("UTF-8"),目前每个 Servlet 要写一遍,很烦。可以定义一个CharacterEncodingFilter,在doFilter里统一设置编码,然后再chain.doFilter。这一步做完,你就正式踏入了"用 Filter 解决横切问题"的思路,后面接触 SpringMVC 时你会发现,一切都是有迹可循的。

我自己的体会是,JavaWeb 阶段的价值不在于用到了多高级的技术,而在于让你亲手把"用户输入 → 服务器处理 → 数据库读写 → 页面反馈"这条链路上每一个环节的粗细都摸了一遍。网上再多的教程和笔记,都抵不过自己从头搭一遍项目来得扎实。你要是照着做下来,期间踩过两三个坑,处理过一两回乱码,那这门课的核心部分就真正属于你了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询