☰
JavaWeb JDBC连接MySQL实战:通讯录增删改查完整教程
2026/10/13 0:23:17 网站建设 项目流程

简介:本资源是一份面向JavaWeb初学者与入门开发者的JDBC数据库实战教学材料,聚焦MySQL通讯录系统的增删改查功能实现,帮助读者掌握Web应用中数据库连接、SQL执行、资源管理等核心技能。压缩包为单文件PDF文档(111KB),内容涵盖Eclipse项目搭建、Tomcat配置、BaseDao通用数据库操作类设计、Contact实体建模、ContactDao具体CRUD方法实现,以及MySQL时区配置、PreparedStatement参数绑定、ResultSet结果映射等关键细节,并附有完整可运行代码片段与界面效果说明。已有1880人学习下载,适合正在学习JavaWeb基础、准备课程设计或夯实JDBC底层原理的开发者。读者可直接复用其中的DAO分层结构、异常处理逻辑与连接配置方案,快速构建自己的数据库交互模块。

1. JavaWeb JDBC + MySQL 通讯录:一个能跑通、能调试、能改出生产味的最小闭环

你不是在学“JDBC 是什么”,而是在解决一个具体问题:用最轻量、最可控的方式,让一个 Java Web 页面真正连上 MySQL,点一下就增删改查一条联系人数据,不报错、不丢数据、不乱码、不时区偏移。这个通讯录案例,表面看是学生作业级 CRUD,实则是 Java Web 工程师绕不开的“第一道真实关卡”——它把数据库驱动加载、连接字符串参数博弈、SQL 注入防御边界、JSP/Servlet 生命周期耦合、日期格式化陷阱、资源手动释放风险,全压缩进一个可运行的 WAR 包里。它适合刚配好 Tomcat 的新手,也适合想快速验证 JDBC 底层行为的老手:没有 Spring Boot 自动装配的黑匣子,所有连接、预编译、结果集遍历、异常堆栈都赤裸可见。如果你正被SQLException: The server time zone value 'XXX' is unrecognized卡住,或发现rs.getDate("date")返回 null 而数据库明明有值,或改完代码重启 Tomcat 后页面还是旧的——这个项目就是你的调试沙盒。它不教 ORM,但教会你为什么 ORM 是必要的;它不用连接池,却让你亲手踩出连接泄漏的坑。


2. 从零建项目:Eclipse + Tomcat + MySQL 的三件套落地实操

2.1 创建 Dynamic Web Project 并校准 JDK 与 Servlet 版本

在 Eclipse 中选择File → New → Dynamic Web Project。项目名建议用ContactWebApp(避免空格和中文)。关键配置点有三个:

  • Target runtime:必须选中已配置好的 Tomcat 服务器(如 Apache Tomcat v9.0)。若未配置,点击右侧New Runtime,指向 Tomcat 解压目录的bin上一级路径。
  • Dynamic web module version:选4.0(对应 Servlet 4.0 规范)。这是当前主流 Tomcat 9+ 的默认版本,能支持@WebServlet注解,避免在web.xml中手动注册 Servlet。
  • Configuration:勾选Generate web.xml deployment descriptor。虽然注解可用,但保留web.xml便于后续添加过滤器、监听器等扩展。

提示:创建后检查项目根目录下build path → Libraries,确认Apache Tomcat运行时库已自动加入,且JRE System Library版本 ≥ JDK 8(本案例基于 JDK 8 编译)。

2.2 数据库准备:MySQL 8.0+ 的建库、建表与用户授权

打开 Navicat 或 MySQL 命令行,执行以下 SQL:

-- 创建数据库,显式指定字符集与排序规则,规避中文乱码 CREATE DATABASE IF NOT EXISTS demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 切换到 demo 库 USE demo; -- 创建 contact 表,注意 date 字段类型为 DATE(非 DATETIME),与 Java 的 java.util.Date 映射更稳定 CREATE TABLE contact ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, age INT NOT NULL CHECK (age BETWEEN 1 AND 133), phone VARCHAR(20) NOT NULL, date DATE NOT NULL, favorite VARCHAR(100) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 创建专用应用用户(非 root),最小权限原则 CREATE USER 'contact_app'@'localhost' IDENTIFIED BY 'contact123'; GRANT SELECT, INSERT, UPDATE, DELETE ON demo.contact TO 'contact_app'@'localhost'; FLUSH PRIVILEGES;

逻辑说明:utf8mb4是 MySQL 8.0 默认字符集,支持完整 Unicode(包括 emoji),比旧版utf8更可靠;CHECK约束直接在数据库层拦截非法年龄值,比 Java 层校验更前置;专用用户contact_app避免硬编码 root 密码,符合安全基线。

2.3 JDBC 驱动引入:mysql-connector-java 8.0.x 的正确姿势

不要下载mysql-connector-java-5.1.x.jar(兼容老版 MySQL 5.x,但不支持serverTimezone参数);必须使用8.0.28+版本(如mysql-connector-java-8.0.33.jar)。下载地址: https://dev.mysql.com/downloads/connector/j/ (选择 Platform Independent 的 ZIP 包)。
解压后,将mysql-connector-java-8.0.33.jar复制到项目WebContent/WEB-INF/lib/目录下。在 Eclipse 中右键项目 →Build Path → Configure Build Path → Libraries → Add JARs,选中该 JAR。
验证是否生效:在BaseDao.java中尝试import com.mysql.cj.jdbc.Driver;—— 若无红色波浪线,即导入成功。

2.4 项目目录结构固化:按标准 Web 应用分层组织

确保项目结构如下(Eclipse 自动生成部分,需手动补全):

ContactWebApp/ ├── src/ # Java 源码根目录 │ ├── pers.contact.dao/ # BaseDao 所在包 │ ├── pers.contact.entity/ # Contact 实体类 │ ├── pers.contact.service/ # ContactService 业务逻辑 │ └── pers.contact.servlet/ # ContactServlet ├── WebContent/ # Web 资源根目录 │ ├── index.jsp # 主页(展示联系人列表) │ ├── add.jsp # 新增表单页 │ ├── edit.jsp # 修改表单页 │ ├── WEB-INF/ │ │ ├── web.xml # 部署描述符(即使用注解也保留) │ │ └── lib/ │ │ └── mysql-connector-java-8.0.33.jar │ └── resources/ # (可选)存放 JSTL 等额外依赖 ├── build/ # 编译输出目录(Eclipse 自动生成) └── .project/.classpath # Eclipse 项目元数据

参数说明:src下的包名pers.contact.*是反向域名命名法(pers表示 personal),避免类名冲突;WebContent是 Eclipse 的 Web 根目录,Tomcat 部署时会将其映射为应用上下文根路径(如http://localhost:8080/ContactWebApp/)。


3. JDBC 连接核心:BaseDao 的参数解析、时区修复与资源管理真相

3.1 连接 URL 深度拆解:每个参数都是救命稻草

原代码中的 URL:

public static final String URL = "jdbc:mysql://localhost:3306/demo?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&serverTimezone=GMT%2B8";

我们逐个解析其必要性与风险:

参数必要性作用替代方案/风险提示
rewriteBatchedStatements=true⚠️ 推荐开启将addBatch()的多条 INSERT 合并为一条INSERT ... VALUES (...),(...),提升批量插入性能。若关闭,每条 INSERT 单独提交,效率极低。MySQL 5.7+ 默认 false;8.0+ 可设为 true,但需确认驱动版本 ≥ 5.1.37
useUnicode=true&characterEncoding=utf8✅ 强制要求告诉 JDBC 驱动以 UTF-8 编码传输 SQL 和结果。若缺失,中文字段(如name,favorite)存入/读取必乱码。utf8在 MySQL 8.0+ 中实际指utf8mb3,应升级为characterEncoding=utf8mb4(需驱动 8.0.12+)
allowMultiQueries=true❌ 不推荐允许;分隔多条 SQL(如"SELECT *; INSERT ..."),是严重 SQL 注入漏洞温床。本案例未使用,应删除。删除后,若某处误写多语句,executeQuery()会抛SQLException,反而暴露问题
serverTimezone=GMT%2B8✅ MySQL 8.0+ 必须MySQL 8.0 默认时区为SYSTEM(常为 UTC),而 JVM 时区为Asia/Shanghai(GMT+8)。不指定会导致ResultSet.getDate()返回 null 或错误日期。GMT%2B8是 URL 编码后的GMT+8。更规范写法:serverTimezone=Asia/Shanghai(需系统时区数据库支持);或serverTimezone=GMT%2B08:00(ISO 8601 格式)

修正后的 URL(推荐):

public static final String URL = "jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true";

3.2 DriverManager.getConnection() 的隐藏陷阱与现代替代方案

BaseDao.getConnection()中调用DriverManager.getConnection(URL, USER, PASSWORD)是 JDBC 1.0 时代写法。它的问题在于:

  • 无连接池:每次getConnection()都新建 TCP 连接,高并发下耗尽 MySQL 连接数(默认 151);
  • 无超时控制:网络抖动时getConnection()可能无限阻塞;
  • 密码明文硬编码:PASSWORD = "sasa"存在严重安全风险。

血泪经验:在本案例中,因是单机学习环境,可暂不引入连接池(如 HikariCP),但必须做两件事:

  1. 将密码从代码中剥离,改为从外部属性文件读取:
    在src/下新建db.properties:
    jdbc.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai jdbc.username=contact_app jdbc.password=contact123
    修改BaseDao,用Properties加载:
    private static Properties props = new Properties(); static { try (InputStream is = BaseDao.class.getClassLoader().getResourceAsStream("db.properties")) { props.load(is); } catch (IOException e) { throw new RuntimeException("Failed to load db.properties", e); } } public void getConnection() { try { Class.forName("com.mysql.cj.jdbc.Driver"); conn = DriverManager.getConnection( props.getProperty("jdbc.url"), props.getProperty("jdbc.username"), props.getProperty("jdbc.password") ); } catch (Exception e) { e.printStackTrace(); } }
  2. 为getConnection()添加超时:
    // 在 getConnection() 中替换 DriverManager.getConnection(...) 为: conn = DriverManager.getConnection( props.getProperty("jdbc.url") + "&connectTimeout=3000&socketTimeout=30000", props.getProperty("jdbc.username"), props.getProperty("jdbc.password") );
    connectTimeout=3000(3秒)防连接建立卡死;socketTimeout=30000(30秒)防查询执行过长。

3.3 closeAll() 方法的致命缺陷与正确关闭顺序

原closeAll()方法存在两个严重问题:

  1. 未判空直接 close:若rs或pstmt为null(如executeQuery()抛异常后),调用rs.close()会触发NullPointerException,掩盖原始 SQL 错误;
  2. 关闭顺序错误:应按ResultSet → PreparedStatement → Connection逆序关闭。若先关Connection,rs和pstmt可能失效,导致SQLException。

修正后的closeAll():

public void closeAll() { // 逆序关闭:ResultSet → PreparedStatement → Connection if (rs != null) { try { rs.close(); } catch (SQLException e) { // 记录日志,但不抛出,避免覆盖主流程异常 System.err.println("Failed to close ResultSet: " + e.getMessage()); } } if (pstmt != null) { try { pstmt.close(); } catch (SQLException e) { System.err.println("Failed to close PreparedStatement: " + e.getMessage()); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { System.err.println("Failed to close Connection: " + e.getMessage()); } } }

逻辑说明:if (xxx != null)判空是防御性编程基础;System.err.println替代e.printStackTrace(),避免控制台刷屏;关闭失败不中断主流程,保证其他资源能继续释放。


4. 增删改查实现:ContactService 的 SQL 安全边界与日期处理玄学

4.1 Contact 实体类:Java Date 与 MySQL DATE 的精准映射

Contact类中private Date date;对应 MySQL 的DATE类型(仅年月日,无时分秒)。关键点:

  • 构造函数传参:new Contact(..., rs.getDate(5), ...)中rs.getDate(5)返回java.sql.Date,它是java.util.Date的子类,但只保留日期部分(时分秒为 0)。若数据库存的是DATETIME,此处会丢失时间。
  • 前端传参解析:add.jsp中<input type="date">提交的格式为yyyy-MM-dd(如2023-10-05),但SimpleDateFormat("yy-MM-dd")会将2023解析为2020(yy只取后两位)。

避坑修正:

  1. SimpleDateFormat改为yyyy-MM-dd:
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // 注意是 yyyy,非 yy
  2. 使用java.time.LocalDate替代java.util.Date(推荐,Java 8+):
    • MySQL 表date DATE→ JavaLocalDate;
    • rs.getObject("date", LocalDate.class)替代rs.getDate("date");
    • PreparedStatement.setObject(5, contact.getDate())替代setDate(5, new java.sql.Date(contact.getDate().getTime()))。

    优势:LocalDate无时区概念,与DATE类型语义完全一致,避免java.util.Date的时区转换混乱。

4.2 ContactService 的 SQL 注入防御实践

原AddContact()方法:

String sql = "insert into contact(name,age,phone,date,favorite) values(?,?,?,?,?)"; num = executeUpdate(sql, contact.getName(), contact.getAge(), contact.getPhone(), contact.getDate(), contact.getFavorite());

这是正确的 PreparedStatement 用法:?占位符 +setObject()绑定,天然防御 SQL 注入。但需警惕两个常见误用:

  • 拼接 SQL 字符串(❌ 危险!):
    // 错误示范:若 name='O'Reilly',SQL 变成 ...values('O'Reilly',...),语法错误 String sql = "insert into contact(name) values('" + contact.getName() + "')";
  • 对数字类型用字符串拼接(❌ 低效且危险):
    // 错误:age 是 int,但被转成字符串再拼接,失去类型安全 String sql = "update contact set age=" + contact.getAge() + " where id=" + id;

ContactService 中各方法的安全写法总结:

操作SQL 模板绑定参数方式关键说明
新增INSERT INTO contact(name,age,phone,date,favorite) VALUES(?,?,?,?,?)pstmt.setString(1, name); pstmt.setInt(2, age); ...age用setInt(),date用setObject(4, localDate, Types.DATE)
查询全部SELECT * FROM contact无参数简单查询,无需占位符
按 ID 查询SELECT * FROM contact WHERE id = ?pstmt.setInt(1, id)id是主键,用setInt最安全
删除DELETE FROM contact WHERE id = ?pstmt.setInt(1, id)同上,避免WHERE id = '1 OR 1=1'注入
更新UPDATE contact SET name=?,age=?,phone=?,date=?,favorite=? WHERE id=?pstmt.setString(1,name); ... pstmt.setInt(6,id)WHERE条件必须用?,且id放最后

4.3 executeUpdate() 与 executeQuery() 的返回值语义辨析

  • executeUpdate(String sql, Object... obj):用于INSERT/UPDATE/DELETE,返回影响行数(int)。若返回0,表示没匹配到任何记录(如DELETE WHERE id=999),而非执行失败。
  • executeQuery(String sql, Object... obj):用于SELECT,返回 ResultSet。必须用while(rs.next())遍历,否则rs指针在首行前,rs.getString(1)会抛SQLException。

典型错误排查:

  • 现象:GetAllContact()返回空List,但数据库有数据。
  • 原因:rs = executeQuery(sql);后未调用rs.next(),直接rs.getInt(1)。
  • 解决:确保循环内rs.next()为true才取值:
    while (rs.next()) { // 必须先调用 next() Contact f = new Contact( rs.getInt("id"), rs.getString("name"), rs.getInt("age"), rs.getString("phone"), rs.getObject("date", LocalDate.class), // 推荐用列名而非序号 rs.getString("favorite") ); list.add(f); }

5. 避坑指南:JSP/Servlet 开发中 5 个高频翻车现场与后悔药

5.1 现象:访问index.jsp时页面空白,控制台无报错,浏览器显示 404

原因:index.jsp未部署到 Tomcat 的webapps目录下,或 Eclipse 未启用自动发布。
解决:

  • 在 Eclipse 中右键项目 →Properties → MyEclipse → Web → Web Deployment Assembly,确认src和WebContent已映射到/WEB-INF/classes和/;
  • 右键项目 →Run As → Run on Server,选择 Tomcat,勾选Auto publish when resources change;
  • 检查 Tomcatwebapps/目录下是否存在ContactWebApp/文件夹,及其中index.jsp是否存在。

5.2 现象:新增联系人后,alert('新增成功!')弹出,但刷新index.jsp无新数据

原因:ContactServlet中doPost()的response.sendRedirect("ContactServlet?do=index")发起的是客户端重定向,但index.jsp顶部的 Java 脚本块:

<% ContactService ud = new ContactService(); List<Contact> ulist = ud.GetAllContact(); request.setAttribute("ulist", ulist); %>

未被执行(因为重定向后是全新请求,index.jsp的脚本块在服务端渲染时才执行)。
解决:将数据获取逻辑移到ContactServlet的do=index分支中,统一由 Servlet 控制流程:

if (dos.equals("index")) { List<Contact> ulist = ud.GetAllContact(); request.setAttribute("ulist", ulist); request.getRequestDispatcher("/index.jsp").forward(request, response); // forward 保持 request 作用域 return; }

关键:forward()是服务端内部跳转,request属性可传递;sendRedirect()是客户端跳转,request属性丢失。

5.3 现象:edit.jsp中修改生日,提交后数据库date字段变为0000-00-00

原因:SimpleDateFormat("yyyy-MM-dd")解析前端2023-10-05时,生成的Date对象包含时分秒(如2023-10-05 00:00:00),而PreparedStatement.setDate(4, new java.sql.Date(date.getTime()))会截断为2023-10-05,但若date本身为null或格式错误,setDate()会设为0000-00-00。
解决:

  • 前端<input type="date">提交的值已是yyyy-MM-dd,后端直接解析为LocalDate:
    String dates = request.getParameter("date"); LocalDate localDate = LocalDate.parse(dates); // 自动识别 yyyy-MM-dd 格式 Contact contact = new Contact(id, name, age, phone, localDate, favorite);
  • Contact类中date字段改为LocalDate,BaseDao中executeUpdate()用pstmt.setObject(4, localDate, Types.DATE)。

5.4 现象:add.jsp提交后,中文姓名显示为??(乱码)

原因:ContactServlet中request.setCharacterEncoding("utf-8")位置错误。该方法必须在读取任何请求参数前调用,否则getParameter()已按默认 ISO-8859-1 解码,再设编码无效。
解决:将request.setCharacterEncoding("utf-8")移至doPost()开头:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); // 必须第一行! response.setContentType("text/html;charset=utf-8"); // ... 后续逻辑 }

同时,add.jsp的<head>中添加:

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

5.5 现象:ContactServlet中@WebServlet("/ContactServlet")注解不生效,访问http://localhost:8080/ContactWebApp/ContactServlet报 404

原因:web.xml中<web-app>根元素的version属性与 Servlet 规范不匹配。若web.xml是 Eclipse 自动生成的,可能为2.5(对应 Servlet 2.5),不支持注解。
解决:

  • 打开WebContent/WEB-INF/web.xml,将第一行改为:
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">
  • 或直接删除web.xml中servlet和servlet-mapping配置,完全依赖@WebServlet。

6. 进阶验证:用 Postman 模拟请求、日志埋点与连接泄漏自检技巧

6.1 绕过浏览器,用 Postman 直接测试 Servlet 接口

JSP 页面是 UI 层,真正的业务逻辑在ContactServlet。用 Postman 可精准验证后端行为,避免前端干扰:

  • 新增联系人:
    • Method:POST
    • URL:http://localhost:8080/ContactWebApp/ContactServlet?do=add
    • Body → x-www-form-urlencoded:
      name: 张三 age: 25 phone: 13800138000 date: 1998-05-15 favorite: 篮球
    • Expected: 返回alert('新增成功!')的 HTML 片段,且数据库contact表新增一行。
  • 查询全部:
    • Method:GET
    • URL:http://localhost:8080/ContactWebApp/ContactServlet?do=index
    • Expected: 返回index.jsp渲染后的 HTML,含<tr>表格行。

价值:Postman 可保存请求集合,形成 API 文档雏形;可设置Authorization头测试登录态(本例未实现,但为后续扩展预留)。

6.2 在 BaseDao 中添加轻量级日志埋点,定位慢 SQL

当功能跑通后,下一步是可观测性。在executeUpdate()和executeQuery()中加入执行时间统计:

public int executeUpdate(String sql, Object... obj) { long start = System.currentTimeMillis(); int num = 0; getConnection(); try { PreparedStatement pstmt = conn.prepareStatement(sql); for (int i = 0; i < obj.length; i++) { pstmt.setObject(i + 1, obj[i]); } num = pstmt.executeUpdate(); long cost = System.currentTimeMillis() - start; if (cost > 1000) { // 超过 1 秒标为慢 SQL System.err.println("[SLOW SQL] " + sql + " | Cost: " + cost + "ms"); } } catch (SQLException e) { System.err.println("[SQL ERROR] " + sql + " | " + e.getMessage()); throw e; // 重新抛出,不吞异常 } finally { closeAll(); } return num; }

效果:当DELETE FROM contact WHERE id=1因索引缺失变慢时,控制台立即打印[SLOW SQL] ...,驱动你去EXPLAIN DELETE ...分析执行计划。

6.3 连接泄漏自检:用 MySQL 命令行实时监控活跃连接

JDBC 手动管理连接极易泄漏(conn未关闭)。在 MySQL 命令行执行:

-- 查看当前所有连接 SHOW PROCESSLIST; -- 查看 contact_app 用户的连接(重点关注 State 为 'Sleep' 且 Time > 60 的僵尸连接) SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO FROM INFORMATION_SCHEMA.PROCESSLIST WHERE USER = 'contact_app' AND TIME > 60; -- 强制杀死指定连接(ID 从上一步获取) KILL 123; -- 123 是连接 ID

预防策略:在BaseDao的closeAll()末尾添加连接状态检查(开发环境):

if (conn != null && !conn.isClosed()) { System.err.println("WARNING: Connection not closed! Leaking..."); }

从那以后我每次写完 DAO 方法,都强制走一遍SHOW PROCESSLIST,尤其在add/edit/del操作后。一次发现TIME持续增长,顺藤摸瓜找到GetContact()中rs未关闭的 bug。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询