ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

JavaWeb通讯录案例:JDBC增删改查实战与避坑指南

JavaWeb通讯录案例:JDBC增删改查实战与避坑指南 简介面向JavaWeb初学者的通讯录案例文档基于EclipseTomcatMySQL环境完整讲解JDBC连接数据库、BaseDao工具类封装、DAO层完成联系人增删改查的全过程并重点提示MySQL时区设置等连接常见坑点。资源包为单个PDF文件大小仅111KB适合随查随用。已有1880人学习下载。文档从Contact实体类、BaseDao封装到ContactDao操作均有代码呈现还结合前端AJAX请求与Servlet调用梳理了完整调用链路可帮助读者快速搭建可运行的联系人管理模块巩固ServletJDBC基础也可作为课堂作业或毕设的起步模板。1. 一个 JavaWeb 通讯录案例为什么值得先抄一个 JDBC 版再谈框架如果你正在学 JavaWeb 的 JDBC MySQL 增删改查手头最缺的不是文档而是一个「能跑起来、看得见数据变化」的完整项目。这个通讯录案例恰好就是这么个东西Eclipse 建的 Web 项目、Tomcat 跑服务、MySQL 存数据、Servlet 收请求、JSP 渲染页面一条请求从浏览器点按钮到数据库再回到页面的完整链路都有。它不是那种只贴几段 SQL 的碎片教程而是一个能直接部署、点新增、点修改、点删除都能看到效果的闭环。适合两类人一是刚学完 Servlet 和 JSP 语法、想找个小项目练手的新手二是想快速回顾 JDBC 原生写法、或者准备给新人做培训的熟手。我的建议很直接先把这个 JDBC 手写版跑通再去碰 MyBatis否则你根本不知道 ORM 帮你省了哪些事。2. 先拆项目结构三层分包 六个核心类的职责分配2.1 分包设计先看清楚每个类该放哪拿到项目先别急着部署先扫一眼包结构。整个工程分了四个包pers.contact.dao放数据库访问层pers.contact.entity放实体类pers.contact.service放业务逻辑层pers.contact.servlet放控制器层。这个分层在练手项目里不算复杂但已经够你看明白一件事实体类只存数据不碰数据库DAO 层只写 SQL不写业务判断Service 层在 Spring 没出现之前其实和 DAO 层的边界是模糊的——这个项目里ContactService直接继承了BaseDao严格说它既是 Service 又是 DAO。对新手来说这不是缺点反而方便因为少绕一层就能看清 SQL 是怎么被调起来的。我还注意到一个细节Eclipse 里新建 Dynamic Web Project 之后WEB-INF/lib目录默认是空的需要手动把 MySQL 驱动 jar 和 JSTL 的 jar 放进去。很多新手在这里翻车项目跑起来报ClassNotFoundException第一反应是代码写错了其实只是 jar 没到位。后面避坑章节我会把这个问题展开说。2.2 实体类 Contact字段定义直接对应表结构Contact这个类的六个字段——id、name、age、phone、date、favorite每一行都对应contact表的一列。代码本身没什么技术含量但它定了整个项目的基调数据库里有什么Java 里就有什么。public class Contact { private int id; private String name; private int age; private String phone; private Date date; private String favorite; // 全参构造方法 getter/setter public Contact(int id, String name, int age, String phone, Date date, String favorite) { super(); this.id id; this.name name; this.age age; this.phone phone; this.date date; this.favorite favorite; } }全参构造方法在这个项目里承担的角色很关键ContactService里查完ResultSet之后直接new Contact(rs.getInt(1), rs.getString(2), ...)一行代码把结果集转成对象。我建议你留意date字段的类型是java.util.Date不是java.sql.Date——这个区分在写 Servlet 时特别容易踩坑后面日期格式化那一节我会专门讲。2.3 BaseDao把连接和释放写成模板方法先看第三段核心代码——BaseDao。这个类是整套 JDBC 操作的底座它把「加载驱动、拿连接、执行 SQL、关资源」四件事封装成了模板方法public class BaseDao { private static final String DRIVER com.mysql.jdbc.Driver; public static final String URL jdbc:mysql://localhost:3306/demo? rewriteBatchedStatementstrueuseUnicodetrue characterEncodingutf8allowMultiQueriestrue serverTimezoneGMT%2B8; public static final String USER root; public static final String PASSWORD sasa; Connection conn null; PreparedStatement pstmt null; ResultSet rs null; public void getConnection() { try { Class.forName(DRIVER); conn DriverManager.getConnection(URL, USER, PASSWORD); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } } public int executeUpdate(String sql, Object... obj) { 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(); } catch (SQLException e) { e.printStackTrace(); } finally { closeAll(); } return num; } public ResultSet executeQuery(String sql, Object... obj) { getConnection(); try { PreparedStatement pstmt conn.prepareStatement(sql); for (int i 0; i obj.length; i) { pstmt.setObject(i 1, obj[i]); } rs pstmt.executeQuery(); } catch (SQLException e) { e.printStackTrace(); } return rs; } }这套写法有两个点值得你细看。第一executeUpdate和executeQuery都接收可变参数Object... obj调用方传几个参数都行循环里用pstmt.setObject(i 1, obj[i])按顺序绑定占位符。这就是 PreparedStatement 相比 Statement 的核心优势——参数化查询SQL 注入被挡在语法层面。第二getConnection()在每个方法内部调用不是构造方法里调这样每个操作都拿新连接虽然性能差但练手项目里逻辑清晰。提示serverTimezoneGMT%2B8里的%2B是 URL 编码的加号意思是 GMT8。MySQL 8.x 默认时区是 UTC如果不加这个参数连接时会报时区错误这是新手第一个翻车点。2.4 Service 层继承 BaseDaoSQL 与业务混在一起ContactService直接extends BaseDao所以它能直接调executeQuery和executeUpdate。再看一眼它写的五个方法GetAllContact查全部、AddContact插入、DeleteContact按 id 删、GetContact按 id 查单个、UpdateContact按 id 改。SQL 就写在方法体里没有单独的 mapper 文件也没有DAO接口——这是原生 JDBC 最朴素的形态。public int AddContact(Contact contact) { int num 0; String sql insert into contact(name,age,phone,date,favorite) values(?,?,?,?,?); try { num executeUpdate(sql, contact.getName(), contact.getAge(), contact.getPhone(), contact.getDate(), contact.getFavorite()); } catch (Exception e) { e.printStackTrace(); } return num; } public int UpdateContact(Contact contact) { int num 0; String sql update contact set name ?,age ?,phone ?,date ?,favorite ? where id ?; try { num executeUpdate(sql, contact.getName(), contact.getAge(), contact.getPhone(), contact.getDate(), contact.getFavorite(), contact.getId()); } catch (Exception ex) { ex.printStackTrace(); } return num; }注意UpdateContact的 SQL 顺序set子句里 5 个字段where里 1 个 id一共 6 个占位符调用时参数顺序必须严格对应。这是我见过新手最容易写错的地方——参数个数对不上或者顺序写反运行时不报错但数据更新错列。写这类方法时建议对着 SQL 数一遍占位符再数一遍实参养成习惯。3. 数据库连接是第一步URL 参数、时区、驱动缺一不可3.1 建表把 JSP 页面字段翻译成 SQL项目跑起来之前数据库得先准备好。连接 URL 里写的是localhost:3306/demo所以你要在 MySQL 里建一个名为demo的库再建一张contact表。表字段根据实体类和页面表单反推即可CREATE DATABASE IF NOT EXISTS demo DEFAULT CHARACTER SET utf8; USE demo; CREATE TABLE IF NOT EXISTS contact ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, age INT NOT NULL, phone VARCHAR(20) NOT NULL, date DATE DEFAULT NULL, favorite VARCHAR(100) DEFAULT NULL, PRIMARY KEY (id) );id设成AUTO_INCREMENT自增主键新增时不需要传 id数据库自动生成。date字段用DATE类型对应页面上input typedate提交的值。如果你用 Navicat 操作直接新建数据库demo字符集选utf8mb4再执行上面的建表语句就行。utf8mb4比utf8多支持 emoji现在写新库我一般直接用utf8mb4。3.2 连接 URL 参数逐个拆解连接串是这套代码里含金量最高的部分我拆给你看URL 参数作用备注rewriteBatchedStatementstrue开启批处理语句重写批量操作时性能提升明显useUnicodetruecharacterEncodingutf8指定编码为 UTF-8不设会乱码allowMultiQueriestrue允许一次执行多条 SQL用分号分隔serverTimezoneGMT%2B8指定服务器时区为东八区解决 MySQL 默认时区与本地相差 8 小时的问题我自己写项目时还会额外加两个参数useSSLfalse和allowPublicKeyRetrievaltrue。MySQL 8.x 默认开启 SSL本地开发环境不配证书就会报 SSL 连接错误allowPublicKeyRetrieval是为了配合useSSLfalse使用避免 RSA 公钥获取失败。这两个坑在避坑章节会细说。3.3 驱动 jar 的引入方式三类典型错误驱动类com.mysql.jdbc.Driver需要 mysql-connector jar 包才能加载。常见的引入方式有三种我建议按下面的顺序排查把mysql-connector-java-x.x.x.jar放到WEB-INF/lib下这是动态 Web 项目最稳妥的方式打包部署时 jar 会自动跟着走。如果你用的是 Maven 工程在pom.xml里加mysql-connector-java依赖然后让 IDE 自动下载。注意 8.x 版本的驱动类名是com.mysql.cj.jdbc.Driver和 5.x 的com.mysql.jdbc.Driver不完全一样。只在项目构建路径里添加引用、但没复制进WEB-INF/lib——Tomcat 运行时会找不到类启动不报错一访问页面就抛ClassNotFoundException。我见过太多人栽在第三种情况上Eclipse 里 Build Path 配好了代码不报红一跑起来就翻车。检查办法很简单展开项目的Deployment Assembly看 jar 是否被发布到了WEB-INF/lib。这个项目不是 Maven 工程所以老老实实走第一种方式最省心。4. 增删改查的实现套路PreparedStatement、参数绑定与 Servlet 分发4.1 查全部与查单个ResultSet 转实体类的两种姿势GetAllContact和GetContact都用executeQuery拿ResultSet区别只在于有没有where条件。看查全部这段public ListContact GetAllContact() { ListContact list new ArrayList(); String sql select * from contact; rs executeQuery(sql); try { while (rs.next()) { Contact f new Contact(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getString(4), rs.getDate(5), rs.getString(6)); list.add(f); } } catch (SQLException e) { e.printStackTrace(); } return list; }我提醒两个小毛病你可以对比着自己的代码看看。第一new ArrayList()没写泛型编译器会报警告建议写成new ArrayListContact()。第二rs.getInt(1)这种按列索引取值的方式阅读性不如rs.getInt(id)一旦 SQL 里调整了列顺序索引全错。虽然这个项目暂时不会出问题但你要知道这两种写法的差异。rs.next()是游标移动返回true表示有下一行while循环里每次new一个Contact对象塞进List这是 JDBC 查询的标准姿势。GetContact与之类似只是 SQL 变成了select * from contact where id ?调用时传 id 参数。注意它返回的是单个Contact没查到就返回null调用方要处理空指针风险。4.2 新增与修改表单参数到实体类的映射新增的入口在 Servlet 的doadd分支里。前端add.jsp把五个字段通过post提交到ContactServlet?doaddServlet 逐个getParameter取值组装成Contact对象再调ud.AddContact(contact)。关键是日期字段的处理String dates request.getParameter(date); SimpleDateFormat sdf new SimpleDateFormat(yy-MM-dd); Date date null; try { date sdf.parse(dates); } catch (ParseException e) { e.printStackTrace(); }这段代码有坑SimpleDateFormat(yy-MM-dd)里的yy是两位年份虽然 Java 解析时会按「80 年问题」的规则把24解析为2024但如果你输入的是1998四位年份会被yy模式截断解析结果就是错误年份。更规范的做法是用yyyy-MM-dd和 HTML 表单input typedate的提交格式完全对齐。这个坑我在避坑章节里会再讲一次因为它值得讲。修改的流程稍微绕一点点「修改」链接先走doeditbeforeServlet 里查出这条联系人存进 session然后sendRedirect(edit.jsp)JSP 页面从 session 里取edituser回显到表单提交时再走doedit分支执行UpdateContact。这种「先查后改」的两步走设计在练手项目里很常见但也暴露了一个问题数据塞在 session 里刷新页面或者开多个标签页时容易读到旧数据。等你做了 Spring MVC 再回头看会发现这里用 request 转发 隐藏表单域更干净。4.3 Servlet 分发一个入口用 do 参数区分动作ContactServlet只重写了doGet和doPostdoGet里直接调doPost所以表单提交和链接跳转共用同一套逻辑。请求进来后先取do参数然后按照index / add / del / editbefore / edit五个值走不同分支String dos request.getParameter(do); if (dos null || dos.equals()) { dos index; } if (dos.equals(index)) { ListContact ulist ud.GetAllContact(); request.setAttribute(ulist, ulist); request.getRequestDispatcher(/index.jsp).forward(request, response); return; } if (dos.equals(add)) { // 取参数 - 组 Contact - AddContact - 输出 script 脚本跳转 }删除操作值得单独看一眼因为它的设计很典型——前端直接发链接请求if (dos.equals(del)) { String ids request.getParameter(id); int id Integer.parseInt(ids); ud.DeleteContact(id); out.print(scriptalert(删除成功);window.locationContactServlet?doindex;/script); }hrefContactServlet?dodelid${U.id}这种写法简单粗暴适合练手。但你要知道这在生产环境里是禁止的GET 请求不该做删除操作因为搜索引擎爬虫或者浏览器预加载都可能触发删数据。你要是在简历里写这个项目面试官很可能会追问「你有没有避免 GET 删除」提前想好怎么回答。4.4 页面层JSTL 遍历 Foundation 样式index.jsp用 JSTL 的c:forEach遍历ulist渲染表格这是最基础的 JSP JSTL 组合。要注意的是它引用了http://java.sun.com/jsp/jstl/core这个 taglib必须把jstl.jar和standard.jar放到WEB-INF/lib下否则 JSP 编译阶段直接报错。页面样式用的是 Foundation 5.5.3 的 CDNjQuery 2.1.1 也是 CDN 引入所以运行时需要能访问外部 CDN。index.jsp里有一行ContactService ud new ContactService(); ListContact ulist ud.GetAllContact(); request.setAttribute(ulist, ulist);。这段 scriptlet 重复查了一次数据库——Servlet 的index分支已经查了一次JSP 又查了一次。功能上没毛病但你要是想进阶第一件事就是把 JSP 顶部的 scriptlet 删干净只靠 Servlet 转发过来的数据渲染页面。这是 JSP 规范里被强调过无数次的原则JSP 只负责展示不写 Java 逻辑。5. 实战避坑从时区报错到日期消失四类高频问题的复盘5.1 时区报错The server time zone value 与本地相差 8 小时现象第一次连接 MySQL 抛出The server time zone value Öйú±ê׼ʱ¼ä is unrecognized或者数据插入后查出来时间差 8 小时。原因项目的 MySQL 是 8.x驱动是 5.x 风格com.mysql.jdbc.Driver而 MySQL 8.x 安装时默认时区是美国时区和本地中国时区差 8 个小时。更糟的是如果 MySQL 端时区没设置驱动无法识别服务器时区连接直接被拒。解决URL 里加serverTimezoneGMT%2B8或者在建库时执行set global time_zone 8:00。如果仍然报错把 URL 里的GMT%2B8换成Asia/Shanghai试试这是另一个常见写法。这条几乎是 JDBC MySQL 8 的必踩坑网上检索量极大我在这里标记一下它出现在任何一篇 MySQL 8 连接教程里都不意外。5.2 日期消失或变成 0000-00-00SimpleDateFormat 格式与表单格式不一致现象新增联系人时页面上input typedate选择生日提交后数据库存的是空值或者0000-00-00。原因SimpleDateFormat(yy-MM-dd)的格式是两位年份而 HTML5 的日期控件提交格式是yyyy-MM-dd比如2024-08-15。yy模式解析时优先取前两位2024-08-15会被解析成20-08-15这种怪异结果如果严格模式下还会直接抛ParseException导致date变量为null。解决把SimpleDateFormat的模式统一改为yyyy-MM-dd同时确保 MySQL 表里date字段允许 NULL 或设置默认值。这个坑隐蔽之处在于本地单独测日期解析可能看不出问题一旦走了 Web 表单浏览器自动格式化的日期字符串就会和yy模式打架。5.3 JSP taglib 无法解析jstl.jar 和 standard.jar 缺失现象部署到 Tomcat 后访问index.jsp报错org.apache.jasper.JasperException: The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved。原因JSP 页面% taglib %声明要使用 JSTL但WEB-INF/lib下没有 JSTL 实现包。Eclipse 的 Dynamic Web Project 不会自动引入这两个 jar。解决下载jstl-1.2.jar新版或jstl.jarstandard.jar旧版放进WEB-INF/lib刷新项目重启 Tomcat。如果你用的 Servlet 容器是 Tomcat 9 以上建议直接上jakarta.servlet.jsp.jstl相关的 3.x 包注意包名有变化。检查方式在 Eclipse 的 Deployment Assembly 里确认 jar 已被发布。5.4 中文乱码response 设置编码的时机太晚现象页面提交中文姓名后数据库里存的是???或者返回的提示信息乱码。原因request.getParameter()拿到的是乱码说明请求体的编码没被正确解码response.setContentType设置在getWriter()之后输出的响应也会乱码。解决在 Servlet 的方法最前面、任何参数读取之前写上这两行request.setCharacterEncoding(utf-8); response.setContentType(text/html;charsetutf-8);同时保证 MySQL 库表字符集是utf8或utf8mb4连接 URL 里有characterEncodingutf8三处一致才能彻底解决中文问题。我用 Navicat 操作时会额外检查表字段的字符集因为建表语句里如果漏了DEFAULT CHARACTER SET utf8表会继承数据库默认字符集而 MySQL 8 的默认字符集是utf8mb4两者混用偶尔会出怪问题。5.5 驱动类找不到mysql-connector jar 没进 WEB-INF/lib现象启动正常一点「新增」按钮就抛java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因jar 只在 Build Path 里被引用没有被发布到WEB-INF/lib。Tomcat 运行时不认 Eclipse 的构建路径配置只认WEB-INF/lib下的物理文件。解决把mysql-connector-java的 jar 文件直接复制到WEB-INF/lib目录刷新项目后重新发布。还有一个隐藏点如果你用的驱动 jar 是 8.x 版本com.mysql.jdbc.Driver这个类名虽然还在但属于兼容保留官方推荐的是com.mysql.cj.jdbc.Driver。原项目用的是旧类名能跑但会打警告日志顺手改成新类名更干净。6. 案例跑通之后验证清单与三条进阶改造思路项目部署到 Tomcat、数据库导入、jar 放齐之后第一件事不是急着写新功能而是把增删改查四个动作完整走一遍并且有意识地制造故障来验证你的理解是否正确。我的验证清单是这样的打开首页http://localhost:8080/项目名/ContactServlet?doindex看列表是否有数据没有数据先走「新增」。点「新增小伙伴」填一条完整记录提交确认页面alert提示新增成功并跳回首列表页。回到列表页确认新记录出现在表格里刷新浏览器再来一次验证数据确实落库。点「修改」看edit.jsp是否把原数据回显在表单里改一个字段提交确认列表里对应字段变化。点「删除」确认记录消失同时到 Navicat 里执行select * from contact验证数据库状态。关闭 Tomcat重启后再访问首页确认数据还在——这一步是验证数据持久化。然后就是进阶改造了。我建议按三条路径选一条走难度递增第一把BaseDao换成连接池版。Druid 或 HikariCP 都行加一个druid.properties配置文件把getConnection改成从连接池拿。改造完成后你再对比原版每次新建连接的方式会非常直观地理解「连接池为什么快」。注意DruidDataSource初始化时要设validationQuery为select 1并且设置合理的空闲连接检测间隔否则连接池可能拿不到连接时报错。第二把do参数分发的 Servlet 改造成 RESTful 风格。原项目用?doadd、?dodel区分动作你可以改成POST /contact新增、PUT /contact修改、DELETE /contact/{id}删除Servlet 3.1 开始支持WebServlet配多个路径。改造过程中你会发现前端表单和链接都要跟着改这一步能帮你理解 HTTP 方法语义。第三把 JDBC 换成 MyBatis 重写一遍。这个跨度最大但你已经有原版做对照了——对比ContactService里的 SQL 和 MyBatis Mapper XML 里的 SQL你会发现 SQL 几乎不用改变的是参数绑定和结果映射方式。面试时你就可以说「我用原生 JDBC 写过完整的增删改查理解参数化查询和连接管理后来用 MyBatis 重构过知道框架解决了什么问题。」这三条路径不需要都走挑一条最贴近你当前目标的。从那以后我每次带新人做这个练习都强制要求他们先把BaseDao里getConnection和closeAll的调用顺序默写一遍再让他们把新增和修改的 SQL 占位符数量数三遍确认无误才放行做页面。这套土办法看起来笨但确实帮不少人避开了参数顺序错乱和资源泄漏的坑。希望帮到你。本文还有配套的精品资源点击获取
返回列表