简介通讯录管理系统课程设计包面向高校Java课程设计学习者与需要完成小型桌面应用实践的学生围绕注册登录、通讯录浏览查找及增删改维护等核心需求给出可参考的完整实现思路。方案采用JDBC连接MySQL界面可在Swing GUI或JSP技术路线中选用涉及类的设计、继承与多态、窗体与事件处理、数据库表结构设计及增删改查等知识点适合用来理解面向对象编程与模块化开发。压缩包共5个文件由4个Java源码和1个SQL脚本组成整体约8KB源码对应登录、注册、通讯录维护及数据库工具等模块SQL脚本用于初始化数据表结构轻量便于阅读和二次修改。已有1562人学习可作为课程设计选题落地、功能扩展和答辩准备的参考样本。1. 从验收现场倒推java课程设计里的通讯录管理系统到底要交付什么课程设计验收时老师通常不会只看登录页而是随手点“新增”填一个手机号再搜“张”接着改一条、删一条、刷新表格。通讯录管理系统看着简单真正拉开差距的是数据能不能落库、搜索分页是否准、删除后刷新会不会还留着脏数据。这个 java课程设计 项目适合刚学完 Java SE、JDBC 和 Swing 的读者用来把增删改查、分层、异常处理和打包串成一条完整链路也适合已经写完控制台版、想补桌面界面和 CSV 导入导出的同学。下面从工程骨架开始按能跑、能改的顺序拆。2. 工程骨架与数据模型从目录分层到 contacts 表的字段取舍通讯录管理系统的第一道分水岭不在界面而在工程骨架。很多课程设计一开始把所有代码塞进一个Main类新增、查询、弹窗、SQL 全混在一起演示时能跑改一个字段就牵一发动全身。常见做法是分成entity、dao、service、ui四层再用 Maven 管依赖。这样后面加导出、加登录、换数据库时不用把整个项目推倒重来。技术选型上Java Swing 做桌面界面、JDBC 直连 MySQL、Maven 管理驱动包是课程设计里成本较低也容易讲清楚的一条线。2.1 目录结构controller、service、dao、entity 各放什么我一般把桌面程序里的“controller”弱化成界面事件层真正的业务校验放serviceSQL 放dao。目录不用追求大厂分层但职责要稳定界面只负责收集输入和展示结果service 负责判空、格式校验、把 SQLException 转成用户能看懂的提示dao 只关心表和对象之间的映射。目录/包职责关键类课程设计里的注意点entity数据载体Contact字段和表列一一对应别在实体里写 SQLdao数据库访问ContactDao所有PreparedStatement集中在这里service业务校验与事务ContactService手机号格式、重复联系人在这里拦uiSwing 窗口与事件ContactFrame、ContactTableModel不在 EDT 里做耗时查询util连接与配置DBUtil连接串、驱动、关闭资源resources配置与图标db.properties打包后要用 classpath 读取Maven 依赖不用堆太多课程设计里通常只需要 MySQL 驱动和测试依赖。驱动版本跟着本地数据库来不要从网上随便复制一个和本地不匹配的版本否则启动就报No suitable driver。dependencies !-- MySQL 驱动scope 用 runtime 即可代码里只依赖 java.sql -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version${mysql.version}/version scoperuntime/scope /dependency /dependencies这段pom.xml的关键不是版本号本身而是scope。JDBC 代码只使用java.sql包驱动在运行时加载所以runtime足够如果写成provided打 jar 后很可能找不到驱动。${mysql.version}放在properties里统一管理换机器时只改一处。2.2 contacts 表设计与实体类映射通讯录的核心表不用复杂但字段要覆盖演示场景姓名、手机号、邮箱、公司、地址、备注再加删除标记和时间字段。手机号是否唯一是课程设计里经常被忽略的点。我的做法是给phone和deleted建联合唯一索引deleted默认 0删除时把deleted写成当前id这样同一条手机号被软删除后还能重新录入历史记录也不会互相顶掉。CREATE TABLE contacts ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, email VARCHAR(100), company VARCHAR(100), address VARCHAR(200), remark VARCHAR(255), deleted BIGINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_phone_deleted (phone, deleted) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明utf8mb4保证姓名、地址里的中文和特殊符号能存deleted0表示有效记录deletedid表示这条记录已被软删除同时避开联合唯一索引冲突。updated_at用ON UPDATE CURRENT_TIMESTAMP省去每次手写时间。课程设计如果不想做软删除也可以直接用物理删除但验收时老师问“删掉的数据还能恢复吗”软删除会更好解释。实体类保持扁平不要急着上 Lombok课程设计环境里手写 getter/setter 更稳。public class Contact { private Long id; private String name; private String phone; private String email; private String company; private String address; private String remark; private long deleted; public boolean hasValidPhone() { return phone ! null phone.matches(\\d{11}); } // getter / setter 省略课程设计里手写即可 }hasValidPhone()把手机号规则收在实体里service 调用它做校验界面不再重复写正则。注意手机号字段用String而不是long前导 0 和长度控制都更安全deleted用long是为了和id对齐。2.3 数据库配置与初始化一条能跑通的启动链路连接数据库的代码最容易被复制来复制去最后每个 DAO 里都有一段DriverManager.getConnection。正确做法是收口到DBUtil连接串参数一次写清楚。下面这段把驱动加载、连接获取、资源关闭都放在一个工具类里DAO 只管拿连接。public final class DBUtil { private static final String URL jdbc:mysql://localhost:3306/contact_book ?useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/ShanghaiuseSSLfalse; 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 目录); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception ignored) { } } } } }连接串里的characterEncodingutf8和建库的utf8mb4要配套serverTimezone不写某些环境会报时区错误useSSLfalse只是课程设计本地环境的简化真实项目不要照抄。close用可变参数是为了兼容Connection、PreparedStatement、ResultSet不过更推荐在 DAO 里直接写 try-with-resources后面会看到。数据库初始化可以手动执行 SQL也可以写一个init.sql放resources下。课程设计验收前我会先确认三件事数据库contact_book已创建、contacts表存在、root密码和代码一致。任何一项不对启动时都会在第一个查询处翻车。提示把连接信息放进db.properties更利于打包后修改读取时用DBUtil.class.getResourceAsStream(/db.properties)不要用new File(db.properties)。3. 核心功能链路新增、搜索分页、修改删除的 SQL 与 Java 实现通讯录管理系统的功能听起来就是增删改查但每一段都有课程设计里常见的坑新增时手机号重复、搜索时LIKE拼接被注入、分页偏移量算错、删除后列表不刷新。把这几条链路写扎实界面哪怕朴素验收也能站住。下面按新增、搜索分页、修改删除三段拆每段都给能直接放进项目的代码。3.1 新增联系人表单校验、PreparedStatement 与主键回填新增的入口在界面但校验不能只靠界面。用户可能从 CSV 导入也可能在输入框里粘一段带空格的文本。service 层先做标准化再交给 DAO。DAO 使用PreparedStatement并用RETURN_GENERATED_KEYS把自增主键回填到对象里界面新增后可以直接定位到这一行。public Long add(Contact c) throws SQLException { String sql INSERT INTO contacts(name, phone, email, company, address, remark) VALUES(?, ?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, c.getName()); ps.setString(2, c.getPhone()); ps.setString(3, c.getEmail()); ps.setString(4, c.getCompany()); ps.setString(5, c.getAddress()); ps.setString(6, c.getRemark()); int rows ps.executeUpdate(); if (rows 0) { throw new SQLException(新增联系人失败影响行数为 0); } try (ResultSet keys ps.getGeneratedKeys()) { if (keys.next()) { c.setId(keys.getLong(1)); } } return c.getId(); } }参数说明Statement.RETURN_GENERATED_KEYS让 MySQL 返回自增主键setString的索引从 1 开始顺序和 SQL 占位符一致try-with-resources保证Connection、PreparedStatement、ResultSet关闭。rows 0在新增里几乎不会出现但保留判断能让异常更早暴露。service 层负责把“用户输入错误”和“数据库错误”分开。public Long create(Contact c) { if (c.getName() null || c.getName().trim().isEmpty()) { throw new IllegalArgumentException(姓名不能为空); } c.setName(c.getName().trim()); c.setPhone(c.getPhone() null ? : c.getPhone().trim()); if (!c.hasValidPhone()) { throw new IllegalArgumentException(手机号需为 11 位数字); } try { return dao.add(c); } catch (SQLIntegrityConstraintViolationException e) { throw new IllegalStateException(该手机号已存在请检查是否重复录入); } catch (SQLException e) { throw new RuntimeException(新增联系人失败请查看数据库连接, e); } }SQLIntegrityConstraintViolationException是唯一索引冲突时常见的异常类型捕获后转成业务提示界面就不会弹出一大段英文堆栈。trim()放在 service 而不是 DAO是为了让所有入口共用同一套规则。这里有个细节手机号为空时先转成空字符串再走正则避免NullPointerException把界面卡住。3.2 搜索与分页LIKE 参数拼接和 LIMIT 偏移量搜索框里输入“张”希望姓名、手机号、公司都能命中。SQL 里不要用字符串拼%而是把%放进参数值里。分页则要同时写列表查询和总数查询否则页码会出现“有数据但显示第 0 页”或者“最后一页空白”。public ListContact search(String keyword, int page, int pageSize) throws SQLException { String kw keyword null ? : keyword.trim(); String like % kw %; int safePage Math.max(page, 1); int safeSize Math.max(pageSize, 1); int offset (safePage - 1) * safeSize; String sql SELECT id, name, phone, email, company, address, remark FROM contacts WHERE deleted 0 AND (name LIKE ? OR phone LIKE ? OR company LIKE ?) ORDER BY id DESC LIMIT ? OFFSET ?; ListContact list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, like); ps.setString(2, like); ps.setString(3, like); ps.setInt(4, safeSize); ps.setInt(5, offset); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); } } } return list; }参数说明safePage和safeSize防止界面传入 0 或负数offset是跳过的行数不是页码LIKE的通配符放在 Java 字符串里拼进参数值占位符仍然只写?。ORDER BY id DESC让新增的联系人排在前面演示时更直观。mapRow是私有方法把ResultSet当前行转成Contact字段顺序和查询列一致。总数查询单独写别在列表查询里用SQL_CALC_FOUND_ROWS课程设计环境里容易遇到兼容性问题。public int count(String keyword) throws SQLException { String kw keyword null ? : keyword.trim(); String like % kw %; String sql SELECT COUNT(*) FROM contacts WHERE deleted 0 AND (name LIKE ? OR phone LIKE ? OR company LIKE ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, like); ps.setString(2, like); ps.setString(3, like); try (ResultSet rs ps.executeQuery()) { return rs.next() ? rs.getInt(1) : 0; } } }列表和总数用同一套搜索条件关键词为空时%%会命中全部有效记录。页码控件可以根据count和pageSize算总页数当前页超过总页数时回退到最后一页。这里别用LIMIT ?, ?的简写LIMIT offset, size和LIMIT size OFFSET offset顺序容易记反统一写OFFSET更不容易出错。3.3 修改与删除按主键更新、软删除与事务边界修改必须带id和deleted 0条件否则可能改到已删除的历史记录。删除采用软删除把deleted写成id既保留恢复可能也避免联合唯一索引冲突。更新和删除的影响行数要判断返回false时界面提示“记录不存在或已被删除”。public boolean update(Contact c) throws SQLException { String sql UPDATE contacts SET name ?, phone ?, email ?, company ?, address ?, remark ? WHERE id ? AND deleted 0; 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.getCompany()); ps.setString(5, c.getAddress()); ps.setString(6, c.getRemark()); ps.setLong(7, c.getId()); return ps.executeUpdate() 1; } } public boolean softDelete(long id) throws SQLException { String sql UPDATE contacts SET deleted id WHERE id ? AND deleted 0; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setLong(1, id); return ps.executeUpdate() 1; } }update的WHERE id ? AND deleted 0是后悔药能防止误改历史数据softDelete把deleted从 0 改成主键值联合唯一索引(phone, deleted)会让原手机号在有效记录里释放出来。事务边界在单条操作里可以不手动开JDBC 默认自动提交就够但批量导入 CSV 时必须手动setAutoCommit(false)否则导入到一半失败会留下半截数据。这个区别在后面避坑章节会展开。4. Swing 界面与事件模型把课程设计做成能演示的桌面程序Swing 课程设计最怕两件事界面能显示但数据不刷新或者查询一慢整个窗口卡死。把JTable、TableModel、SwingWorker三样东西用对演示效果会稳很多。界面不需要做得花哨顶部工具栏放搜索框和按钮中间放表格底部放状态栏新增和修改用弹窗表单删除前加确认框已经足够覆盖验收操作。4.1 主窗口布局JTable、工具栏与状态栏主窗口用BorderLayout分区顶部JToolBar放关键字输入和搜索按钮中间JScrollPane包JTable底部JLabel显示当前页和总条数。表格模型单独写类不要直接用DefaultTableModel到处addRow否则刷新和列名维护会很乱。public class ContactFrame extends JFrame { private final ContactTableModel tableModel new ContactTableModel(); private final JTable table new JTable(tableModel); private final JTextField keywordField new JTextField(18); private final JButton searchBtn new JButton(搜索); private final JLabel statusLabel new JLabel(就绪); public ContactFrame() { setTitle(通讯录管理系统); setDefaultCloseOperation(EXIT_ON_CLOSE); setLayout(new BorderLayout(8, 8)); JToolBar toolBar new JToolBar(); toolBar.add(new JLabel(关键字)); toolBar.add(keywordField); toolBar.add(searchBtn); add(toolBar, BorderLayout.NORTH); add(new JScrollPane(table), BorderLayout.CENTER); add(statusLabel, BorderLayout.SOUTH); pack(); setLocationRelativeTo(null); } }pack()按内容计算窗口大小setLocationRelativeTo(null)让窗口居中演示时不会跑到屏幕角落。JTable放进JScrollPane才有表头和滚动条。ContactTableModel继承AbstractTableModel列名固定为“编号、姓名、手机号、邮箱、公司、地址、备注”setRows里先清空再fireTableDataChanged()避免表格还显示旧数据。4.2 事件监听与 EDT查询放后台表格刷新回前台Swing 的事件回调运行在 EDT 上在按钮监听里直接查数据库数据量一大窗口就会“未响应”。常见做法是用SwingWorkerdoInBackground里查库done里回到 EDT 更新表格和按钮状态。searchBtn.addActionListener(e - { String keyword keywordField.getText(); searchBtn.setEnabled(false); statusLabel.setText(查询中...); new SwingWorkerListContact, Void() { Override protected ListContact doInBackground() throws Exception { return service.search(keyword, 1, 50); } Override protected void done() { try { tableModel.setRows(get()); statusLabel.setText(共 tableModel.getRowCount() 条); } catch (Exception ex) { JOptionPane.showMessageDialog(ContactFrame.this, 查询失败 rootMessage(ex), 错误, JOptionPane.ERROR_MESSAGE); } finally { searchBtn.setEnabled(true); } } }.execute(); });doInBackground的返回值类型是ListContactget()会阻塞 EDT 直到后台完成但因为done本身在 EDT更新组件是安全的。rootMessage用来剥掉ExecutionException外壳找到真正的SQLException或业务异常。按钮在查询期间禁用防止用户连点导致多个查询竞争刷新表格。注意不要在doInBackground里直接操作JTable或弹JOptionPaneSwing 组件不是线程安全的这类代码在 IDE 里偶尔能跑打包后可能随机卡死。4.3 CSV 导入导出字符集、表头校验与失败回滚课程设计加一个 CSV 导出演示效果会明显好于只有表格。导出时写 UTF-8 BOMExcel 打开中文不乱码字段里如果有逗号或引号用双引号包裹并把内部引号转义。导入时先校验表头再逐行插入整批放在一个事务里。public void exportCsv(Path path, ListContact rows) throws IOException { try (BufferedWriter w Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { w.write(\ufeff); // Excel 识别 UTF-8 中文 w.write(姓名,手机号,邮箱,公司,地址,备注\n); for (Contact c : rows) { w.write(csv(c.getName()) , csv(c.getPhone()) , csv(c.getEmail()) , csv(c.getCompany()) , csv(c.getAddress()) , csv(c.getRemark()) \n); } } } private String csv(String value) { if (value null) { return ; } return \ value.replace(\, \\) \; }\ufeff是 BOM放在文件开头csv方法给每个字段加双引号内部双引号替换成两个双引号这是 CSV 的常见转义规则。导入端不要用line.split(,)地址里出现逗号就会错列。课程设计可以简化成只支持无逗号数据但至少要在表头校验失败时给出明确提示。导入的事务写法如下public int importCsv(Path path) throws IOException, SQLException { try (Connection conn DBUtil.getConnection(); BufferedReader reader Files.newBufferedReader(path, StandardCharsets.UTF_8)) { conn.setAutoCommit(false); try { String header reader.readLine(); if (header null || !header.contains(姓名)) { throw new IllegalArgumentException(CSV 表头不正确至少需要姓名列); } int count 0; String line; while ((line reader.readLine()) ! null) { // 简化解析实际项目应使用 CSV 解析库处理引号内逗号 String[] parts line.split(,, -1); Contact c new Contact(); c.setName(parts[0].trim()); c.setPhone(parts[1].trim()); c.setEmail(parts[2].trim()); c.setCompany(parts[3].trim()); c.setAddress(parts[4].trim()); c.setRemark(parts[5].trim()); service.create(c); count; } conn.commit(); return count; } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } } }setAutoCommit(false)后任何一行失败都rollback不会出现导入一半的脏数据。split(,, -1)保留末尾空字段避免备注为空时数组长度不够。这里为了课程设计可读性用了简化解析真实项目建议换成成熟 CSV 库或者至少写一个带引号状态机的解析器。5. 避坑与排查课程设计验收前最容易翻车的 5 个点通讯录管理系统的代码量不大翻车点却很集中驱动、乱码、唯一索引、打包路径、事务。下面五条都是我在课程设计项目里反复见到的现象按“现象 → 原因 → 解决”写验收前逐条过一遍能省不少返工。5.1 驱动加载失败与连接泄漏现象启动时报ClassNotFoundException: com.mysql.cj.jdbc.Driver或者运行一段时间后数据库报Too many connections。原因通常有两个Maven 依赖没打进运行 classpath或者每个 DAO 方法都DriverManager.getConnection却没有关闭Connection。IDE 里因为依赖自动加载容易掩盖第一个问题第二个问题在演示数据少时看不出来连续点几十次查询就开始卡。解决先确认mysql-connector-j在pom.xml里且 scope 不是provided老项目没有 Maven 时把驱动 jar 放进lib并在运行配置里加入 classpath。连接必须用 try-with-resources或者统一走DBUtil.close。不要在每个方法里写conn.close()却漏掉异常分支资源关闭要放在finally或 try-with-resources 里。try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { // 三个资源按相反顺序自动关闭 }try-with-resources的关闭顺序是ResultSet、PreparedStatement、Connection正好符合 JDBC 的释放顺序。课程设计不需要上连接池但要知道连接是稀缺资源不能随手 new。5.2 中文乱码与数据库排序规则现象表格里姓名显示成问号或者按姓名排序时“张”排在“李”后面和预期不一致。原因一般是建库时用了latin1或者连接串没写characterEncoding也可能是 CSV 导出没有 BOM。排序问题则和数据库排序规则有关utf8mb4_general_ci和utf8mb4_unicode_ci对中文的排序结果可能不同。解决建库统一utf8mb4连接串加useUnicodetruecharacterEncodingutf8CSV 导出写\ufeff。Swing 界面如果字体不支持中文可以显式设置new Font(Microsoft YaHei, Font.PLAIN, 14)但更稳的是用系统默认字体别硬编码一个不存在的字体名。排序如果要按拼音课程设计里可以在 Java 层用Collator.getInstance(Locale.CHINA)排不要完全依赖数据库默认规则。5.3 重复联系人导致的主键冲突现象新增时弹出Duplicate entry 138xxxx for key uk_phone_deleted或者导入 CSV 到一半整批失败。原因是你给手机号建了唯一索引用户重复录入同一个号码如果做了软删除唯一索引只包含phone还会导致已删除号码无法再次录入。解决service 层先查一次是否已存在有效记录给出“该手机号已存在”的提示同时保留数据库唯一索引作为最后防线捕获SQLIntegrityConstraintViolationException转成业务提示。软删除场景用(phone, deleted)联合唯一索引删除时deleted id。导入 CSV 时允许用户选择“跳过重复”或“整批回滚”不要静默失败。public boolean existsByPhone(String phone) throws SQLException { String sql SELECT 1 FROM contacts WHERE phone ? AND deleted 0 LIMIT 1; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, phone); try (ResultSet rs ps.executeQuery()) { return rs.next(); } } }LIMIT 1让查询在命中后立刻停止只查deleted 0避免把历史删除记录算进去。这个查询和新增之间仍有并发窗口所以数据库唯一索引不能省。5.4 打包后配置文件和图标找不到现象IDE 里运行正常mvn package后双击 jar 报FileNotFoundException: db.properties或图标不显示。原因是用new File(src/main/resources/db.properties)这种相对路径打包后工作目录变了文件根本不在那里。解决配置文件放src/main/resources用 classpath 读取图片用getResource拿 URL再交给ImageIcon。路径以/开头表示从 classpath 根目录找。try (InputStream in DBUtil.class.getResourceAsStream(/db.properties)) { if (in null) { throw new IllegalStateException(db.properties 未打包进 classpath); } Properties props new Properties(); props.load(in); // 读取 url、user、password }getResourceAsStream返回 null 时不要继续load否则空指针很难定位。打包后先用jar tf target/xxx.jar看一下db.properties在不在根目录再排查代码。5.5 演示数据、事务与验收操作顺序现象批量导入 CSV 时中间某行手机号重复导入结束后表格里多了一半数据或者演示时先删除再搜索结果列表还显示已删除记录。原因是导入没有手动事务删除后没有刷新查询条件或者查询 SQL 漏了deleted 0。解决批量导入统一setAutoCommit(false)全部成功再commit失败rollback删除后重新调用搜索方法刷新表格不要只从TableModel里删一行。验收前准备一组演示数据按“新增 → 搜索 → 修改 → 删除 → 导出 → 再搜索”的顺序走一遍确认每一步页面和数据一致。事务代码前面已经给过关键是finally里把autoCommit恢复避免影响后续单条操作。6. 二次开发把通讯录扩展成小型信息管理系统的改造路径课程设计交付后很多同学会想加登录、加分组、换 SQLite、导 Excel。直接改界面会越改越乱比较稳的路径是先抽接口再换实现最后用冒烟用例验证。通讯录管理系统的分层如果前面搭好了二次开发其实就是替换dao和扩展字段。6.1 先抽接口DAO 与 Service 的可替换写法把ContactDao抽成ContactRepository接口Swing 界面只依赖ContactService不直接 new DAO。这样换 MySQL 到 SQLite、或者加一个内存实现做测试都不用动界面。public interface ContactRepository { Long add(Contact c) throws SQLException; ListContact search(String keyword, int page, int pageSize) throws SQLException; int count(String keyword) throws SQLException; boolean update(Contact c) throws SQLException; boolean softDelete(long id) throws SQLException; }接口方法只抛SQLException业务异常在 service 转。新增分组时给Contact加groupId给表加group_id再写一个GroupRepository比在界面里塞下拉框逻辑清晰得多。6.2 改造验证用一组冒烟用例检查回归每次改完 DAO至少跑下面这组检查别只点一次新增就认为没问题。检查项操作预期新增输入姓名和 11 位手机号表格新增一行数据库有记录重复新增同一手机号再新增提示已存在不产生第二条有效记录搜索输入姓名关键字只显示匹配且deleted0的记录修改改手机号后保存列表刷新旧手机号不再命中删除删除后搜索原姓名列表不显示数据库deletedid导出导出当前列表CSV 用 Excel 打开中文正常这组用例覆盖了唯一索引、软删除、搜索条件和字符集改字段后回归成本很低。6.3 一个收口技巧统一结果对象和异常提示界面层最怕每个按钮都写一遍 try-catch。可以做一个简单的ResultT把成功数据、失败消息包在一起service 返回它界面只判断isSuccess()。这样二次开发加新功能时异常提示不会散落各处。public class ResultT { private final boolean success; private final T data; private final String message; private Result(boolean success, T data, String message) { this.success success; this.data data; this.message message; } public static T ResultT ok(T data) { return new Result(true, data, ); } public static T ResultT fail(String message) { return new Result(false, null, message); } public boolean isSuccess() { return success; } public T getData() { return data; } public String getMessage() { return message; } }参数说明success表示业务是否成功data是成功时的返回数据message是失败提示。service 里把IllegalArgumentException和SQLException分别转成fail(手机号格式不正确)、fail(数据库连接失败)界面统一弹getMessage()。注意不要把 SQL 原文直接弹给用户日志里保留堆栈即可。血泪经验是我每次改完 DAO 都会先用一条测试手机号走完新增、搜索、修改、删除、导出五步再谈界面美化。从那以后我每次交付课程设计前都强制走一遍这条链路不再相信“刚才还能跑”。如果你也在做同类 java课程设计这份通讯录管理系统源码可以先把数据库跑通再按上面的接口和结果对象往下改后面加分组、登录或换存储都不会太痛。希望帮到你。本文还有配套的精品资源点击获取