JDBC直连MySQL通讯录源码:增删改查与Servlet+JSP实战
简介这份PDF资料面向JavaWeb初学者与需要巩固JDBC基础的开发者围绕JDBC连接MySQL实现通讯录的增删改查展开帮助读者打通从数据库连接到前端交互的完整链路。资源包内共1个PDF文件约111KB以图文与代码结合的方式呈现便于对照阅读与动手实践。内容涵盖Eclipse新建Web项目、Tomcat配置、BaseDao封装数据库连接与资源关闭、Contact实体设计、ContactDao实现插入查询更新删除以及Servlet接收前端请求并返回数据的处理流程同时提示了MySQL时区设置等常见报错点。目前已有1880人学习适合作为课程设计、期末作业或入门练手项目的参考读者可据此理解PreparedStatement参数绑定、ResultSet结果集遍历与DAO分层思想并进一步对比ORM框架的差异为后续学习MyBatis或Hibernate打下基础。1. 从一份能跑通的通讯录源码说起JDBC 直连 MySQL 的增删改查到底长什么样很多人第一次接触 JavaWeb 项目卡住的地方不是 Servlet 写不出来而是数据库连不上、页面查不出数据、点删除没反应。这份通讯录案例源码正好覆盖了这条完整链路Eclipse 建 Web 项目、Navicat 建库建表、JDBC 直连 MySQL、Servlet 分发请求、JSP 渲染列表四个动作——新增、修改、删除、查询——全部跑通。它适合刚学完 Servlet 和 JSP、想找一个能直接导入运行的项目练手的人也适合需要快速搭一个 CRUD 骨架做二次开发的人。源码结构不复杂BaseDao 封装连接和释放ContactService 写业务 SQLContactServlet 做请求路由三个 JSP 页面负责展示和表单提交。下面按实际复现顺序拆开讲重点放在参数怎么配、SQL 怎么写、哪里容易翻车。2. 环境搭建与数据库连接从建库到 BaseDao 封装2.1 项目结构与依赖准备在 Eclipse 里新建 Dynamic Web ProjectTarget runtime 选 Tomcat。目录结构按源码来pers.contact.dao放 BaseDaopers.contact.entity放 Contactpers.contact.service放 ContactServicepers.contact.servlet放 ContactServletJSP 放在 WebContent 根目录下。需要额外引入两个 jarjstl.jar和standard.jar否则 index.jsp 里的% taglib urihttp://java.sun.com/jsp/jstl/core prefixc%会直接报错页面连编译都过不去。MySQL 驱动用mysql-connector-java版本要和 MySQL 服务端匹配5.7 用 5.1.x 系列8.0 用 8.0.x 系列放WEB-INF/lib下并加入 Build Path。数据库这边用 Navicat 建一个名为demo的库字符集选 utf8mb4。建表语句按 Contact 实体字段来CREATE TABLE contact ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, age INT, phone VARCHAR(20), date DATE, favorite VARCHAR(100) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;id 设成自增主键插入时不需要传值Contact 构造函数里传 0 占位即可。date 字段用 DATE 类型和 Java 侧的java.util.Date通过rs.getDate(5)映射注意这里用的是java.sql.Date的子类关系直接取不会报错但格式化输出时要注意。2.2 BaseDao 的连接参数与时区坑BaseDao 是整个项目的黑匣子所有数据库操作都从这里走。核心是三个常量加两个方法public class BaseDao { private static final String DRIVER com.mysql.jdbc.Driver; public static final String URL jdbc:mysql://localhost:3306/demo ?rewriteBatchedStatementstrue useUnicodetrue characterEncodingutf8 allowMultiQueriestrue serverTimezoneGMT%2B8; public static final String USER root; public static final String PASSWORD sasa; public void getConnection() { try { Class.forName(DRIVER); conn DriverManager.getConnection(URL, USER, PASSWORD); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } } }URL 里几个参数各有用途。useUnicodetruecharacterEncodingutf8保证中文姓名和爱好不乱码缺一个就可能出现问号。serverTimezoneGMT%2B8是血泪经验——MySQL 安装后默认时区是美国的和本地差 8 小时不设这个参数插入日期时可能报The server time zone value ... is unrecognized或者日期整体偏移一天。rewriteBatchedStatementstrue在批量插入时能提升性能这个项目虽然用不到批量但留着不碍事。allowMultiQueriestrue允许一次执行多条 SQL一般 CRUD 用不上但有些场景需要。executeUpdate和executeQuery两个方法用可变参数Object... obj接收 SQL 占位符的值内部用pstmt.setObject(i1, obj[i])逐个绑定。这里有个细节setObject对日期类型会做自动转换传java.util.Date进去也能写进 DATE 字段但前提是格式能被驱动识别。closeAll()里依次关闭 rs、pstmt、conn顺序不能反先关结果集再关语句再关连接否则可能抛异常。注意closeAll()里对 rs 和 pstmt 的关闭没有判空如果查询还没执行就调用会直接 NPE。实际用的时候建议加if (rs ! null)判断或者至少保证调用顺序正确。3. 增删改查的 SQL 实现ContactService 里的四个方法3.1 查询全部与单条查询GetAllContact()返回ListContactSQL 是select * from contact遍历 ResultSet 时按列序号取值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; }列序号 1 到 6 分别对应 id、name、age、phone、date、favorite和建表顺序一致。用序号而不是列名好处是写起来快坏处是表结构一改就全乱。生产项目里更推荐rs.getString(name)这种写法可读性和抗变更能力都强一些。GetContact(int id)是单条查询SQL 带where id ?返回一个 Contact 对象用于修改前回显数据。3.2 新增、删除与修改新增走AddContact(Contact contact)SQL 是insert into contact(name,age,phone,date,favorite) values(?,?,?,?,?)注意这里没有 id因为数据库自增。参数按顺序传name、age、phone、date、favorite。executeUpdate返回受影响行数大于 0 表示成功。删除是DeleteContact(int id)SQL 为delete from contact where id ?只传一个 id 参数。修改是UpdateContact(Contact contact)SQL 为update contact set name?,age?,phone?,date?,favorite? where id?注意 where 条件的 id 放在最后一位和前面五个 set 参数一起按顺序绑定。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; }四个方法的 SQL 都用了 PreparedStatement 占位符这是防 SQL 注入的基本操作。如果直接把用户输入的 name 拼进 SQL 字符串输入 or 11就能把整张表拖出来。用?占位符之后驱动会自动做转义安全性有保障。提示executeUpdate内部每次调用都会新建连接执行完就关闭。这种短连接模式在并发量低的时候没问题但频繁操作时开销较大。常见做法是引入连接池比如 Druid 或 HikariCP把连接的创建和销毁交给池管理。4. Servlet 请求分发与 JSP 页面渲染从表单到数据库的完整链路4.1 ContactServlet 的 do 参数路由ContactServlet 是整个项目的调度中心所有请求都打到/ContactServlet通过do参数区分动作。doGet直接转调doPost保证 GET 和 POST 都能处理。进入doPost后先设编码response.setContentType(text/html;charsetutf-8); request.setCharacterEncoding(utf-8);这两行必须放在获取任何参数之前否则中文参数会乱码。然后取do参数为空则默认index走首页列表逻辑。index分支调用GetAllContact()拿到列表塞进request.setAttribute(ulist, ulist)然后forward到 index.jsp。add分支从请求里取 name、age、phone、date、favorite 五个参数其中 age 用Integer.parseInt转 intdate 用SimpleDateFormat(yy-MM-dd)解析。这里有个格式约定前端input typedate提交的格式是yyyy-MM-dd而代码里用的是yy-MM-dd两位年份和四位年份在解析时行为不同可能导致日期解析异常或年份错误。稳妥的做法是统一用yyyy-MM-dd。del分支取 id 参数转 int调DeleteContact。editbefore分支先查出当前联系人存进 session 的edituser属性再重定向到 edit.jsp 做回显。edit分支取所有字段加 id调UpdateContact完成修改。每个操作完成后用out.print输出一段 JavaScript 弹窗加跳转回到首页刷新列表。4.2 index.jsp 的 JSTL 遍历与 add/edit 表单index.jsp 开头除了 page 指令还需要 JSTL 的 taglib 声明。页面里有一段小脚本直接调ContactService.GetAllContact()并塞进 request这是为了直接访问 index.jsp 时也能显示数据不依赖 Servlet 转发。表格部分用c:forEach varU items${ulist}遍历每行输出 id、name、age、phone、date、favorite最后一列放修改和删除链接th a hrefContactServlet?doeditbeforeid${U.id}修改/a a hrefContactServlet?dodelid${U.id}删除/a /th修改链接带doeditbefore和 id删除链接带dodel和 id。删除没有二次确认点一下直接删实际用的时候建议加个onclickreturn confirm(确定删除)。add.jsp 是一个纯表单页面actionContactServlet?doaddmethod 为 post。五个输入框分别对应 name、age、phone、date、favorite其中 age 用typenumber限制范围 1 到 133date 用typedate让浏览器弹出日期选择器。所有字段都加了requiredrequired前端做第一层校验。edit.jsp 结构类似但需要从 session 里取edituser回填到输入框的 value 属性提交到ContactServlet?doedit并且多一个隐藏的 id 字段。注意edit.jsp 从 session 取数据回显如果用户直接访问 edit.jsp 而没有经过 editbeforesession 里没有 edituser页面会报空指针。常见做法是在 edit.jsp 开头判断 session 属性是否存在不存在就重定向回首页。5. 避坑与排查这套 JDBC 通讯录最容易翻车的五个地方5.1 驱动类名报 ClassNotFoundException现象启动 Tomcat 后访问页面控制台抛java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因是 MySQL 驱动 jar 没有放进WEB-INF/lib目录或者虽然放了但没有加入 Build Path。Eclipse 里右键项目 → Build Path → Configure Build Path → Libraries → Add JARs选中 lib 下的驱动 jar。如果是 Maven 项目检查 pom.xml 里mysql-connector-java的 scope 是不是 providedprovided 的依赖不会打进 WEB-INF/lib。5.2 时区报错导致连接失败现象The server time zone value ?й???ʱ?? is unrecognized or represents more than one time zone。原因是 MySQL 服务端时区没有正确识别驱动无法确定用哪个时区。解决方式是在 JDBC URL 里显式加serverTimezoneGMT%2B8或者登录 MySQL 执行SET GLOBAL time_zone 8:00永久修改。前者更推荐因为不用动数据库配置换环境也不会丢。5.3 中文乱码从请求到数据库全链路排查现象新增联系人后列表里姓名显示成问号或乱码。排查顺序先看数据库表和字段的字符集是不是 utf8mb4再看 JDBC URL 里有没有useUnicodetruecharacterEncodingutf8然后看 Servlet 里request.setCharacterEncoding(utf-8)有没有放在取参数之前最后看 JSP 页面 pageEncoding 是不是 UTF-8。四个环节任何一个漏了都会乱码按这个顺序查基本能定位。5.4 日期格式不匹配导致 ParseException现象新增时填了日期提交后控制台报java.text.ParseException: Unparseable date。原因是SimpleDateFormat的格式串和实际传入的日期字符串对不上。前端input typedate提交的是2024-01-15这种yyyy-MM-dd格式而代码里写的是yy-MM-dd年份位数不匹配。把格式串改成yyyy-MM-dd即可。另外注意sdf.parse()返回的是java.util.Date而rs.getDate()返回的是java.sql.Date两者在存入数据库时驱动会做转换但格式不对就会抛异常。5.5 删除和修改没有生效但页面提示成功现象点删除后弹窗提示“删除成功”但列表里数据还在。原因是executeUpdate返回的 num 没有被检查SQL 执行了但可能影响 0 行。比如删除时 id 传错、或者 where 条件没匹配到记录。排查时在DeleteContact里打印 num 的值如果为 0 说明 SQL 没匹配到任何行。另一个可能是事务没有提交但 JDBC 默认自动提交除非手动关了setAutoCommit(false)。检查 BaseDao 里有没有误关自动提交。6. 进阶技巧把 BaseDao 改造成可复用的 CRUD 骨架这套代码跑通之后最值得做的一件事是把 BaseDao 从“能用”改成“好用”。原版每次操作都新建连接、用完关闭在本地练手没问题但稍微上点量就扛不住。我一般会做三处改造。第一引入连接池。用 HikariCP 或 Druid 替换DriverManager.getConnectionBaseDao 里只保留一个DataSource静态实例getConnection()从池里取closeAll()把连接还回池里而不是真正关闭。这样并发请求时不会频繁创建 TCP 连接响应速度明显提升。第二把executeUpdate和executeQuery的异常处理改一下。原版直接e.printStackTrace()线上出问题只能看控制台。改成抛自定义的DaoException带上 SQL 语句和参数信息上层 Servlet 捕获后统一处理排查时能直接定位是哪条 SQL 出的问题。第三加一个通用的分页查询方法。通讯录数据量大了之后select * from contact全量查出来再渲染页面会越来越慢。分页 SQL 用limit ?,?传 offset 和 pageSize 两个参数返回当前页数据和总记录数。总记录数用select count(*) from contact单独查一次或者用窗口函数一次查出。这个方法加到 BaseDao 里所有实体类都能复用。public ListContact getByPage(int page, int pageSize) { int offset (page - 1) * pageSize; String sql select * from contact limit ?,?; ResultSet rs executeQuery(sql, offset, pageSize); ListContact list new ArrayList(); try { while (rs.next()) { list.add(new Contact(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getString(4), rs.getDate(5), rs.getString(6))); } } catch (SQLException e) { e.printStackTrace(); } finally { closeAll(); } return list; }验证改造是否成功最简单的办法是写一个 main 方法直接调getByPage(1, 3)看返回的 list 大小是不是 3再调getByPage(2, 3)看第二页数据是否正确。如果返回空或者数量不对检查 offset 计算和数据库里的实际记录数。从那以后我每次拿到一个 JDBC 项目都会先确认三件事驱动 jar 在不在 lib 下、URL 里时区参数有没有加、setCharacterEncoding有没有放在取参数之前。这三步走完八成以上的连接和乱码问题都能提前避开。希望帮到你。本文还有配套的精品资源点击获取