简介:本资源是一个基于Java Swing与MySQL开发的桌面端通讯录管理系统,面向Java初学者及Swing GUI开发入门者,解决联系人信息的增删改查、本地持久化存储与可视化交互管理问题。项目采用标准MVC分层结构,包含6个编译后的class文件、5个Java源码(含Contact实体类、GUI主界面AddressSwing等)、1个MySQL数据库文件(.db)及配套SQL建表脚本,辅以5张界面图标png、工程配置文件(.project、.classpath)和运行依赖jar包,共23个文件,总大小2.16MB。已有3701人学习下载,资源结构完整,目录清晰呈现src源码、bin编译输出、sql数据库脚本、lib依赖库及image资源路径,开箱即可导入IDE运行调试。读者可直接复用核心JDBC连接逻辑、Swing事件驱动设计模式与表格数据绑定方案,深入理解Java桌面应用从界面构建、业务封装到数据库交互的全流程实现。
1. Java+Swing+MySQL通讯录管理系统:不是“古董技术”,而是验证Java工程能力的黄金练手项目
你可能在招聘JD里见过“熟悉Swing开发”被列为加分项,也可能在面试时被问到“如果不用Spring Boot,怎么从零搭一个带数据库交互的桌面应用?”——这恰恰是Java+Swing+MySQL通讯录管理系统存在的真实价值:它不追求高并发或云原生,而是一套可闭环验证Java核心能力的最小可信系统。它要求你亲手处理JDBC连接池、Swing事件线程与UI更新的同步、MySQL字符集与时间戳兼容性、CRUD操作中的SQL注入防护边界、以及Swing组件状态与数据模型的双向绑定逻辑。新手能靠它打通“写代码→编译→运行→连库→存查改”全链路;老手则常拿它当“压力测试场”:比如把10万条联系人导入后观察Swing TableModel内存泄漏点,或故意用setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)配合未关闭的Connection触发资源泄露——这些血泪经验,在Spring Boot自动配置的黑匣子下反而难复现。本文不讲“为什么Swing过时”,只聚焦:如何用最简路径跑通、调稳、扩出可用性,且每一步都经得起生产级追问。
2. 环境筑基:JDK、MySQL、IDE三件套的精准版本锚定与避坑配置
2.1 JDK选型:为什么坚持用JDK 8u391而非JDK 17+?
Swing在JDK 9+中虽未废弃,但JTable的默认渲染器对java.time.LocalDate支持不完整,JDatePicker等第三方组件在JDK 17上会因模块化(Module System)报IllegalAccessError。实测JDK 8u391(LTS)+ MySQL 8.0.33组合最稳定。安装后验证:
java -version # 输出必须含 "1.8.0_391" 且无 "openjdk" 字样(OpenJDK部分Swing字体渲染异常) javac -version # 必须与java版本一致提示:若用IntelliJ IDEA,需在Project Structure → Project → Project SDK中手动指定JDK 8路径,而非依赖IDE自动检测——自动检测常误选JBR(JetBrains Runtime),其Swing DPI缩放逻辑与标准JDK不同,导致Windows高分屏下按钮文字错位。
2.2 MySQL安装与通讯录专用库初始化
避开MySQL Installer图形向导(易残留服务冲突),用命令行静默安装:
# Windows下解压mysql-8.0.33-winx64.zip到D:\mysql # 创建my.ini(关键!必须包含以下三段) [mysqld] port=3306 character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci default-time-zone='+08:00' # 必须加此行,否则Java读取DATETIME字段时区偏移为0导致时间错乱 [client] default-character-set=utf8mb4 [mysql] default-character-set=utf8mb4启动服务后执行建库脚本:
CREATE DATABASE contact_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE contact_db; CREATE TABLE contacts ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, email VARCHAR(100), address TEXT, birthday DATE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB;参数说明:
utf8mb4支持emoji(如联系人昵称含😊),DEFAULT CURRENT_TIMESTAMP ON UPDATE确保Java层无需手动维护更新时间,BIGINT避免未来联系人超千万时ID溢出。
2.3 IDE工程结构:拒绝“一个.java文件打天下”
按MVC分层创建包结构:
src/ ├── main/ │ ├── java/ │ │ ├── model/ # Contact实体类(含getter/setter/toString) │ │ ├── dao/ # ContactDao接口及MySQL实现类(含JDBC连接管理) │ │ ├── service/ # ContactService业务逻辑(含输入校验、事务控制) │ │ └── ui/ # Swing界面类(ContactFrame主窗体、ContactDialog新增窗体) │ └── resources/ │ └── db.properties # 数据库连接参数(driver,url,user,password) └── test/ # JUnit测试用例(重点测DAO层SQL执行)关键设计:
db.properties必须外置(不打包进jar),便于部署时修改密码;ContactDao中getConnection()方法需用ThreadLocal<Connection>避免多线程争抢连接——Swing事件队列虽单线程,但用户可能开多个窗口触发并发操作。
3. 核心编码:从Contact实体到Swing界面的逐层贯通
3.1 Contact实体类:为什么必须重写equals()和hashCode()?
public class Contact { private Long id; private String name; private String phone; private String email; private String address; private LocalDate birthday; // 注意:用java.time.LocalDate而非java.util.Date // 构造函数、getter/setter省略... @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Contact contact = (Contact) o; // 仅比较业务字段,忽略id(新增时id为null,更新时id已存在) return Objects.equals(name, contact.name) && Objects.equals(phone, contact.phone) && Objects.equals(email, contact.email); } @Override public int hashCode() { return Objects.hash(name, phone, email); // 同样不包含id } }逻辑说明:Swing表格编辑后需判断“是否真有修改”,若直接用
==比较对象引用必错;hashCode()用于HashSet去重(如批量导入时过滤重复联系人)。LocalDate替代Date避免时区转换错误——MySQLDATE类型与LocalDate天然映射,无需SimpleDateFormat解析。
3.2 ContactDao实现:JDBC连接池与SQL注入防护的硬核写法
public class ContactDaoImpl implements ContactDao { private static final String INSERT_SQL = "INSERT INTO contacts (name,phone,email,address,birthday) VALUES (?,?,?,?,?)"; private static final String SELECT_ALL_SQL = "SELECT id,name,phone,email,address,birthday,created_at,updated_at FROM contacts ORDER BY created_at DESC"; @Override public void save(Contact contact) throws SQLException { try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(INSERT_SQL, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, contact.getName()); ps.setString(2, contact.getPhone()); ps.setString(3, contact.getEmail()); ps.setString(4, contact.getAddress()); ps.setObject(5, contact.getBirthday()); // setObject自动适配LocalDate ps.executeUpdate(); // 获取自增ID并回填到contact对象 try (ResultSet rs = ps.getGeneratedKeys()) { if (rs.next()) { contact.setId(rs.getLong(1)); } } } } @Override public List<Contact> findAll() throws SQLException { List<Contact> contacts = new ArrayList<>(); try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(SELECT_ALL_SQL); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Contact c = new Contact(); c.setId(rs.getLong("id")); c.setName(rs.getString("name")); c.setPhone(rs.getString("phone")); c.setEmail(rs.getString("email")); c.setAddress(rs.getString("address")); c.setBirthday(rs.getObject("birthday", LocalDate.class)); // 关键!用getObject而非getDate c.setCreatedAt(rs.getTimestamp("created_at").toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime()); c.setUpdatedAt(rs.getTimestamp("updated_at").toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime()); contacts.add(c); } } return contacts; } }参数说明:
ps.setObject(5, contact.getBirthday())比ps.setDate(5, Date.valueOf(contact.getBirthday()))更安全,避免java.sql.Date时区转换;rs.getObject("birthday", LocalDate.class)是JDBC 4.2+标准写法,杜绝rs.getDate()返回java.sql.Date再转LocalDate的精度丢失。
3.3 Swing主界面:JTable与TableModel的动态绑定实战
public class ContactFrame extends JFrame { private final ContactTableModel tableModel; private final JTable contactTable; public ContactFrame() { setTitle("Java通讯录管理系统"); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); // 防止误点关闭丢失未保存数据 setLayout(new BorderLayout()); // 初始化TableModel(继承AbstractTableModel) tableModel = new ContactTableModel(); contactTable = new JTable(tableModel); contactTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // 列宽自适应(Swing经典痛点) TableColumnModel columnModel = contactTable.getColumnModel(); columnModel.getColumn(0).setPreferredWidth(50); // ID列窄 columnModel.getColumn(1).setPreferredWidth(120); // 姓名列宽 columnModel.getColumn(2).setPreferredWidth(100); // 手机列宽 add(new JScrollPane(contactTable), BorderLayout.CENTER); add(createButtonPanel(), BorderLayout.SOUTH); loadContacts(); // 启动时加载数据 pack(); setLocationRelativeTo(null); } private void loadContacts() { try { List<Contact> contacts = contactService.findAll(); tableModel.setContacts(contacts); // 触发fireTableDataChanged() } catch (SQLException e) { JOptionPane.showMessageDialog(this, "加载联系人失败:" + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } // ... 其他按钮事件处理方法(add/update/delete) }关键细节:
JFrame.DO_NOTHING_ON_CLOSE强制用户通过菜单或按钮退出,避免EXIT_ON_CLOSE直接杀进程导致Connection未关闭;setPreferredWidth()比setAutoResizeMode(JTable.AUTO_RESIZE_OFF)更可控,防止列宽被拉伸到不可读。
4. 避坑指南:Swing+MySQL通讯录开发中90%开发者踩过的5个深坑
4.1 现象:Swing界面卡死,鼠标变成沙漏,但CPU占用率极低
原因:在Swing事件分发线程(EDT)中执行耗时的JDBC操作(如contactService.findAll()),阻塞了UI刷新。Swing所有UI更新必须在EDT,但数据库IO不能在EDT。
解决:用SwingWorker异步加载数据:
new SwingWorker<List<Contact>, Void>() { @Override protected List<Contact> doInBackground() throws Exception { return contactService.findAll(); // 在后台线程执行 } @Override protected void done() { try { List<Contact> contacts = get(); tableModel.setContacts(contacts); } catch (Exception e) { JOptionPane.showMessageDialog(ContactFrame.this, "加载失败:" + e.getMessage()); } } }.execute();4.2 现象:中文姓名存入MySQL后变成乱码(如“张三”显示为“??”)
原因:MySQL客户端连接未声明字符集,或JDBC URL缺少useUnicode=true&characterEncoding=utf8mb4参数。
解决:db.properties中URL必须为:
jdbc.url=jdbc:mysql://localhost:3306/contact_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=GMT%2B8注意:
serverTimezone=GMT%2B8是URL编码后的GMT+8,若写成serverTimezone=GMT+8会因+被解析为空格导致连接失败。
4.3 现象:新增联系人后,JTable不刷新,但数据库已写入
原因:ContactTableModel未正确调用fireTableRowsInserted()或fireTableDataChanged(),或setContacts()方法中直接替换List引用却未通知视图。
解决:ContactTableModel.setContacts()必须:
public void setContacts(List<Contact> contacts) { this.contacts.clear(); this.contacts.addAll(contacts); fireTableDataChanged(); // 或 fireTableRowsInserted(0, contacts.size()-1) }4.4 现象:删除联系人后,再次新增时ID不连续(如删掉ID=3,新记录ID=4而非3)
原因:MySQLAUTO_INCREMENT机制本身不重用已删除ID,这是正常行为。但用户期望“逻辑序号连续”。
解决:不修复。向用户说明这是数据库设计原则(保证主键唯一性),若强行ALTER TABLE contacts AUTO_INCREMENT=1会导致并发插入失败。可在UI显示时用tableModel.getRowCount()生成序号列,而非依赖数据库ID。
4.5 现象:JDialog弹出后,主窗体仍可点击(模态失效)
原因:JDialog构造时未传入父Frame,或setModal(true)调用位置错误(必须在setVisible(true)前)。
解决:
ContactDialog dialog = new ContactDialog(ContactFrame.this); // 传入this作为owner dialog.setModal(true); dialog.setVisible(true); // setVisible必须在setModal之后5. 进阶实战:让通讯录系统真正可用的3个关键增强
5.1 搜索功能:用PreparedStatement实现模糊查询防注入
// ContactDaoImpl中新增方法 @Override public List<Contact> search(String keyword) throws SQLException { String sql = "SELECT id,name,phone,email,address,birthday,created_at,updated_at " + "FROM contacts WHERE name LIKE ? OR phone LIKE ? OR email LIKE ?"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { String likeKeyword = "%" + keyword + "%"; // 用户输入直接拼接LIKE,但用?占位符防注入 ps.setString(1, likeKeyword); ps.setString(2, likeKeyword); ps.setString(3, likeKeyword); // ... 执行查询,同findAll() } }为什么安全:
ps.setString(1, likeKeyword)将%张%整体作为字符串参数传递,MySQL不会将其解析为SQL语法,彻底规避' OR '1'='1类注入。切忌用字符串拼接:"WHERE name LIKE '%" + keyword + "%'"。
5.2 数据导出:一键生成Excel(Apache POI轻量集成)
添加Maven依赖:
<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.4</version> </dependency>导出逻辑(在ContactFrame中):
private void exportToExcel() { try { XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("通讯录"); // 表头 String[] headers = {"ID", "姓名", "手机", "邮箱", "地址", "生日", "创建时间", "更新时间"}; XSSFRow headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 数据行 List<Contact> contacts = contactService.findAll(); for (int i = 0; i < contacts.size(); i++) { Contact c = contacts.get(i); XSSFRow row = sheet.createRow(i + 1); row.createCell(0).setCellValue(c.getId()); row.createCell(1).setCellValue(c.getName()); row.createCell(2).setCellValue(c.getPhone()); row.createCell(3).setCellValue(c.getEmail()); row.createCell(4).setCellValue(c.getAddress()); row.createCell(5).setCellValue(c.getBirthday() != null ? c.getBirthday().toString() : ""); row.createCell(6).setCellValue(c.getCreatedAt().toString()); row.createCell(7).setCellValue(c.getUpdatedAt().toString()); } // 写入文件 JFileChooser fc = new JFileChooser(); fc.setDialogTitle("选择导出路径"); fc.setSelectedFile(new File("contacts_export.xlsx")); if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { try (FileOutputStream fos = new FileOutputStream(fc.getSelectedFile())) { workbook.write(fos); JOptionPane.showMessageDialog(this, "导出成功!"); } } } catch (Exception e) { JOptionPane.showMessageDialog(this, "导出失败:" + e.getMessage()); } }参数说明:
XSSFWorkbook对应.xlsx格式(兼容Office 2007+),XSSFSheet支持百万行数据;setCellValue()自动处理String/Number/Date类型,无需手动格式化。
5.3 打包发布:用jpackage生成免JRE安装包(JDK 14+)
# 编译后生成jar javac -d out src/main/java/**/*.java jar -cfe ContactApp.jar com.example.ui.ContactFrame -C out . # 用jpackage打包(Windows示例) jpackage --input . --name "通讯录管理系统" --main-jar ContactApp.jar \ --main-class com.example.ui.ContactFrame \ --type exe \ --win-per-user-install \ --win-menu \ --win-shortcut \ --icon src/main/resources/app-icon.ico关键优势:生成的
.exe安装包自带JRE,用户双击即用,无需预装Java。--win-menu和--win-shortcut自动创建开始菜单和桌面快捷方式,体验接近商业软件。
我带新人做这个项目时,总强调一句:“别急着写完,先让save()方法在断网时抛出明确异常,再让loadContacts()在空表时UI不崩溃——可用性比功能多一条更重要。” 这套组合拳下来,你手里握的就不是“一个过时的Demo”,而是能随时拆解、替换、演进的Java工程骨架。希望帮到你。
本文还有配套的精品资源,点击获取