
简介这是一份面向Java初学者与课程设计学习者的通讯录管理系统完整项目源码对应Java课程设计综合题要求采用JDBC连接数据库界面可选Swing GUI或JSP技术实现。系统涵盖用户注册登录、通讯录信息的浏览、查找与增删改维护等核心功能适合用来练习面向对象编程思想理解类的设计、继承与多态并掌握Swing窗体、面板、按钮、文本框等组件的使用及事件处理机制。资源包共5个文件包含4个java源码文件与1个sql数据库脚本压缩包约8KB其中java文件分别对应登录界面、注册界面、通讯录主界面与数据库连接工具类sql文件用于建库建表结构清晰便于对照学习。目前已有1562人学习下载。通过这份资源读者可获得一套可直接运行的课程设计参考方案学习JDBC连接MySQL、数据库表结构设计及增删改查操作并体会模块化拆分系统的设计思路适合作为课程设计起步或期末项目参考。1. 通讯录管理系统从课程设计到能跑起来的 Java 项目很多同学的 Java 课程设计都卡在同一个地方功能想得挺全代码一跑就报错界面和数据库各玩各的。通讯录管理系统就是最典型的例子——看起来只是增删改查真动手写的时候联系人字段怎么设计、数据存哪里、界面怎么刷新每一步都能踩坑。这个项目适合正在做 Java 课程设计、需要一份能直接跑通的参考实现的同学也适合想拿它当模板改造成图书管理、学生信息管理的开发者。它用 Java 实现覆盖联系人录入、查询、修改、删除、列表展示这些核心功能数据持久化方案常见的有两种文件存储和数据库存储。下面从项目定位、目录结构、核心代码到二次开发完整拆一遍。2. 项目定位与技术选型为什么用 Java 做通讯录2.1 这个项目到底解决什么问题通讯录管理系统的本质是一个带持久化的 CRUD 应用。用户打开程序后能录入联系人信息姓名、电话、邮箱、分组等能按姓名或电话搜索能修改已有记录能删除不再需要的联系人退出程序后数据不丢失。听起来简单但课程设计的要求通常不止于此——老师会看你的代码结构是否清晰、有没有分层、异常处理是否到位、界面是否可用。这个项目的价值在于它足够小小到一个人几天能写完又足够完整完整到能体现 Java 面向对象、集合框架、IO 流或 JDBC、Swing 或 JavaFX 界面编程这些核心知识点。很多同学做课程设计时容易贪大选个电商系统或者社交平台结果功能没写完代码一团乱。通讯录管理系统反而是最稳的选择功能边界清晰扩展方向也多。从适用人群来看如果你是刚学完 Java 基础、正在做课程设计的学生这个项目能让你把零散的知识点串起来。如果你已经工作想找个练手项目熟悉某个框架通讯录管理系统也能当脚手架用。2.2 技术选型Swing JDBC 还是 JavaFX 文件存储技术选型直接决定你后面写代码的难度和老师验收时的印象分。常见的组合有两种第一种是 Swing JDBC MySQL。Swing 是 Java 自带的桌面 GUI 库不需要额外配置资料多遇到问题好搜。JDBC 连 MySQL 是课程设计里最标准的数据库方案老师一看就知道你学过数据库。缺点是 Swing 界面比较丑需要自己调布局MySQL 还要装环境、建库建表。第二种是 JavaFX 文件存储JSON 或 CSV。JavaFX 界面更现代Scene Builder 拖拽布局效率高。文件存储省去了装数据库的麻烦用 Jackson 或 Gson 序列化联系人列表就行。缺点是 JavaFX 在部分 JDK 版本里需要单独引入模块文件存储的并发和查询性能不如数据库。我一般会推荐第一种组合因为课程设计的验收场景通常是老师在你电脑上跑一遍看功能是否完整、代码是否规范。Swing JDBC 的代码结构更传统分层清晰老师一眼就能看出你用了 MVC 模式。而且数据库操作能体现你对 SQL 的掌握加分项更多。下面是一个典型的联系人表结构设计CREATE TABLE contact ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, email VARCHAR(100), group_name VARCHAR(30), remark VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );字段说明id是自增主键方便后续修改和删除时定位记录name和phone设为非空因为通讯录的核心就是这两项group_name用来分类比如“家人”“同事”“客户”create_time记录创建时间方便按时间排序。如果老师要求支持多个电话可以再加一张contact_phone表做一对多关联但课程设计一般不需要这么复杂。2.3 分层架构别把代码全塞进一个类里很多同学写课程设计时习惯把所有代码写在一个Main类里按钮事件里直接写 SQL界面和业务逻辑混在一起。这样写当时爽后面改一个字段要翻几百行代码老师一看就皱眉头。合理的做法是分层。常见的是三层结构表现层UI、业务逻辑层Service、数据访问层DAO。表现层只负责展示和接收用户输入业务逻辑层处理校验和流程控制数据访问层只管和数据库打交道。实体类Model单独放一个包贯穿三层。这样分的好处是改界面不影响业务逻辑换数据库不影响界面。老师问你“如果换成文件存储怎么办”你可以说“只需要改 DAO 层的实现Service 和 UI 不用动”。这就是加分项。3. 目录结构与核心模块拆解从入口到数据落库3.1 项目目录结构一个标准的 Maven 项目结构如下contact-manager/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/contact/ │ │ │ ├── Main.java // 程序入口 │ │ │ ├── model/ │ │ │ │ └── Contact.java // 联系人实体 │ │ │ ├── dao/ │ │ │ │ ├── ContactDao.java // DAO 接口 │ │ │ │ └── ContactDaoImpl.java // JDBC 实现 │ │ │ ├── service/ │ │ │ │ └── ContactService.java // 业务逻辑 │ │ │ ├── ui/ │ │ │ │ ├── MainFrame.java // 主窗口 │ │ │ │ └── ContactDialog.java // 新增/编辑弹窗 │ │ │ └── util/ │ │ │ └── DBUtil.java // 数据库连接工具 │ │ └── resources/ │ │ └── db.properties // 数据库配置 └── pom.xml这个结构里model包放实体类dao包放数据访问代码service包放业务逻辑ui包放界面util包放工具类。每个包职责单一找代码的时候直接按包名定位。3.2 实体类与数据库连接先看实体类Contact.javapackage com.example.contact.model; public class Contact { private int id; private String name; private String phone; private String email; private String groupName; private String remark; // 无参构造JDBC 反射需要 public Contact() {} // 新增时用的构造不需要 id public Contact(String name, String phone, String email, String groupName, String remark) { this.name name; this.phone phone; this.email email; this.groupName groupName; this.remark remark; } // getter 和 setter 省略实际项目里要写全 public int getId() { return id; } public void setId(int id) { this.id id; } public String getName() { return name; } public void setName(String name) { this.name name; } // ... 其他 getter/setter }实体类的作用是承载数据。注意这里保留了一个无参构造因为后面用 JDBC 从ResultSet映射对象时通常会先new一个空对象再逐个set。如果你用 MyBatis 或 Hibernate无参构造也是必须的。再看数据库连接工具DBUtil.javapackage com.example.contact.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.util.Properties; public class DBUtil { private static String url; private static String username; private static String password; // 静态块在类加载时读取配置文件 static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); url props.getProperty(db.url); username props.getProperty(db.username); password props.getProperty(db.password); Class.forName(props.getProperty(db.driver)); } catch (Exception e) { throw new RuntimeException(数据库配置加载失败, e); } } public static Connection getConnection() throws Exception { return DriverManager.getConnection(url, username, password); } }这段代码的关键点配置文件db.properties放在resources目录下打包后会自动进入 classpath。静态块只执行一次避免每次获取连接都读文件。Class.forName加载驱动MySQL 8 以上用com.mysql.cj.jdbc.Driver。连接用完记得关闭后面 DAO 里会用 try-with-resources 处理。db.properties内容示例db.drivercom.mysql.cj.jdbc.Driver db.urljdbc:mysql://localhost:3306/contact_db?useSSLfalseserverTimezoneUTC db.usernameroot db.password1234563.3 DAO 层增删改查的 JDBC 实现DAO 层是课程设计的核心老师重点看的就是这里。先定义接口package com.example.contact.dao; import com.example.contact.model.Contact; import java.util.List; public interface ContactDao { void insert(Contact contact) throws Exception; void update(Contact contact) throws Exception; void delete(int id) throws Exception; Contact findById(int id) throws Exception; ListContact findAll() throws Exception; ListContact searchByName(String keyword) throws Exception; }接口定义好之后实现类ContactDaoImpl.java里写具体 SQLpackage com.example.contact.dao; import com.example.contact.model.Contact; import com.example.contact.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class ContactDaoImpl implements ContactDao { Override public void insert(Contact contact) throws Exception { 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, contact.getName()); ps.setString(2, contact.getPhone()); ps.setString(3, contact.getEmail()); ps.setString(4, contact.getGroupName()); ps.setString(5, contact.getRemark()); ps.executeUpdate(); } } Override public ListContact findAll() throws Exception { String sql SELECT * FROM contact ORDER BY create_time DESC; ListContact list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); } } return list; } Override public ListContact searchByName(String keyword) throws Exception { String sql SELECT * FROM contact WHERE name LIKE ? OR phone LIKE ?; ListContact list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { String pattern % keyword %; ps.setString(1, pattern); ps.setString(2, pattern); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); } } } return list; } // 把 ResultSet 当前行映射成 Contact 对象 private Contact mapRow(ResultSet rs) throws SQLException { 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.setGroupName(rs.getString(group_name)); c.setRemark(rs.getString(remark)); return c; } }这里有几个关键点。第一用PreparedStatement而不是Statement防止 SQL 注入这是课程设计的必查项。第二try-with-resources自动关闭连接、语句和结果集避免资源泄漏。第三mapRow方法把结果集映射抽出来多个查询方法复用减少重复代码。第四搜索用LIKE加%通配符支持模糊匹配姓名和电话。update和delete方法结构类似只是 SQL 不同这里不再重复贴。注意update的 SQL 要带WHERE id ?delete同理。3.4 Service 层校验与业务逻辑Service 层不是简单转发 DAO 调用它要处理业务规则。比如新增联系人时姓名和电话不能为空电话格式要校验重复电话要提示。这些逻辑放在 Service 里UI 层只管调用。package com.example.contact.service; import com.example.contact.dao.ContactDao; import com.example.contact.dao.ContactDaoImpl; import com.example.contact.model.Contact; import java.util.List; public class ContactService { private ContactDao dao new ContactDaoImpl(); public void addContact(Contact contact) throws Exception { // 校验必填字段 if (contact.getName() null || contact.getName().trim().isEmpty()) { throw new IllegalArgumentException(姓名不能为空); } if (contact.getPhone() null || contact.getPhone().trim().isEmpty()) { throw new IllegalArgumentException(电话不能为空); } // 校验电话格式简单用正则 if (!contact.getPhone().matches(\\d{7,15})) { throw new IllegalArgumentException(电话格式不正确); } dao.insert(contact); } public ListContact listAll() throws Exception { return dao.findAll(); } public ListContact search(String keyword) throws Exception { if (keyword null || keyword.trim().isEmpty()) { return dao.findAll(); } return dao.searchByName(keyword.trim()); } public void deleteContact(int id) throws Exception { dao.delete(id); } }Service 层的校验逻辑是老师常问的点。为什么不在 UI 层校验因为 UI 层可能被替换比如从 Swing 换成 Web 界面校验逻辑不应该跟着重写。放在 Service 层任何调用方都能复用同一套规则。3.5 UI 层Swing 主窗口与表格刷新UI 层用 Swing 实现主窗口包含一个JTable展示联系人列表顶部有搜索框和按钮底部有新增、编辑、删除按钮。核心是表格的数据模型和刷新逻辑。package com.example.contact.ui; import com.example.contact.model.Contact; import com.example.contact.service.ContactService; import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.awt.*; import java.util.List; public class MainFrame extends JFrame { private ContactService service new ContactService(); private DefaultTableModel tableModel; private JTable table; private JTextField searchField; public MainFrame() { setTitle(通讯录管理系统); setSize(800, 500); setDefaultCloseOperation(EXIT_ON_CLOSE); setLocationRelativeTo(null); initUI(); refreshTable(); } private void initUI() { // 表格列名 String[] columns {ID, 姓名, 电话, 邮箱, 分组, 备注}; tableModel new DefaultTableModel(columns, 0) { Override public boolean isCellEditable(int row, int column) { return false; // 表格只读编辑走弹窗 } }; table new JTable(tableModel); // 顶部搜索栏 JPanel topPanel new JPanel(new FlowLayout(FlowLayout.LEFT)); searchField new JTextField(15); JButton searchBtn new JButton(搜索); searchBtn.addActionListener(e - doSearch()); topPanel.add(new JLabel(姓名/电话)); topPanel.add(searchField); topPanel.add(searchBtn); // 底部操作按钮 JPanel bottomPanel new JPanel(new FlowLayout(FlowLayout.RIGHT)); JButton addBtn new JButton(新增); JButton editBtn new JButton(编辑); JButton deleteBtn new JButton(删除); addBtn.addActionListener(e - openDialog(null)); editBtn.addActionListener(e - { int row table.getSelectedRow(); if (row 0) { JOptionPane.showMessageDialog(this, 请先选中一行); return; } int id (int) tableModel.getValueAt(row, 0); openDialog(id); }); deleteBtn.addActionListener(e - doDelete()); bottomPanel.add(addBtn); bottomPanel.add(editBtn); bottomPanel.add(deleteBtn); add(topPanel, BorderLayout.NORTH); add(new JScrollPane(table), BorderLayout.CENTER); add(bottomPanel, BorderLayout.SOUTH); } private void refreshTable() { try { ListContact list service.listAll(); tableModel.setRowCount(0); // 清空旧数据 for (Contact c : list) { tableModel.addRow(new Object[]{ c.getId(), c.getName(), c.getPhone(), c.getEmail(), c.getGroupName(), c.getRemark() }); } } catch (Exception ex) { JOptionPane.showMessageDialog(this, 加载失败 ex.getMessage()); } } private void doSearch() { try { ListContact list service.search(searchField.getText()); tableModel.setRowCount(0); for (Contact c : list) { tableModel.addRow(new Object[]{ c.getId(), c.getName(), c.getPhone(), c.getEmail(), c.getGroupName(), c.getRemark() }); } } catch (Exception ex) { JOptionPane.showMessageDialog(this, 搜索失败 ex.getMessage()); } } private void doDelete() { int row table.getSelectedRow(); if (row 0) { JOptionPane.showMessageDialog(this, 请先选中一行); return; } int id (int) tableModel.getValueAt(row, 0); int confirm JOptionPane.showConfirmDialog(this, 确定删除); if (confirm JOptionPane.YES_OPTION) { try { service.deleteContact(id); refreshTable(); } catch (Exception ex) { JOptionPane.showMessageDialog(this, 删除失败 ex.getMessage()); } } } private void openDialog(Integer id) { // 打开新增/编辑弹窗传 null 表示新增 ContactDialog dialog new ContactDialog(this, id); dialog.setVisible(true); refreshTable(); // 弹窗关闭后刷新表格 } public static void main(String[] args) { SwingUtilities.invokeLater(() - new MainFrame().setVisible(true)); } }这段代码里DefaultTableModel的isCellEditable返回false让表格只读避免用户直接在表格里改数据导致状态不一致。refreshTable每次先setRowCount(0)清空再重新填充这是 Swing 表格刷新的标准做法。搜索和删除操作后都调用刷新保证界面和数据同步。ContactDialog是新增/编辑弹窗里面放JTextField接收输入点保存时调用service.addContact或service.updateContact。这里不再展开结构类似。4. 避坑与常见问题课程设计里最容易翻车的地方4.1 数据库连不上报 ClassNotFoundException现象程序启动时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因MySQL 驱动 jar 包没有加到项目依赖里。如果你用 Mavenpom.xml里没写mysql-connector-java依赖如果是普通项目WEB-INF/lib或 classpath 里没有驱动 jar。解决Maven 项目在pom.xml加依赖dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency普通项目把 jar 包放到lib目录然后在 IDE 里右键添加到 classpath。注意 MySQL 8 以上驱动类名是com.mysql.cj.jdbc.Driver不是旧的com.mysql.jdbc.Driver。4.2 中文乱码姓名存进去变成问号现象联系人姓名是中文存到数据库后变成???或者界面显示乱码。原因数据库字符集不是utf8mb4或者 JDBC URL 没指定编码。解决建库时指定字符集CREATE DATABASE contact_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;JDBC URL 加上useUnicodetruecharacterEncodingutf8db.urljdbc:mysql://localhost:3306/contact_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneUTC如果已经建了库用ALTER DATABASE contact_db CHARACTER SET utf8mb4;修改。4.3 表格数据不刷新改了数据库界面没变现象新增联系人后数据库里有了但表格还是旧数据。原因refreshTable没有在新增/编辑/删除后调用或者tableModel没有清空旧数据直接追加。解决每次数据变更后调用refreshTable()并且刷新时先tableModel.setRowCount(0)清空。不要用tableModel.fireTableDataChanged()单独触发因为数据源没变的话界面不会更新。4.4 关闭连接报错或者连接数过多现象程序运行一段时间后报Too many connections或者关闭窗口时抛异常。原因DAO 里获取的Connection没有关闭或者关闭顺序不对。解决统一用try-with-resources把Connection、PreparedStatement、ResultSet都放在 try 的括号里JVM 会自动按相反顺序关闭。不要手动在finally里写close()容易漏或者顺序错。4.5 搜索没结果但数据库里明明有数据现象输入姓名搜索返回空列表。原因SQL 里用了而不是LIKE或者%拼错了位置。解决模糊搜索用LIKE ?参数拼成% keyword %。如果搜电话注意电话字段是字符串类型不要当数字处理。另外检查keyword有没有trim()用户输入前后空格会导致匹配失败。5. 二次开发与改造从通讯录到通用管理系统5.1 换数据库从 MySQL 到 SQLite课程设计验收时老师可能不想装 MySQL。把数据库换成 SQLite 是最省事的改造。SQLite 不需要服务端一个文件就是数据库。改造步骤第一pom.xml把 MySQL 驱动换成sqlite-jdbc。第二db.properties改驱动和 URLdb.driverorg.sqlite.JDBC db.urljdbc:sqlite:contact.db db.username db.password第三建表 SQL 里AUTO_INCREMENT改成AUTOINCREMENTDATETIME DEFAULT CURRENT_TIMESTAMP改成DEFAULT (datetime(now))。DAO 层代码几乎不用动因为 JDBC API 是统一的。这就是分层的好处——换存储只改配置和少量 SQL。5.2 加功能分组筛选与导入导出通讯录管理系统最容易扩展的方向是分组和导入导出。分组筛选只需要在 Service 层加一个listByGroup(String group)方法DAO 里加WHERE group_name ?查询UI 顶部加一个下拉框选择分组。导入导出用 CSV 格式最方便。导出时遍历ListContact每行拼成name,phone,email,group,remark写入文件。导入时用BufferedReader逐行读取按逗号拆分调用service.addContact。注意 CSV 里如果字段本身含逗号要用双引号包裹简单做法是导出前把逗号替换成中文逗号。// 导出 CSV 的核心逻辑 public void exportToCsv(ListContact list, String filePath) throws Exception { try (BufferedWriter writer new BufferedWriter( new OutputStreamWriter(new FileOutputStream(filePath), UTF-8))) { writer.write(姓名,电话,邮箱,分组,备注\n); for (Contact c : list) { writer.write(String.join(,, c.getName(), c.getPhone(), c.getEmail(), c.getGroupName(), c.getRemark()) \n); } } }5.3 改造验证怎么确认改完没坏每次改造后按这个顺序验证先跑findAll看列表能不能加载再跑insert看新增后表格是否刷新然后跑search看模糊匹配是否正常最后跑delete看删除后数据是否消失。如果换了数据库重点验证建表语句和自增主键是否兼容。如果加了导入功能用一个包含中文和特殊字符的 CSV 测试确认编码没问题。我一般会在改造前把原始版本跑一遍记下每个功能的预期结果改完后逐项对比。这样即使出了问题也能快速定位是哪一步改坏了。5.4 一个实用技巧用接口隔离变化如果你打算把这个项目写进简历或者作为毕业设计的起点建议把 DAO 层设计成可替换的。定义一个ContactRepository接口然后分别写JdbcContactRepository和FileContactRepository两个实现。Service 层依赖接口而不是具体实现通过工厂或依赖注入决定用哪个。public interface ContactRepository { void save(Contact contact) throws Exception; ListContact findAll() throws Exception; // ... 其他方法 } public class RepositoryFactory { public static ContactRepository create(String type) { if (file.equals(type)) { return new FileContactRepository(); } return new JdbcContactRepository(); } }这样老师问你“如果不用数据库怎么办”你可以直接切换实现代码不用大改。这个技巧在课程设计答辩时很加分因为它体现了你对面向接口编程的理解。从那以后我每次做课程设计都会先把接口定好再写实现哪怕功能再简单也强制走一遍。这样后面加功能或者换方案的时候不会因为牵一发动全身而翻车。希望帮到你。本文还有配套的精品资源点击获取