本文还有配套的精品资源,点击获取
简介:这是一款用Java Swing开发的桌面端通讯录工具,启动后先登录,就能增删改查联系人信息。后台直连MySQL 8.0,附带完整的建表脚本address.sql,开箱就能跑。包里有全部源码(src目录)、编译好的class文件(bin目录)、JDBC驱动mysql-connector-java-8.0.16.jar、操作图标(create.png、update.png、delete.png、search.png)、配置文件(.classpath、.project)和资源目录(image、com包结构、lib依赖等)。项目按标准Eclipse Java工程组织,路径清晰,适合练手Swing界面开发、MySQL数据库连接、JDBC调用、图标资源管理以及桌面程序打包部署。不需要额外配置环境,导入Eclipse即可运行,也支持命令行编译执行。
1. 项目概述:一个“能跑、能学、能改”的Java桌面通讯录
我做过不少Java桌面小工具,但真正能让我在教学现场直接打开、三分钟内让学生看懂逻辑、五分钟内动手改功能的,还真不多。这个Java通讯录就是其中少有的一个——它不是教科书里的HelloWorld式Demo,也不是堆砌了二十个设计模式却连增删改查都卡在SQL异常里的“教学陷阱”。它是一个真实可运行、结构可拆解、逻辑可追踪、错误可复现的完整工程。核心关键词就三个:Java通讯录、Swing界面、MySQL8连接——没有花哨的Spring Boot、没有前端框架、不依赖任何云服务,纯本地JVM进程+Swing GUI+MySQL 8.0直连,所有依赖打包进一个文件夹,双击run.bat(Windows)或执行sh run.sh(Linux/macOS)就能启动登录页。
它解决的是一个非常具体、非常实际的问题:如何让初学者在不被环境配置和框架抽象层淹没的前提下,亲手打通“界面点击→数据封装→SQL执行→结果回显”这条完整链路?不是只写DAO层模拟数据,也不是只画个按钮摆样子。它强制你面对真实数据库连接失败时的SQLException堆栈,让你亲手处理Swing事件线程与数据库IO线程的阻塞风险,也逼你思考:为什么删除联系人后表格没刷新?为什么中文姓名存进数据库变成问号?为什么换台电脑就报“Class not found: com.mysql.cj.jdbc.Driver”?这些问题,在这个项目里全都有迹可循、有解可试。
适合谁?如果你是刚学完Java基础语法、正准备接触GUI或数据库的在校学生;如果你是转行想快速建立“Java能做什么”具象认知的新人;如果你是带实训课的老师,需要一个不依赖网络、不需额外安装服务、导入即跑的教学案例——那它就是为你量身定做的。它不追求炫酷动画或响应式布局,但每一个按钮背后的ActionListener实现、每一行SQL语句的参数绑定方式、每一次数据库连接的获取与释放时机,都经得起放大镜审视。我把它比作一把“解剖刀”:刀锋所至,Swing事件分发机制、JDBC预编译原理、MySQL字符集配置、资源路径加载逻辑……全都清晰可见。
2. 整体架构与设计思路拆解:为什么是Swing + MySQL 8,而不是别的?
2.1 技术选型的底层逻辑:轻量、可控、教学友好
很多人看到“Swing”第一反应是“过时”,看到“MySQL 8”又担心驱动兼容性。但恰恰是这两个看似“保守”的选择,构成了本项目教学价值的核心支点。
先说Swing。它不是因为“历史遗留”才被选中,而是因为它足够简单、足够透明、足够暴露底层机制。对比JavaFX,Swing没有复杂的CSS样式系统和Scene Graph渲染树;对比Web方案,它不需要理解HTTP协议、浏览器渲染流程或前后端分离概念。一个JButton的点击事件,你点进去就能看到ActionListener.actionPerformed()方法签名,里面写的全是自己写的Java代码,没有魔法。JTable的数据模型TableModel接口只有几行定义,你完全可以自己实现一个AddressBookTableModel去控制数据怎么来、怎么更新、怎么通知视图刷新——这种“手把手教你造轮子”的体验,在高级框架里早已被封装得无影无踪。
再说MySQL 8。选它不是赶时髦,而是因为它的默认安全策略倒逼你理解关键配置。MySQL 8.0默认启用caching_sha2_password认证插件,而旧版JDBC驱动(如5.x系列)不支持。项目明确指定mysql-connector-java-8.0.16.jar,这背后是一次真实的兼容性教育:你必须确认驱动版本与数据库大版本匹配,否则连登录页面都打不开。同样,MySQL 8默认sql_mode更严格(比如禁止零日期),当你执行INSERT INTO contact (name, phone) VALUES ('张三', '')时,它会直接抛出DataTruncationException,而不是默默存入空字符串——这迫使你思考数据校验该放在哪一层(界面输入拦截?DAO层参数检查?还是数据库约束?)。这些“麻烦”,恰恰是生产环境里最常踩的坑,而本项目把它们前置到了学习阶段。
提示:项目未使用连接池(如HikariCP),而是每次操作都新建
Connection再关闭。这不是疏忽,而是刻意为之。新手如果一上来就学连接池,很容易把“获取连接”当成魔法,忽略Connection对象本身是重量级资源、必须显式关闭的核心事实。等你亲手写过十次try-with-resources包裹的Connection,再引入连接池,才能真正理解它解决了什么问题。
2.2 分层结构解析:从包名到职责的物理映射
项目采用标准Java工程目录结构,src下com包路径清晰体现了MVC思想的朴素实践:
com.addressbook.ui:纯粹的界面层。包含LoginFrame(登录窗口)、MainFrame(主窗口)、ContactDialog(新增/编辑弹窗)等Swing组件。这里绝不出现SQL语句或数据库连接代码,所有数据操作都通过调用ContactService完成。com.addressbook.dao:数据访问层。核心是ContactDAO类,封装了所有对contact表的CRUD操作。它持有DataSource(实际是DriverManager的简单封装),负责将Contact对象转换为PreparedStatement参数,并将ResultSet结果集映射回Contact对象。注意:这里的DAO没有用泛型或反射,所有方法名直白如insert(Contact c)、update(Contact c),参数类型明确,便于跟踪调试。com.addressbook.model:数据模型层。只有一个Contact类,包含id、name、phone、email、address等字段及标准getter/setter。它不继承任何框架类,就是一个纯粹的POJO,确保数据流转过程透明无污染。com.addressbook.service:业务逻辑层。ContactService是真正的“胶水”,它协调DAO与UI:接收UI传来的Contact对象,调用DAO.insert(),捕获可能的SQLException并转换为用户友好的提示(如“手机号格式错误”而非“Duplicate entry ‘138****’ for key ‘phone_unique’”),最后通知UI刷新表格。这一层的存在,让界面代码彻底摆脱SQL细节,也方便未来替换数据库(比如换成SQLite)时只需修改DAO实现。
这种分层不是为了炫技,而是为了让每个.java文件的职责边界像玻璃一样清晰。当你想查“为什么搜索功能不生效”,你只需要打开ContactService.search()和ContactDAO.search()两个文件;当你想改“新增窗口的布局”,你只动ContactDialog.java,完全不用碰DAO里的SQL。
2.3 数据库设计的务实主义:够用、健壮、可扩展
address.sql脚本仅创建一张contact表,但字段设计体现了对真实场景的考量:
CREATE TABLE `contact` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL COMMENT '姓名,不能为空', `phone` VARCHAR(20) NOT NULL UNIQUE COMMENT '手机号,唯一且非空', `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱,可为空', `address` TEXT DEFAULT NULL COMMENT '地址,可为空', `created_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', PRIMARY KEY (`id`), INDEX `idx_name` (`name`) COMMENT '姓名索引,加速模糊搜索' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;VARCHAR(50)vsVARCHAR(255):姓名长度设为50,是基于中国身份证姓名最长记录(如“欧阳修远”共4字,加空格最多约20字符),留出余量但不过度。避免盲目设255导致索引效率下降。UNIQUE约束:手机号唯一性由数据库强制保证,而非仅靠Java层校验。这是数据一致性底线,防止并发插入时出现脏数据。DEFAULT CURRENT_TIMESTAMP:创建和更新时间自动维护,省去Java层手动赋值,也避免因客户端时间不准导致的时间戳混乱。utf8mb4字符集:明确指定支持Emoji和生僻汉字(如“䶮”、“𠈌”),避免存入乱码。这是MySQL 8的推荐设置,项目脚本已固化。
注意:建表脚本末尾有一行
ALTER DATABASE addressbook CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;。很多新手会忽略这点,直接在默认字符集(如latin1)的数据库里执行建表,结果name字段虽设utf8mb4,但整个库仍是latin1,最终还是存乱码。这个脚本强制修正库级字符集,是防坑的关键一步。
3. 核心细节解析与实操要点:从登录验证到图标加载的全流程
3.1 登录模块:不只是密码校验,更是安全意识启蒙
登录界面LoginFrame看似简单,但隐藏着几个关键教学点:
密码加密存储的取舍:项目数据库
user表(脚本中已预置admin/123456)的密码字段是明文存储。这不是漏洞,而是教学设计——它让你直观看到:SELECT * FROM user WHERE username='admin' AND password='123456'这条SQL是如何工作的。等你理解了整个流程,再引导你思考:“如果换成MD5哈希,SQL该怎么写?”、“盐值(salt)该存在哪?”、“前端传输密码要不要加密?”。明文是起点,不是终点。Swing事件线程阻塞防护:登录按钮的
ActionListener中,数据库查询代码被包裹在SwingWorker中:java new SwingWorker<Boolean, Void>() { @Override protected Boolean doInBackground() throws Exception { return userService.validate(username, password); // 真实数据库查询 } @Override protected void done() { try { if (get()) { // 登录成功,显示主窗口 MainFrame main = new MainFrame(); main.setVisible(true); dispose(); // 关闭登录窗口 } else { JOptionPane.showMessageDialog(this, "用户名或密码错误"); } } catch (Exception ex) { JOptionPane.showMessageDialog(this, "登录失败:" + ex.getMessage()); } } }.execute();
这段代码的价值在于:它强制你面对Swing的单线程规则。如果不加SwingWorker,数据库查询(哪怕只是毫秒级)也会阻塞Event Dispatch Thread(EDT),导致登录窗口“假死”,鼠标悬停按钮无反应、进度条卡住。SwingWorker是Swing官方推荐的异步方案,它把耗时操作移出EDT,完成后安全地回调到EDT更新UI。这是桌面应用开发的必修课。资源路径加载的跨平台陷阱:登录窗口背景图
login_bg.jpg的加载代码是:java ImageIcon icon = new ImageIcon(LoginFrame.class.getResource("/image/login_bg.jpg")); JLabel bgLabel = new JLabel(icon);
注意/image/...前的斜杠!它表示从classpath根目录开始查找。如果写成"image/login_bg.jpg"(无斜杠),则会从LoginFrame.class所在包路径(com.addressbook.ui)下找,即试图加载com/addressbook/ui/image/login_bg.jpg,必然失败。这个细节在Eclipse里可能因工作区配置“宽容”而侥幸通过,但导出为jar包后100%报错。项目所有资源路径均采用绝对路径(/开头),确保稳定性。
3.2 图标资源管理:不只是贴图,更是工程规范实践
项目提供了create.png、update.png、delete.png、search.png四个操作图标,它们的使用贯穿整个UI:
统一尺寸与格式:所有PNG图标均为24x24像素,无透明通道(alpha=255)。这是Swing对
ImageIcon最友好的尺寸,避免缩放失真;无透明通道则杜绝了某些JVM版本下PNG加载的兼容性问题(如Java 8u202曾有PNG alpha解析Bug)。资源目录结构:
image文件夹位于src同级,与src、lib并列。在Eclipse中,需右键image文件夹 →Build Path→Use as Source Folder,将其纳入classpath。这样getClass().getResource("/image/create.png")才能正确解析。很多新手卡在这一步,以为把图片放src里就行,结果getResource()返回null。图标复用技巧:同一个
create.png图标,在主窗口工具栏、新增弹窗的确定按钮、甚至右键菜单里都被复用。实现方式是定义一个静态工具类:java public class IconUtil { public static final ImageIcon CREATE_ICON = new ImageIcon(IconUtil.class.getResource("/image/create.png")); public static final ImageIcon UPDATE_ICON = new ImageIcon(IconUtil.class.getResource("/image/update.png")); // ... 其他图标 }
所有UI组件直接引用IconUtil.CREATE_ICON,避免重复加载、节省内存,也方便后期统一更换图标(只需改一处)。
实操心得:我在测试不同JVM版本时发现,Java 11+对
getResource()的路径解析更严格。如果image文件夹未被正确添加为Source Folder,getResource()会静默返回null,导致按钮无图标但不报错。调试技巧:在IconUtil构造器里加一行System.out.println("Icon path: " + IconUtil.class.getResource("/image/create.png"));,立刻定位路径问题。
3.3 JDBC连接配置:从URL参数到字符集的硬核细节
ContactDAO中的数据库连接字符串是理解MySQL 8集成的关键:
private static final String URL = "jdbc:mysql://localhost:3306/addressbook?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true";逐个参数解析其必要性:
useSSL=false:MySQL 8默认要求SSL连接,但本地开发通常未配置SSL证书。关闭SSL是开发阶段的合理妥协,但必须清楚:生产环境必须开启并配置有效证书。serverTimezone=Asia/Shanghai:这是最高频的坑!MySQL服务器时区与JVM时区不一致会导致DATETIME字段存取错乱。例如,数据库设为SYSTEM(即系统时区),而你的Windows JVM时区是GMT+8,但MySQL服务实际运行在Docker容器里(默认UTC),NOW()函数返回的时间就会差8小时。显式指定serverTimezone强制对齐,避免时间戳错位。allowPublicKeyRetrieval=true:配合caching_sha2_password认证插件的必需参数。当JDBC驱动需要从服务器获取公钥来加密密码时,此参数允许该行为。缺少它会报Public Key Retrieval is not allowed。
连接驱动加载代码:
static { try { Class.forName("com.mysql.cj.jdbc.Driver"); // 显式加载驱动类 } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL JDBC Driver not found!", e); } }这段static块至关重要。它确保在ContactDAO任何方法执行前,驱动类已被JVM加载并注册到DriverManager。如果没有它,首次调用DriverManager.getConnection()时会因找不到驱动而抛SQLException。项目将此逻辑放在DAO类静态块中,而非每次连接都执行,既保证可靠性又提升性能。
4. 实操过程与核心环节实现:从零部署到功能验证的完整流水线
4.1 环境准备与一键部署:三步走通路
部署过程被设计为“三步极简法”,无需任何命令行记忆:
第一步:安装MySQL 8.0
- 下载MySQL 8.0 Community Server(推荐ZIP免安装版,避免Windows服务配置复杂化)
- 解压到任意路径(如D:\mysql8)
- 初始化数据目录:以管理员身份运行CMD,进入bin目录,执行:bash mysqld --initialize --console
控制台最后一行会输出临时root密码(形如A temporary password is generated for root@localhost: xxxxxx),务必复制保存。
- 启动MySQL服务:mysqld --console(前台运行,便于查看日志)或mysqld --install后net start mysql(后台服务)
第二步:执行建表脚本
- 使用MySQL客户端(如mysql -u root -p)登录,输入临时密码
- 创建数据库:CREATE DATABASE addressbook CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
- 切换数据库:USE addressbook;
- 执行脚本:source D:/path/to/address.sql(注意路径用正斜杠或双反斜杠)
第三步:运行通讯录程序
- 解压项目包,进入根目录
- Windows用户双击run.bat;Linux/macOS用户执行sh run.sh
-run.bat内容极其简单:bat @echo off java -cp "bin;lib/mysql-connector-java-8.0.16.jar" com.addressbook.ui.LoginFrame pause
它清晰展示了Java类路径(-cp)的构成:bin目录(含所有.class文件)、lib目录下的JDBC驱动。没有Maven、没有Gradle,纯手工拼接,让你一眼看懂JVM如何定位类。
验证是否成功:启动后出现登录窗口,输入
admin/123456能进入主界面,主界面表格显示预置的3条联系人数据(张三、李四、王五),即表示部署成功。此时你已经站在了整个系统的入口。
4.2 核心功能代码深度剖析:增删改查的每一步
新增联系人(Create)
触发流程:主窗口点击+按钮 → 弹出ContactDialog→ 填写信息 → 点击“确定”
关键代码在ContactDialog的确定按钮监听器:
okButton.addActionListener(e -> { Contact contact = new Contact(); contact.setName(nameField.getText().trim()); contact.setPhone(phoneField.getText().trim()); contact.setEmail(emailField.getText().trim()); contact.setAddress(addressArea.getText().trim()); // 前端基础校验 if (contact.getName().isEmpty() || contact.getPhone().isEmpty()) { JOptionPane.showMessageDialog(this, "姓名和手机号不能为空!"); return; } try { contactService.insert(contact); // 调用业务层 JOptionPane.showMessageDialog(this, "添加成功!"); this.dispose(); // 关闭对话框 mainFrame.refreshContactTable(); // 通知主窗口刷新表格 } catch (SQLException ex) { JOptionPane.showMessageDialog(this, "添加失败:" + ex.getMessage()); } });- 校验时机:校验放在
insert()调用之前,属于“快速失败”。避免无效数据进入数据库再被约束拒绝,减少IO开销。 refreshContactTable()的奥秘:MainFrame中此方法并非重新查询数据库,而是调用tableModel.addRow(...)。tableModel是DefaultTableModel的子类,它持有一个List<Contact>缓存。insert()成功后,ContactService会同步更新这个缓存(tableModel.addContact(contact)),因此addRow()能立即生效。这是Swing MVC中“模型主动通知视图”的典型实践。
查询联系人(Search)
搜索框位于主窗口顶部,支持模糊匹配:
searchField.addActionListener(e -> { String keyword = searchField.getText().trim(); if (!keyword.isEmpty()) { List<Contact> results = contactService.search(keyword); contactTableModel.setContacts(results); // 替换整个数据列表 contactTable.revalidate(); // 强制重绘 contactTable.repaint(); } });search()方法在ContactService中执行SELECT * FROM contact WHERE name LIKE ? OR phone LIKE ?,两个?参数均为"%"+keyword+"%"。setContacts()是ContactTableModel的自定义方法,它清空原有List,添加新结果,并调用fireTableDataChanged()通知JTable刷新。这里没有用fireTableRowsInserted()等细粒度通知,因为搜索结果集大小不确定,全量刷新更稳妥。
修改与删除(Update & Delete)
二者共享一个前提:用户必须先在表格中选中一行。JTable的getSelectedRow()返回的是视图行号,需转换为模型行号:
int viewRow = contactTable.getSelectedRow(); if (viewRow == -1) { JOptionPane.showMessageDialog(this, "请先选择要操作的联系人!"); return; } int modelRow = contactTable.convertRowIndexToModel(viewRow); // 关键转换! Contact selected = contactTableModel.getContactAt(modelRow);- 为什么需要
convertRowIndexToModel()?因为JTable支持排序和过滤。当用户点击列标题排序后,视图行序(getSelectedRow())与模型行序(tableModel.getRowCount())不再一致。直接用视图行号去tableModel.getValueAt()会取错数据。这个转换是Swing表格开发的黄金法则。
删除操作的ContactDAO.delete()实现:
public void delete(int id) throws SQLException { String sql = "DELETE FROM contact WHERE id = ?"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); ps.executeUpdate(); // 返回影响行数,可用于判断是否真删除了 } }- 使用
try-with-resources确保Connection和PreparedStatement自动关闭,即使发生异常也不会泄露连接。 ps.executeUpdate()返回int,项目虽未使用该返回值,但它是判断操作是否成功的依据(如返回0表示WHERE id=?未匹配到任何行)。
4.3 配置文件与IDE适配:Eclipse工程的“隐形骨架”
项目包含.classpath和.project文件,这是Eclipse识别Java工程的“身份证”:
.project文件声明了这是一个org.eclipse.jdt.core.javaproject,指定了构建器(org.eclipse.jdt.core.javabuilder)和性质(org.eclipse.jdt.core.javanature)。.classpath文件定义了构建路径:xml <classpathentry kind="src" path="src"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> <classpathentry kind="lib" path="lib/mysql-connector-java-8.0.16.jar"/> <classpathentry kind="output" path="bin"/>
它告诉Eclipse:源码在src,输出类文件到bin,依赖库在lib下的JAR包。导入时选择File → Import → Existing Projects into Workspace,Eclipse会自动读取这些文件,无需手动配置构建路径。
实操心得:若导入后出现红色波浪线(如
Contact类报错),首要检查lib/mysql-connector-java-8.0.16.jar是否在Package Explorer中显示为“Referenced Libraries”。若未显示,右键项目 →Properties → Java Build Path → Libraries → Add JARs...,手动添加该JAR。这是Eclipse最常见的导入失败原因。
5. 常见问题与排查技巧实录:那些年我们踩过的坑
5.1 连接失败类问题:从“驱动未找到”到“时区不匹配”
| 现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver | JDBC驱动JAR未加入类路径 | 检查run.bat中-cp参数是否包含lib/mysql-connector-java-8.0.16.jar;检查Eclipse中Referenced Libraries是否列出该JAR | 确保JAR路径正确,Eclipse中右键JAR →Build Path → Add to Build Path |
Access denied for user 'root'@'localhost' | MySQL用户名密码错误,或用户无addressbook库权限 | 在MySQL客户端执行SELECT User,Host FROM mysql.user;和SHOW GRANTS FOR 'root'@'localhost'; | 重置密码:ALTER USER 'root'@'localhost' IDENTIFIED BY 'newpass';;授权:GRANT ALL PRIVILEGES ON addressbook.* TO 'root'@'localhost'; |
The server time zone value 'XXX' is unrecognized | serverTimezone参数未设置或值错误 | 查看MySQL中SELECT @@global.time_zone, @@session.time_zone; | 在连接URL中添加serverTimezone=Asia/Shanghai(根据MySQL实际时区调整) |
Public Key Retrieval is not allowed | MySQL 8.0caching_sha2_password插件要求 | 查看MySQL中SELECT host,user,plugin FROM mysql.user WHERE user='root'; | 在连接URL中添加allowPublicKeyRetrieval=true |
5.2 数据显示类问题:中文乱码与表格不刷新
| 现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
中文姓名显示为???或方块 | 数据库、表、字段字符集非utf8mb4 | 执行SHOW CREATE DATABASE addressbook;和SHOW CREATE TABLE contact; | 执行ALTER DATABASE addressbook CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;和ALTER TABLE contact CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; |
| 新增/删除后表格无变化 | JTable未收到刷新通知 | 在refreshContactTable()方法中添加System.out.println("Refreshing table with "+tableModel.getRowCount()+" rows"); | 确保ContactService.insert()后调用了tableModel.addContact()或setContacts(),并调用fireTableDataChanged() |
| 搜索结果为空,但数据库中有匹配数据 | SQLLIKE参数未加%通配符 | 在ContactDAO.search()中打印最终SQL:System.out.println("Executing SQL: " + sql + " with param: %" + keyword + "%"); | 确保ps.setString(1, "%" + keyword + "%");和ps.setString(2, "%" + keyword + "%"); |
5.3 UI交互类问题:按钮无响应与窗口错位
| 现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 点击按钮无反应(无报错) | ActionListener未正确绑定,或事件被其他组件拦截 | 在按钮创建后添加System.out.println("Button created: " + addButton);;检查addActionListener()是否在initComponents()之后调用 | 确保addActionListener()在组件实例化后执行;避免在paint()等重绘方法中重复添加监听器 |
| 主窗口启动后位置偏移或尺寸异常 | MainFrame构造器中setLocationRelativeTo(null)未生效 | 在setVisible(true)前添加System.out.println("Frame size: " + getSize() + ", location: " + getLocation()); | 将setLocationRelativeTo(null)放在pack()之后、setVisible(true)之前,确保尺寸计算完成 |
5.4 进阶避坑指南:那些文档不会写的实战经验
Swing线程安全的“灰色地带”:
JOptionPane.showMessageDialog()看似可以在任意线程调用,但官方文档明确指出“应在EDT中调用”。实践中,SwingWorker.done()回调确实在EDT,所以安全;但若你在doInBackground()里直接调用,会导致不可预测的UI冻结。我的经验是:所有涉及UI组件的方法(setText()、setVisible()、showMessageDialog()),无论多简单,都必须确保在EDT中执行。宁可多写一行SwingUtilities.invokeLater(),也不要赌运气。JDBC资源泄漏的隐性杀手:项目用
try-with-resources很规范,但新手常犯的错是:在DAO方法中,Connection由getConnection()获取,但PreparedStatement和ResultSet未用try-with-resources包裹。例如:java // ❌ 危险写法 Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery(); // 忘记close(rs), close(ps), close(conn)
正确做法是三层嵌套try-with-resources,或像项目中那样,将Connection、PreparedStatement、ResultSet全部放入同一try括号中。这是血泪教训——资源泄漏不会立刻报错,但运行几小时后连接池耗尽,整个应用就卡死了。图标路径的“相对绝对”哲学:
getClass().getResource("image/create.png")(无斜杠)和getClass().getResource("/image/create.png")(有斜杠)的区别,本质是类加载器的资源查找策略。前者是“相对路径查找”,后者是“绝对路径查找”。项目采用后者,是因为它不依赖于调用类的包路径,只要image在classpath根目录,任何类都能加载。这个原则可以推广到所有资源:配置文件(config.properties)、SQL脚本(sql/insert.sql)、国际化文件(i18n/messages_zh_CN.properties)——一律用绝对路径(/开头),工程健壮性提升一个数量级。
6. 项目延展与学习路径:从通讯录到更广阔的世界
这个通讯录绝不是终点,而是一个精心设计的“能力发射台”。当你能熟练修改它的增删改查逻辑、读懂每一条SQL的意图、理解Swing事件流的走向,你就已经具备了向多个方向纵深发展的坚实基础。
向数据库方向深化:尝试给contact表增加“分组”功能。你需要新建group表和contact_group关联表,修改DAO层支持多表JOIN查询,改造UI增加分组下拉框和批量操作。这会带你深入理解关系型数据库的设计范式、外键约束、以及MyBatis等ORM框架如何自动化处理关联映射。
向GUI方向升级:用JavaFX重写界面。你会发现,同样的“新增联系人”功能,JavaFX需要定义FXML布局文件、Controller类、CSS样式表,数据绑定用StringProperty而非setText()。这个过程会让你深刻体会“声明式UI”与“命令式UI”的哲学差异,也为学习Android(View Binding)、Web前端(React/Vue)打下思维基础。
向工程化迈进:将项目迁移到Maven。创建pom.xml,声明mysql-connector-java依赖,用maven-compiler-plugin指定Java版本,用maven-shade-plugin打包成一个包含所有依赖的fat jar。你会第一次体会到依赖管理、构建生命周期、以及“一次编写,到处运行”的现代Java工程实践。
最后分享一个小技巧:在ContactDAO中,把所有SQL语句提取到final static String常量中,并按功能分组:
public class ContactDAO { // 查询相关 private static final String SQL_SELECT_ALL = "SELECT * FROM contact ORDER BY created_time DESC"; private static final String SQL_SEARCH = "SELECT * FROM contact WHERE name LIKE ? OR phone LIKE ?"; // 更新相关 private static final String SQL_INSERT = "INSERT INTO contact (name, phone, email, address) VALUES (?, ?, ?, ?)"; private static final String SQL_UPDATE = "UPDATE contact SET name=?, phone=?, email=?, address=? WHERE id=?"; // ... }这样做不仅让SQL集中管理、易于审计,更重要的是,当你需要做SQL性能分析时,可以直接在数据库监控工具中搜索这些常量名,瞬间定位慢查询来源。这是我带团队时推行的“SQL可追溯性”规范,效果显著。
这个Java通讯录项目,就像一把磨得锃亮的瑞士军刀——它不追求单一功能的极致,但每一个小刀片(Swing、JDBC、MySQL、工程结构)都经过千锤百炼,随时准备帮你切开下一个技术难题。现在,刀已在手,接下来的路,就看你如何挥动了。
本文还有配套的精品资源,点击获取
简介:这是一款用Java Swing开发的桌面端通讯录工具,启动后先登录,就能增删改查联系人信息。后台直连MySQL 8.0,附带完整的建表脚本address.sql,开箱就能跑。包里有全部源码(src目录)、编译好的class文件(bin目录)、JDBC驱动mysql-connector-java-8.0.16.jar、操作图标(create.png、update.png、delete.png、search.png)、配置文件(.classpath、.project)和资源目录(image、com包结构、lib依赖等)。项目按标准Eclipse Java工程组织,路径清晰,适合练手Swing界面开发、MySQL数据库连接、JDBC调用、图标资源管理以及桌面程序打包部署。不需要额外配置环境,导入Eclipse即可运行,也支持命令行编译执行。
本文还有配套的精品资源,点击获取