简介:这份资源是面向Java桌面应用开发初学者与课程设计者的通讯录管理系统完整项目源码,基于Java Swing构建图形界面、MySQL负责数据存储,通过JDBC实现联系人信息的增删改查。系统涵盖用户登录注册、联系人添加编辑删除、条件搜索等核心功能,并包含数据模型封装、业务逻辑处理、数据库连接与异常处理等模块,适合作为Java GUI编程与数据库综合实践的参考案例。压缩包共23个文件,约2.16MB,包含5个java源文件与6个class编译文件、5张png界面截图、1个sql建表脚本、1个db数据库文件及jar依赖库等,源码与可运行文件齐备,便于直接导入IDE调试运行。目前已有3701人学习下载,读者可借此理解Swing组件布局、事件监听机制与JDBC操作流程,掌握桌面端通讯录从界面到持久化的完整实现思路,也可在此基础上扩展权限控制、数据加密等功能。
1. Java+Swing+Mysql通讯录管理系统:为什么它是课程设计里最稳的选题
每年毕业季和期末周,总有一批人卡在「通讯录管理系统」这个选题上。不是因为它难,而是因为坑太散:Swing 的表格模型写不对,数据死活刷不出来;Mysql 连上了但中文乱码;增删改查写完了,一关窗口数据全丢。我带过几届学生的课程设计,也帮同事改过类似的小工具,发现一个反直觉的结论——这个系统真正的门槛不在界面,而在「数据流怎么从输入框走到数据库再回到表格」这条链路上。标题里的 Java 负责逻辑、Swing 负责界面、Mysql 负责持久化,三者各司其职,但新手最容易把它们搅在一起写。这篇笔记就按我实际做项目的顺序,把选型理由、建表脚本、Swing 表格绑定、JDBC 封装和几个血泪踩坑点讲清楚。适合正在做课程设计、想快速搭一个能跑能演示的桌面通讯录的人,也适合想复习 JDBC 和 Swing 配合的 Java 入门者。
2. 环境与选型:JDK、Mysql 版本和驱动怎么定
2.1 为什么不用 Spring Boot 而坚持纯 JDBC
很多人第一反应是上 Spring Boot + MyBatis,觉得「企业级」。但通讯录管理系统是个单机桌面工具,没有并发压力,没有分布式事务,引入框架只会让依赖变重、启动变慢、调试变绕。我一般会直接写 JDBC,原因有三:第一,课程设计答辩时老师能看懂每一行 SQL 怎么执行;第二,Swing 的事件线程和数据库操作都在一个进程里,不需要考虑远程调用;第三,出问题时排查路径短,不用翻框架源码。常见做法是把 JDBC 的 Connection、PreparedStatement、ResultSet 封装成一个工具类,业务层只调方法。这样既保留了「手写 SQL」的透明度,又不会让代码里到处是 try-catch-finally。
2.2 Mysql 版本选择与安装避坑
热搜里「mysql安装教程8.0」「mysql 5.7.44 安装过程详细」出现频率很高,说明版本选择确实困扰不少人。我的建议是:如果只是本地做课程设计,Mysql 5.7 和 8.0 都能用,但 8.0 的默认认证插件从 mysql_native_password 改成了 caching_sha2_password,老版本的 JDBC 驱动会报「Unable to load authentication plugin」错误。所以要么用 8.0 配套的驱动,要么在安装时把认证方式改回旧版。安装过程中最容易翻车的是端口被占用和 root 密码策略,建议安装时勾选「Use Legacy Authentication Method」,省去后面改配置的麻烦。
# 检查 3306 端口是否被占用(Windows) netstat -ano | findstr :3306 # 如果被占用,可以在 Mysql 安装向导里改端口,比如 3307 # 安装完成后验证服务是否启动 mysql -u root -p -P 3306上面命令用于确认端口状态和登录验证。参数说明:-u指定用户名,-p表示需要输入密码,-P指定端口(注意大写)。如果登录时报「Access denied」,说明密码不对或认证插件不匹配,需要检查安装时的配置。
2.3 JDBC 驱动版本与连接串写法
驱动版本要和 Mysql 服务端匹配。Mysql 8.0 用mysql-connector-java-8.0.x,5.7 用5.1.x也能跑但建议统一到 8.0。连接串里必须带时区和字符集参数,否则中文会乱码或时间差 8 小时。
// 数据库连接工具类核心片段 public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/addressbook" + "?useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai&useSSL=false"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }逻辑说明:useUnicode=true&characterEncoding=utf8保证中文正常存取;serverTimezone=Asia/Shanghai解决时区导致的日期偏差;useSSL=false在本地开发时关闭 SSL,避免「mysql ssl连接错误」这类报错。参数怎么改:如果数据库在另一台机器,把 localhost 换成对应 IP;如果端口改过,同步改 3306。
3. 建表与数据层:从联系人字段到增删改查 SQL
3.1 通讯录表结构设计
通讯录的核心字段不多,但每个字段的类型和约束要想清楚。我一般会建一张主表,包含自增主键、姓名、电话、邮箱、分组、备注和创建时间。电话和邮箱要允许为空,因为不是每个联系人都填全。分组字段用 varchar 而不是枚举,方便后续扩展。
CREATE TABLE contact ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20), email VARCHAR(100), group_name VARCHAR(30) DEFAULT '默认分组', remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:AUTO_INCREMENT让 id 自动增长,避免手动维护;DEFAULT CURRENT_TIMESTAMP让插入时自动记录时间;utf8mb4比 utf8 更完整,能存 emoji 等特殊字符。参数怎么改:如果要做多用户,加一个 user_id 字段并建索引;如果分组要固定选项,把 group_name 改成 enum 或单独建分组表。
3.2 增删改查的 PreparedStatement 写法
数据层我习惯写一个 ContactDao 类,每个方法对应一条 SQL。用 PreparedStatement 而不是 Statement,一是防 SQL 注入,二是参数多时不用拼字符串。
public class ContactDao { // 新增联系人 public void add(Contact c) throws SQLException { String sql = "INSERT INTO contact(name, phone, email, group_name, remark) VALUES(?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, c.getName()); ps.setString(2, c.getPhone()); ps.setString(3, c.getEmail()); ps.setString(4, c.getGroupName()); ps.setString(5, c.getRemark()); ps.executeUpdate(); } } // 按姓名模糊查询 public List<Contact> searchByName(String keyword) throws SQLException { String sql = "SELECT * FROM contact WHERE name LIKE ?"; List<Contact> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ResultSet rs = ps.executeQuery(); while (rs.next()) { list.add(mapRow(rs)); } } return list; } }逻辑说明:try-with-resources 自动关闭连接和语句,避免资源泄漏;%拼接在参数里而不是 SQL 字符串里,既安全又灵活。参数怎么改:模糊查询的关键词由界面输入框传入;如果要分页,在 SQL 末尾加LIMIT ?,?并传入偏移量和条数。
3.3 事务处理在批量删除中的应用
热搜里「mysql事务处理」是个高频词。通讯录里批量删除多条记录时,要么全成功要么全失败,否则删一半留一半很难看。我一般把自动提交关掉,手动 commit。
public void deleteBatch(List<Integer> ids) throws SQLException { String sql = "DELETE FROM contact WHERE id = ?"; Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 try (PreparedStatement ps = conn.prepareStatement(sql)) { for (Integer id : ids) { ps.setInt(1, id); ps.addBatch(); } ps.executeBatch(); conn.commit(); // 全部成功才提交 } catch (SQLException e) { conn.rollback(); // 任何一条失败就回滚 throw e; } } finally { if (conn != null) conn.close(); } }逻辑说明:setAutoCommit(false)关闭自动提交,executeBatch批量执行,commit统一提交,rollback在异常时撤销。参数怎么改:批量大小可以通过addBatch循环控制,一般几百条以内没问题;如果数据量特别大,分批提交避免锁表。
4. Swing 界面:表格模型、事件绑定和实时刷新
4.1 用 DefaultTableModel 绑定联系人列表
Swing 里显示表格用 JTable,但直接往 JTable 里塞数据很别扭,正确做法是自定义一个继承 DefaultTableModel 的模型类,把 List 转成表格行。这样数据一变,表格跟着刷新。
public class ContactTableModel extends DefaultTableModel { private final String[] columns = {"ID", "姓名", "电话", "邮箱", "分组", "备注"}; private List<Contact> data = new ArrayList<>(); public ContactTableModel() { super(); setColumnIdentifiers(columns); } public void setData(List<Contact> list) { this.data = list; fireTableDataChanged(); // 通知表格重新渲染 } @Override public int getRowCount() { return data == null ? 0 : data.size(); } @Override public Object getValueAt(int row, int col) { Contact c = data.get(row); switch (col) { case 0: return c.getId(); case 1: return c.getName(); case 2: return c.getPhone(); case 3: return c.getEmail(); case 4: return c.getGroupName(); case 5: return c.getRemark(); default: return null; } } @Override public boolean isCellEditable(int row, int col) { return false; // 表格只读,编辑走弹窗 } }逻辑说明:fireTableDataChanged是刷新表格的关键,少了它界面不会更新;isCellEditable返回 false 防止用户直接在表格里改数据导致状态不一致。参数怎么改:列名数组可以按需增减;如果要做排序,在模型里加一个排序方法并在表头点击事件里调用。
4.2 查询按钮的事件绑定与线程安全
Swing 的事件都在事件分发线程(EDT)里执行,如果在按钮点击里直接查数据库,数据量大时界面会卡死。我一般把数据库操作放到 SwingWorker 里,查完再回 EDT 更新表格。
searchBtn.addActionListener(e -> { String keyword = searchField.getText().trim(); new SwingWorker<List<Contact>, Void>() { @Override protected List<Contact> doInBackground() throws Exception { return contactDao.searchByName(keyword); // 后台查库 } @Override protected void done() { try { List<Contact> list = get(); tableModel.setData(list); // 回到 EDT 刷新表格 } catch (Exception ex) { JOptionPane.showMessageDialog(null, "查询失败:" + ex.getMessage()); } } }.execute(); });逻辑说明:doInBackground在后台线程执行耗时操作,done在 EDT 里执行界面更新,避免卡顿和线程冲突。参数怎么改:如果查询很快(本地库几百条),也可以直接同步查,但养成 SwingWorker 的习惯没坏处。
4.3 新增和修改共用一个对话框
新增和修改的界面几乎一样,区别只是有没有初始值。我一般写一个 ContactDialog,构造时传入一个 Contact 对象,为空就是新增,不为空就是修改。这样代码量少一半,维护也方便。
public class ContactDialog extends JDialog { private JTextField nameField = new JTextField(15); private JTextField phoneField = new JTextField(15); private JTextField emailField = new JTextField(15); private JComboBox<String> groupBox = new JComboBox<>(new String[]{"默认分组", "家人", "朋友", "同事"}); private JTextArea remarkArea = new JTextArea(3, 15); private boolean confirmed = false; public ContactDialog(JFrame owner, Contact contact) { super(owner, contact == null ? "新增联系人" : "编辑联系人", true); // 省略布局代码,用 GridBagLayout 排列 if (contact != null) { nameField.setText(contact.getName()); phoneField.setText(contact.getPhone()); emailField.setText(contact.getEmail()); groupBox.setSelectedItem(contact.getGroupName()); remarkArea.setText(contact.getRemark()); } // 确定按钮点击后设置 confirmed = true 并 dispose } public boolean isConfirmed() { return confirmed; } public Contact getContact() { /* 从输入框组装 Contact 对象 */ } }逻辑说明:模态对话框(setModal(true))会阻塞父窗口,保证用户处理完才回到主界面;confirmed标记区分「确定」和「取消」。参数怎么改:分组下拉框的选项可以改成从数据库动态加载;备注用 JTextArea 支持多行。
5. 避坑与排查:中文乱码、连接泄漏和表格不刷新
5.1 中文乱码:从数据库到界面三层都要查
现象:输入「张三」保存后变成「??」或「å¼ ä¸」。原因:字符集在数据库、连接串、Java 文件编码三处不一致。解决:建库时用utf8mb4,连接串加characterEncoding=utf8,Java 源文件保存为 UTF-8,编译时加-encoding UTF-8。如果还乱,检查 Mysql 的character_set_server变量。
5.2 连接泄漏:程序跑久了报「Too many connections」
现象:反复增删改查后,Mysql 报连接数超限。原因:Connection 没有关闭,或者异常路径下跳过了 close。解决:统一用 try-with-resources,或者在 finally 里关闭。我一般会在 DBUtil 里加一个连接池(比如 HikariCP),但课程设计用简单封装也够。
5.3 表格不刷新:数据变了界面没变
现象:新增联系人后数据库里有,但表格还是旧数据。原因:直接改了 List 但没有调用fireTableDataChanged,或者表格模型和实际数据源不是同一个对象。解决:所有数据变更都走模型的 setData 方法,确保触发刷新事件。
5.4 事件线程阻塞:点击查询后界面卡住
现象:查询数据量大时,窗口无法拖动,按钮点不动。原因:数据库操作在 EDT 里执行。解决:用 SwingWorker 把耗时操作放到后台线程,完成后回 EDT 更新界面。
5.5 驱动加载失败:ClassNotFoundException
现象:运行时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因:驱动 jar 没加到 classpath,或者版本不对(8.0 的驱动类名带 cj)。解决:确认 jar 已导入,8.0 用com.mysql.cj.jdbc.Driver,5.7 用com.mysql.jdbc.Driver。
6. 进阶技巧:用表格排序和导出 CSV 提升演示效果
课程设计答辩时,功能多不如演示流畅。我一般会加两个小功能:点击表头排序和导出 CSV。排序用 TableRowSorter,一行代码就能让 JTable 支持点击列头排序。
TableRowSorter<ContactTableModel> sorter = new TableRowSorter<>(tableModel); contactTable.setRowSorter(sorter); // 如果某列是数字,设置比较器避免按字符串排序 sorter.setComparator(0, Comparator.comparingInt(o -> (int) o));逻辑说明:TableRowSorter 自动接管表头点击事件,按列排序;数字列要单独设比较器,否则「10」会排在「2」前面。参数怎么改:如果不想让某列排序,用sorter.setSortable(列索引, false)关掉。
导出 CSV 更简单,用 FileWriter 把表格数据写出去,注意处理逗号和换行。
public void exportCsv(JTable table, File file) throws IOException { try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(file), StandardCharsets.UTF_8))) { bw.write('\ufeff'); // 解决 Excel 打开中文乱码 for (int i = 0; i < table.getColumnCount(); i++) { bw.write(table.getColumnName(i)); if (i < table.getColumnCount() - 1) bw.write(","); } bw.newLine(); for (int r = 0; r < table.getRowCount(); r++) { for (int c = 0; c < table.getColumnCount(); c++) { Object val = table.getValueAt(r, c); String text = val == null ? "" : val.toString().replace(",", ","); bw.write(text); if (c < table.getColumnCount() - 1) bw.write(","); } bw.newLine(); } } }逻辑说明:\ufeff是 BOM 头,让 Excel 正确识别 UTF-8;逗号替换成中文逗号避免 CSV 列错位。参数怎么改:导出路径由 JFileChooser 选择;如果数据量大,可以加分页导出。
这两个功能加起来不到五十行代码,但演示时效果很好。我自己的习惯是:每次做完核心功能,就花半小时加一个「看得见」的小特性,答辩时老师往往就记住这个了。希望帮到你。
本文还有配套的精品资源,点击获取