Java课程设计实战:Swing+JDBC+MySQL通讯录管理系统全解析
简介一份面向Java课程设计的通讯录管理系统源码包适合正在完成课程设计、期末项目或希望通过小型项目巩固编程基础的学习者。系统实现了用户注册与登录、通讯录信息浏览、查找及增删改维护等完整功能采用Swing图形界面与JDBC数据库访问相结合在代码中贯穿了面向对象设计、界面事件处理和模块化拆分等要点。压缩包为rar格式含5个文件4个Java源文件和1个SQL脚本分别对应登录、注册、通讯录管理、数据库工具与数据表建立脚本整体仅8KB结构精简、便于快速阅读。目前已有1562人浏览学习可作为课程设计参考或改造起点。借助这份源码读者能够直观学习Swing组件的布局与监听、JDBC增删改查、数据库连接与表设计等核心知识并基于现有框架继续添加功能或优化界面。1. 通讯录管理系统课设选题里最不起眼、练基本功最狠的一个期末课程设计选了通讯录管理系统同组有人做图像识别有人做网页商城我当时觉得这题太简单两晚上就能搞定。真正动手才发现这个题目把 java 课程设计里该考的东西全串了起来Swing 写桌面界面、JDBC 连 MySQL、PreparedStatement 防注入、事务控制、JTable 数据刷新甚至连分组的关联删除都要处理外键约束。它适合两类人一是课设抽到这个题、需要一个能跑能答辩的完整源码做参考的在校生二是想快速熟悉桌面应用从界面到数据库完整链路的人——项目不大但每个环节都不是糊弄人的玩具代码。2. 系统全貌与技术选型Swing、JDBC、MySQL 是怎么定下来的2.1 功能清单课设要交的活儿一共几件先说清楚这个系统到底要做什么。核心功能围绕联系人展开新增联系人、修改联系人、删除联系人、按姓名或电话搜索这是最基础的增删改查扩展功能是联系人分组可以新建分组、删除分组、把联系人归到某个分组里。界面展示用表格形式列出所有联系人点击某一行能回填到表单里进行编辑。功能模块具体说明涉及技术点联系人管理新增、修改、删除、清空列表PreparedStatement、JTable搜索按姓名或手机号模糊查询LIKE 拼接、动态 SQL分组管理新建分组、删除分组、联系人归类外键关联、级联删除数据展示表格分页展示联系人及所属分组DefaultTableModel数据持久化重启后数据不丢失MySQL 数据库存储这套功能做下来一个学期学的 Java SE 知识基本覆盖了大半。重点是它不像有些课设题目那样只考界面或者只考数据库而是把两者通过 JDBC 完整地连在一起答辩时老师问的“数据是怎么从界面走到数据库的”这个问题整条链路都能答上来。2.2 技术选型为什么这个组合最稳技术栈是三个Swing、JDBC、MySQL。很多人在选型时纠结要不要用 JavaFX或者用 MyBatis 替代 JDBC但课程设计的场景下这个组合是经过验证的稳妥选择。Swing 是 JDK 自带的图形界面库不需要额外引入框架用 Eclipse 导出可运行的 Jar 包就能在装了 JRE 的机器上跑起来避免了 JavaFX 在打包和运行环境上的额外配置。JDBC 是 Java 访问数据库的标准接口虽然代码写起来比 MyBatis 啰嗦但恰恰能体现“手写 SQL、手动管理连接和事务”的基本功答辩时有东西可讲。MySQL 则是教学环境里最常见的数据库安装包好找可视化工具齐全。2.3 目录结构解析五个包各管什么项目结构采用经典的三层划分再拆细一层一共五个包目录树如下src/ ├── com/addressbook/entity/ // 实体类Contact、ContactGroup ├── com/addressbook/dao/ // 数据访问层ContactDao、GroupDao、接口定义 ├── com/addressbook/service/ // 业务逻辑层ContactService、GroupService ├── com/addressbook/ui/ // 界面层MainFrame、ContactPanel、GroupPanel ├── com/addressbook/util/ // 工具类DBUtil、StringUtil └── Main.java // 程序入口entity 包存放实体类字段和数据库表字段一一对应Contact 里有 id、name、phone、email、groupId 和用于展示的 groupNamedao 包负责所有 SQL 操作每个方法只干一件事比如 findById、insert、update、deleteservice 包在 dao 之上做业务校验和事务控制比如删除分组前检查联系人是否还挂在这个分组下ui 包是 Swing 界面监听按钮事件后调用 serviceutil 包放数据库连接工具类。分层的好处是各层职责单一界面不管 SQLDAO 不管界面显示将来想给系统加一个 Web 接口只需要在 service 层之上再接一层 Controller已有的业务代码不需要改动。2.4 主界面交互从一个按钮点击到数据库变更主界面的交互链路是理解这个项目最关键的线索。用户在文本框里输入姓名、电话点击“新增”按钮后Swing 的事件监听器会触发 actionPerformed 方法先做非空校验然后调用 ContactService 的新增方法service 层再把数据传给 ContactDao 执行 INSERT 语句语句执行成功后回到 UI 层刷新表格。这条链路里最容易出问题的是最后一步“刷新表格”。初版代码在 insert 执行完后没有重新加载数据用户点完“新增”看不到任何变化还以为是数据库没写进去。实际上数据已经入库了只是表格模型没有更新。这个问题在后面避坑章节会展开讲它是一个典型的“代码没报错但功能不对”的坑。3. 核心模块拆解一个联系人从输入框写进 MySQL 表的完整链路3.1 建表 SQL分组的关联关系怎么设计数据库起名 address_book两张表联系人表 contact 和分组表 contact_group。分组表在前联系人表在后因为联系人表的外键要引用分组表的主键。CREATE DATABASE address_book DEFAULT CHARACTER SET utf8mb4; CREATE TABLE contact_group ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE contact ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, phone VARCHAR(20) DEFAULT NULL, email VARCHAR(100) DEFAULT NULL, group_id INT DEFAULT NULL, FOREIGN KEY (group_id) REFERENCES contact_group(id) ON DELETE SET NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;联系人表里的 group_id 关联分组表主键外键约束指定了 ON DELETE SET NULL意思是分组被删除后属于该分组的联系人不会跟着被删而是把 group_id 置为 NULL变成“未分组”状态。这个设计比 ON DELETE CASCADE 更安全避免误删分组导致联系人数据连带丢失。建库时指定 utf8mb4 字符集是关键一步——MySQL 8.0 默认就是 utf8mb4MySQL 5.7 如果不手动指定建出来的表默认是 latin1后面插中文就会出乱码或者报 Incorrect string value 错误。3.2 DBUtil 工具类连接串里的三个关键参数数据库连接是所有 DAO 操作的地基这个工具类写得好不好直接影响项目能不能跑起来。package com.addressbook.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { // 第三、四个参数决定你是否能连上数据库 private static final String URL jdbc:mysql://localhost:3306/address_book ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; 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(MySQL驱动加载失败检查lib目录下是否有jar包); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }连接串里三个参数每个都有讲究。useSSLfalse 是因为本地开发环境不需要 SSL 加密连接不加这个参数 MySQL 8.0 会输出一大堆 SSL 警告serverTimezoneAsia/Shanghai 是因为 MySQL 8.0 的驱动要求显式指定时区否则报 CST 时区识别不了的错误characterEncodingutf8 保证中文以 UTF-8 编码传输到数据库和建库时的 utf8mb4 对齐。USER 和 PASSWORD 是本地 MySQL 的账号密码公共源码解压后基本都要改成自己机器上的配置否则会看到 Access denied for user 的报错。3.3 ContactDaoPreparedStatement 怎么写出安全的新增和查询DAO 层是整个项目代码量最重的地方拿查询全部联系人的方法举例package com.addressbook.dao; import com.addressbook.entity.Contact; import com.addressbook.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class ContactDao { public ListContact findAll() { // 左连接查出分组名未分组的联系人 groupName 为 null String sql SELECT c.id, c.name, c.phone, c.email, c.group_id, g.name AS group_name FROM contact c LEFT JOIN contact_group g ON c.group_id g.id ORDER BY c.id; ListContact list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Contact c new Contact(); c.setId(rs.getInt(id)); c.setName(rs.getString(name)); c.setPhone(rs.getString(phone)); c.setEmail(rs.getString(email)); c.setGroupId(rs.getInt(group_id)); c.setGroupName(rs.getString(group_name)); list.add(c); } } catch (SQLException e) { e.printStackTrace(); } return list; } public int insert(Contact contact) throws SQLException { String sql INSERT INTO contact (name, phone, email, group_id) VALUES (?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, contact.getName()); ps.setString(2, contact.getPhone()); ps.setString(3, contact.getEmail()); if (contact.getGroupId() ! null) { ps.setInt(4, contact.getGroupId()); } else { ps.setNull(4, Types.INTEGER); } return ps.executeUpdate(); // 返回受影响行数1 表示插入成功 } } }Java 7 的 try-with-resources 写法让 Connection、PreparedStatement、ResultSet 自动关闭不用手写 finally 块。这里有两个要点一是 SQL 全部用占位符 ? 传参杜绝字符串拼接 SQL从根上防住 SQL 注入二是 groupId 为 null 时必须调用 ps.setNull(4, Types.INTEGER)如果直接传 null 给 setInt会报“Column Index out of range”或者类型转换异常。findAll 方法里的 LEFT JOIN 很关键。如果联系人没有分组group_id 是 NULL普通 INNER JOIN 会把这条记录丢掉左连接保证所有联系人都在列表里显示只是分组列显示为空。3.4 Service 层删除分组时的事务控制DAO 只管单条 SQL跨表的业务操作就要交给 Service 层来管事务。删除分组的场景是典型例子如果直接把分组删了联系人表里指向这个分组的 group_id 成了悬空引用虽然外键的 ON DELETE SET NULL 会兜底但这里用事务演示更规范的做法。package com.addressbook.service; import com.addressbook.dao.ContactDao; import com.addressbook.dao.GroupDao; import com.addressbook.util.DBUtil; import java.sql.Connection; import java.sql.SQLException; public class GroupService { private ContactDao contactDao new ContactDao(); private GroupDao groupDao new GroupDao(); public void deleteGroup(int groupId) throws SQLException { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 // 先把该分组下的联系人置为未分组 contactDao.clearGroupId(conn, groupId); // 再删除分组记录 groupDao.deleteById(conn, groupId); conn.commit(); // 全部成功才提交 } catch (SQLException e) { if (conn ! null) { conn.rollback(); // 任何一步失败整体回滚 } throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } } }注意 clearGroupId 和 deleteById 这两个 DAO 方法都额外接收一个 Connection 参数这是事务控制的关键——只有用同一个连接执行的两条 SQL 才能在同一个事务里提交或回滚。如果每个 DAO 方法内部自己通过 DBUtil.getConnection() 拿连接事务是控制不住的每条 SQL 各自提交回滚无从谈起。这样设计的专业点在于先解除联系人和分组的关系再删分组顺序反了就会因为外键约束直接报错。即使顺序写对了事务也保证了两个操作要么同时成功要么数据库回到删除前的状态。3.5 界面层JTable 刷新和 DefaultTableModel 的配合Swing 界面用 JTable 展示联系人列表数据存在 DefaultTableModel 里。新增或修改联系人后界面要重新从数据库拉数据并刷新表格private void refreshTable() { ListContact list contactService.getAllContacts(); // 列名编号、姓名、电话、邮箱、分组 String[] columns {编号, 姓名, 电话, 邮箱, 分组}; Object[][] data new Object[list.size()][5]; for (int i 0; i list.size(); i) { Contact c list.get(i); data[i][0] c.getId(); data[i][1] c.getName(); data[i][2] c.getPhone(); data[i][3] c.getEmail(); data[i][4] c.getGroupName() null ? 未分组 : c.getGroupName(); } table.setModel(new DefaultTableModel(data, columns)); }这里的逻辑是把 ListContact 转换成 JTable 能识别的二维数组和列名数组然后重新 setModel。每次刷新都创建新的 DefaultTableModel 实例简单粗暴但有效。初版踩过的坑是复用同一个 model 实例调 model.setRowCount(0) 清空数据再 addRow 逐行添加结果因为没触发 fireTableDataChanged 导致界面残影后来改成直接换新 model 就彻底解决了。表格刷新在新增、修改、删除和搜索四个操作后都要调用你可以把它理解为整个 UI 层的“最终出口”所有数据变更的反馈都体现在表格重绘上。4. 项目运行与部署从 Eclipse 导入到打包提交的完整流程4.1 环境准备JDK、MySQL、驱动包版本怎么配先把环境跑通再谈项目。推荐 JDK 8 或者 JDK 11两者都能编译运行这个项目不要用 JDK 17 以上部分老版本 Eclipse 对高版本 JDK 支持不好会出现编译参数报错。MySQL 用 5.7 或 8.0 都行8.0 更省事因为驱动和连接串都是按 8.0 标准写的。MySQL 驱动 jar 包是运行时最容易缺的东西。项目 lib 目录下需要放一个 mysql-connector-java 驱动包MySQL 8.0 对应的是 mysql-connector-java-8.0.x.jar。驱动包下载后要手动加入项目的 Build Path后面会讲操作步骤。4.2 导入工程解压、配置 Build Path、改数据库账号拿到源码压缩包后解压到一个不含中文和空格的路径比如 D:\workspace\addressbook避免编码和路径解析问题。Eclipse 里选择 File → Import → Existing Projects into Workspace选中解压后的目录项目会自动出现在包资源管理器里。导入后右键项目名 → Build Path → Configure Build Path → Libraries找到 lib 目录下的 mysql-connector-java 驱动包点击 Add JARs 把它加进来。没有这一步运行时会报 ClassNotFoundException: com.mysql.cj.jdbc.Driver。数据库账号配置在 DBUtil.java 里。把 USER 和 PASSWORD 改成自己 MySQL 的用户名和密码如果是 MySQL 5.7 并且驱动用的 5.1.x 旧版本连接串里的 com.mysql.cj.jdbc.Driver 要改回 com.mysql.jdbc.DriverserverTimezone 参数也要去掉。4.3 启动流程建库、初始化数据、运行 Main 入口启动分三步顺序错了会报各种奇怪的连接错误。第一步在 MySQL 里执行建库建表脚本用命令行或者 Navicat 都行执行 3.1 节那两段 SQL第二步确认 MySQL 服务已启动第三步运行 Main.java 的 main 方法。想要初始界面有数据可以手动执行几条 INSERT 语句往表里塞几条测试数据比如插入一个“同学”分组再插入几个联系人都归到这个分组下。这样第一次启动就能看到表格里有内容方便验证查询和分组功能是否正常。INSERT INTO contact_group (name) VALUES (同学), (同事); INSERT INTO contact (name, phone, email, group_id) VALUES (张三, 13800001111, zhangsanexample.com, 1);4.4 打包发布课程设计提交时的两个常见翻车点答辩或提交源码前很多同学想把项目打包成可执行的 Jar 包。导出时两个坑最常见一是打包后双击 Jar 没反应这是因为 UI 用的是 Swing启动入口在 Main 类里Eclipse 导出时需要指定 Main class二是双击后提示找不到数据库驱动因为驱动 Jar 没有和项目一起打包进去。第一个问题在 Fat Jar 导出时钩选 Launch configuration 选 Main 类即可。第二个问题比较麻烦Eclipse 自带的导出功能不会把依赖 Jar 打进同一个包里两个办法一是交源码时让评审老师用 Eclipse 导入运行lib 目录下保留驱动包即可二是用 Fat Jar 插件导出带依赖的可执行包。课程设计的场景下我建议直接交源码写清楚运行步骤反而比交一个打不好的 Jar 包更稳妥。5. 课程设计避坑指南源码运行中最常见的五个报错场景5.1 Access denied 和连接超时先别怀疑代码查这三处现象启动程序后点任何按钮控制台输出 Access denied for user rootlocalhost或者 Communications link failure 连接超时。原因Access denied 是账号密码错误或者 MySQL 里 root 账号只允许本机 localhost 登录而代码连的地址不是 localhost连接超时一般是 MySQL 服务没启动或者连接串里 localhost:3306 端口和实际端口不一致。解决第一步确认 MySQL 服务已启动Windows 下用net start mysql或服务管理器查看第二步核对 DBUtil 里的 USER 和 PASSWORD 与本地 MySQL 实际账号一致第三步检查地址不要写成远程 IPlocalhost和127.0.0.1在 MySQL 权限表里是两套账号用哪个地址就得有这个地址的授权。5.2 中文乱码从建库到连接串的一连串设置现象表格里联系人姓名显示成问号或者新增中文时报 Incorrect string value: \xE5\xBC\xA0... for column name。原因字符集三层没对齐。数据库和表不是 utf8/utf8mb4、连接串没带 characterEncodingutf8、MySQL 服务端默认字符集不是 UTF-8这三层里任何一层不对中文都会在某一环节变成乱码或者直接写入失败。解决按顺序检查。建库 SQL 加DEFAULT CHARACTER SET utf8mb4连接串 URL 加characterEncodingutf8如果还不行修改 MySQL 配置文件 my.ini 里的character-set-serverutf8mb4并重启服务。课程设计场景里最常见的是前两层第一层解决了九成乱码问题。5.3 新增联系人后表格纹丝不动JTable 模型更新时机现象点“新增”按钮界面没有任何变化但查数据库记录已经插入成功。原因insert 方法执行成功但 UI 层没有重新调用 refreshTable。初版逻辑里把刷新表格的代码写在了按钮监听器的开头SQL 还没执行完就先刷新了一次自然看不到新数据。解决把 refreshTable() 的调用放在数据操作成功返回之后也就是 actionPerformed 里先调用 service 层方法判断返回值大于 0再刷新表格。每次新增、修改、删除后都必须调用一次这是 UI 和数据库之间的“数据同步时刻”漏一个操作就出现一次数据对不上的幻觉。5.4 ClassNotFoundException驱动包没放进 Build Path现象启动程序控制台第一行就报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因lib 目录下有 jar 包但没有加到 Eclipse 的 Build PathJava 运行时根本看不到这个类。很多新手以为包在项目目录里就等于被引用了Eclipse 里 jar 文件图标上有一个小写 J 字母的才是被引用的状态普通文件图标表示它只是个普通资源文件。解决右键项目 → Build Path → Configure Build Path → Libraries → Add JARs选中 lib 下的 mysql-connector-java 驱动包确认图标变化后重新运行。打包 Jar 时同理需要把驱动包一并处理否则换台机器运行照样找不到 Driver。5.5 删除分组报外键约束错误业务逻辑和数据库约束的冲突现象删除一个还有联系人的分组时控制台报Cannot delete or update a parent row: a foreign key constraint fails。原因contact 表里还有记录的 group_id 指向这个分组外键约束不允许直接删掉被引用的父记录。数据库层面虽然建表时写了 ON DELETE SET NULL但如果代码用的是普通 DELETE 语句不同版本的 MySQL 对约束处理行为不完全一致而且业务上先删子数据再删父数据是更规范的操作顺序。解决在 Service 层先执行UPDATE contact SET group_id NULL WHERE group_id ?把该分组下所有联系人置为未分组再执行 DELETE 删除分组。这也是 3.4 节事务代码里做的事——先清关系再删主体两条 SQL 放在一个事务里中间任何一步失败都能回滚到删除前的状态。6. 二次开发改造给通讯录加上模糊搜索和一键导出6.1 扩展思路多条件搜索和导出文件这样落地拿到一份能跑的课设源码最有价值的改造是在现有分层上加功能而不是重写。这里演示两个实用扩展把单一查询改成姓名或电话模糊搜索再加一个联系人列表导出到文本文件的功能。搜索的改动落在 DAO 层。加一个 searchByKeyword 方法SQL 里用 LIKE 拼接关键字姓名、电话、邮箱三个字段任意匹配public ListContact searchByKeyword(String keyword) { String sql SELECT c.id, c.name, c.phone, c.email, g.name AS group_name FROM contact c LEFT JOIN contact_group g ON c.group_id g.id WHERE c.name LIKE ? OR c.phone LIKE ? OR c.email LIKE ? ORDER BY c.id; ListContact list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { String like % keyword %; ps.setString(1, like); ps.setString(2, like); ps.setString(3, like); ResultSet rs ps.executeQuery(); while (rs.next()) { // 封装逻辑同 findAll省略 } // 查询永远返回一个新的空 List而不是 null return list; } catch (SQLException e) { e.printStackTrace(); } return list; }参数里用%包裹关键字实现模糊匹配setString(1, like)对三个字段分别设同一个值。注意ps.setString会自动处理特殊字符转义,不要手动去拼接单引号那会把防注入的遮羞布撕掉。搜索框为空时返回全部联系人相当于空关键字走全表查询这层判断放在 Service 层做。导出功能可以放在 UI 层写一个工具方法用 JFileChooser 让用户选保存位置然后遍历当前表格数据写入 CSV 格式文本文件private void exportToFile() { JFileChooser chooser new JFileChooser(); chooser.setSelectedFile(new File(contacts_ System.currentTimeMillis() .csv)); int result chooser.showSaveDialog(this); if (result ! JFileChooser.APPROVE_OPTION) { return; } try (FileWriter writer new FileWriter(chooser.getSelectedFile(), StandardCharsets.UTF_8)) { DefaultTableModel model (DefaultTableModel) table.getModel(); for (int i 0; i model.getRowCount(); i) { StringBuilder line new StringBuilder(); for (int j 0; j model.getColumnCount(); j) { line.append(model.getValueAt(i, j)).append(,); } writer.write(line.substring(0, line.length() - 1) \n); } JOptionPane.showMessageDialog(this, 导出成功); } catch (IOException e) { JOptionPane.showMessageDialog(this, 导出失败 e.getMessage()); } }改造验证的方法是导入几条数据在不同分组间移动联系人删除一个分组确认联系人变成未分组再点导出看文件内容是否完整。这个流程走完数据从 MySQL 到界面、从界面到本地文件的整条链路都验证过了二次开发的价值就落地了。那次之后我每次改完数据访问层或界面层代码都会强制把新增、修改、删除、搜索、导出这五步完整操作走一遍再提交这套回归顺序帮我挡掉了好几回躲在角落里的低级错误。希望帮到你。本文还有配套的精品资源点击获取