Java课设宠物领养系统实战:Swing+JDBC+MySQL从建表到答辩
简介这是一份基于Java Swing与MySQL实现的宠物领养系统完整课设源码包适合Java课程设计、毕业设计入门及需要快速落地项目的开发者参考。系统包含完整的业务代码、界面素材与数据库脚本覆盖宠物信息管理、领养记录等核心模块能够帮助使用者理解GUI事件处理与JDBC操作的典型配合方式。资源包共84个文件以class编译文件、java源码、png界面截图和xml配置为主附带sql数据库脚本、docx课程设计报告和mp4演示视频整体大小约33.13MB结构清晰便于对照学习。已有502人浏览学习过该资源。代码风格保持原始交付形态功能可正常运行适合作为Java课设的完整参考方案。1. 宠物领养系统的课设定位Swing 是老技术但它的分层逻辑并不过时做 Java 课设时很多人一听到 GUI/Swing 就先入为主觉得“技术太老、拿不出手”。但实际上Swing 项目恰恰是检验 Java 基础功力的最快路径它强迫你处理事件分发、界面与数据模型的解耦、数据库连接的生命周期管理——这些东西在后续接触 Spring Boot 或 Android 开发时都会原样出现。宠物领养系统就是这样一个典型用 JFrame 搭壳、JTable 承载数据、JDBC 读写 MySQL把增删改查从控制台搬到图形界面覆盖了 Java 课程里最核心的几块知识点。这套源码是接单交付后留下的数据库用 MySQL、界面用 Swing、IDE 直接用 IntelliJ IDEA 打开就能跑。它的价值不在于界面有多华丽而在于完整地演示了一个小型管理系统的工程结构。下面我会把项目的数据库设计、JDBC 连接层、界面事件绑定和课设答辩时的演示技巧逐一拆开不绕弯子直接讲代码和踩过坑的地方。2. 从 pets.sql 看数据建模四张表如何撑起一套领养业务2.1 把“系统”拆回数据库管理员、宠物、领养申请的三层关系打开压缩包里的pets.sql你会发现这套系统的表结构非常克制总共四张核心表没有多余的冗余字段。建模时遵循的思路是先理清业务角色再画实体之间的关系。宠物领养业务的角色很清晰——管理员维护宠物信息用户浏览宠物并发起领养申请管理员审核申请后更新宠物状态。这三个动作对应到数据库里就自然分出管理员表、宠物表、领养申请表再加上一个支撑登录注册的用户表。这种建模方式与你平时在课程里学的“学生-课程-选课”三张表如出一辙核心就是通过外键把业务动作串起来。很多同学做课设时喜欢一张表塞十几个字段恨不得把用户头像、宠物照片、申请备注全塞进去结果查询时 JOIN 写得乱七八糟。这套系统的表结构更接近真实项目的最小可行模型每张表只存自己的核心属性表与表之间用 ID 关联。2.2 建表脚本逐段拆解主键设计、时间戳默认值和外键约束先看pets.sql中宠物表的关键片段CREATE TABLE pet ( id int(11) NOT NULL AUTO_INCREMENT, pet_name varchar(50) NOT NULL COMMENT 宠物名称, pet_type varchar(20) DEFAULT NULL COMMENT 类别dog/cat/rabbit, age int(11) DEFAULT NULL COMMENT 年龄月, health_status varchar(100) DEFAULT NULL COMMENT 健康状况描述, adopt_status tinyint(4) DEFAULT 0 COMMENT 0-待领养 1-已领养, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段建表脚本有几个细节值得展开。id用AUTO_INCREMENT做主键保证插入数据时不需要显式传 ID由 InnoDB 自增维护这是小型管理系统的惯用做法。adopt_status用tinyint而不是varchar存储状态0 和 1 两个取值在 Java 端映射为Integer类型判断时用比较就够了比字符串比较更高效也更安全。created_at设置了DEFAULT CURRENT_TIMESTAMP插入时无需手动赋值JDBC 的PreparedStatement里少绑一个参数这个技巧在做新增功能时很实用。再来看领养申请表的外键设计CREATE TABLE apply ( id int(11) NOT NULL AUTO_INCREMENT, pet_id int(11) NOT NULL, user_id int(11) NOT NULL, apply_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, status tinyint(4) DEFAULT 0 COMMENT 0-待审批 1-通过 2-拒绝, PRIMARY KEY (id), KEY idx_pet_id (pet_id), KEY idx_user_id (user_id), CONSTRAINT fk_apply_pet FOREIGN KEY (pet_id) REFERENCES pet (id), CONSTRAINT fk_apply_user FOREIGN KEY (user_id) REFERENCES user (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;apply表本身带着pet_id和user_id两个外键分别指向宠物表和用户表领养行为被抽象成一条申请记录。这里做了两个索引idx_pet_id和idx_user_id它们是给外键查询准备的。实际写 SQL 时WHERE pet_id ?这类条件能直接走索引不会全表扫描。外键约束fk_apply_pet的作用是保证引用完整性——你无法插入一条指向不存在宠物的申请记录这在课设答辩演示时是一个可以主动展示的亮点。2.3 为什么需要四张表而不是三张权限分离的红线有同学会问既然管理员和用户都是人为什么不能合成一张表用role字段区分答案是权限边界。管理员表里存的是后台登录凭据用户表里存的是前台领养人信息两者字段不同、操作权限不同混在一起会让代码里的权限判断遍布if (role 1)这种散弹式逻辑。这套系统把两张表分开Java 端的AdminDao和UserDao也独立编写后续要加管理员操作日志时只需要改管理员相关的类不会牵连用户模块。提示导入pets.sql时如果报错优先检查 MySQL 版本是否支持utf8mb4字符集。MySQL 5.5.3 以下版本不认识这个字符集建议直接使用 5.7 或 8.0 版本。3. JDBC 连接层从 DriverManager 到连接池的演化逻辑3.1 MYSQL 驱动注册与连接的封装方式压缩包里的src目录下数据库连接部分通常是一个独立的工具类常见命名是MysqlConnector或DBUtil。核心功能就是两件事加载驱动、获取连接。看一下典型的实现public class MysqlConnector { private static final String URL jdbc:mysql://localhost:3306/pets?useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (ps ! null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }Class.forName(com.mysql.cj.jdbc.Driver)放在静态代码块里类加载时执行一次驱动只注册一次。URL 末尾的useSSLfalse是 MySQL 8.0 的常见配置本地开发不需要 SSL 加密链路不写这个参数会看到大量 SSL 告警日志。serverTimezoneAsia/Shanghai解决的是时区问题MySQL 8.0 默认使用 UTC不指定时区会导致java.sql.SQLException的日期时间字段错乱。close()方法里的三层判断是为了防止空指针——ResultSet或PreparedStatement可能为null。3.2 DAO 模式下的 SQL 注入防线PreparedStatement 参数绑定连接层解决的是“怎么连”的问题DAO 层解决的是“怎么查”的问题。以宠物搜索功能为例PetDao里查找待领养宠物的方法如下public ListPet findPetsByType(String type) { ListPet list new ArrayList(); String sql SELECT id, pet_name, pet_type, age, health_status, adopt_status FROM pet WHERE adopt_status 0 ; if (type ! null !type.trim().isEmpty()) { sql AND pet_type ?; } try (Connection conn MysqlConnector.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { if (type ! null !type.trim().isEmpty()) { ps.setString(1, type); } try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Pet pet new Pet(); pet.setId(rs.getInt(id)); pet.setPetName(rs.getString(pet_name)); pet.setPetType(rs.getString(pet_type)); pet.setAge(rs.getInt(age)); pet.setHealthStatus(rs.getString(health_status)); pet.setAdoptStatus(rs.getInt(adopt_status)); list.add(pet); } } } catch (SQLException e) { e.printStackTrace(); } return list; }这里的查询条件是动态拼接的当type参数为空时SQL 退化为查全部待领养宠物有值时追加AND pet_type ?。用PreparedStatement而不是Statement核心原因是参数占位符?会被 JDBC 预编译传进来的字符串即使包含单引号或OR 11也会被转义从根上堵住了 SQL 注入。try-with-resources语法自动关闭Connection和PreparedStatement不需要手动调用close()代码比传统写法干净不少。3.3 连接释放的两个隐藏坑事务边界与血淋淋的 TooManyConnectionsDriverManager.getConnection()每次调用都会新建一条物理连接使用频率高时性能会快速劣化。这套系统是单机课设并发量极小所以直接使用DriverManager完全够用。但如果你在答辩演示时连续快速点击查询按钮仍然要把释放资源的逻辑写对。观察下面的反例// 错误写法 Connection conn MysqlConnector.getConnection(); PreparedStatement ps conn.prepareStatement(SELECT * FROM pet); ResultSet rs ps.executeQuery(); // 忘记关闭 conn, ps, rs连接不关闭的直接后果是 MySQL 的max_connections默认 151被快速耗尽后续请求全部报Too many connections。更隐蔽的坑是事务边界问题如果一次领养操作包含“插入申请记录”和“更新宠物状态”两步必须把两个 DAO 调用包在同一个事务里否则会出现申请已提交但宠物状态未变更的数据不一致。正确做法是在业务层手动关闭conn.setAutoCommit(false)操作完成后统一commit()异常时rollback()。提示全部代码跑通后可以在MysqlConnector里加一个static int activeConnectionCount计数器在getConnection()和close()里增减并打印用来验收连接是否全部释放——这是一个能写进课设报告里的“自验步骤”。4. Swing 界面与事件绑定从 JFrame 到 JTable 的 MVC 落地4.1 主窗口结构拆分面板嵌套与组件布局策略Swing 的界面搭建逻辑是“面板套面板”。主窗口MainFrame继承JFrame内容面板设置为BorderLayout北区放查询条件和操作按钮中间区域放JScrollPane包裹的JTable南区放状态栏。核心布局代码public class MainFrame extends JFrame { private JTable petTable; private DefaultTableModel tableModel; public MainFrame() { setTitle(宠物领养系统); setSize(900, 600); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JPanel topPanel new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); JComboBoxString typeBox new JComboBox(new String[]{全部, dog, cat, rabbit}); JButton searchBtn new JButton(查询); JButton adoptBtn new JButton(领养申请); topPanel.add(new JLabel(宠物类别:)); topPanel.add(typeBox); topPanel.add(searchBtn); topPanel.add(adoptBtn); tableModel new DefaultTableModel( new String[]{ID, 名称, 类别, 年龄, 健康状况, 状态}, 0); petTable new JTable(tableModel); JScrollPane scrollPane new JScrollPane(petTable); getContentPane().add(topPanel, BorderLayout.NORTH); getContentPane().add(scrollPane, BorderLayout.CENTER); } }DefaultTableModel的构造参数是“列名数组 初始行数 0”后续通过addRow()动态追加数据。JTable只负责展示模型中的数据不直接操作数据库这正好对应 Swing 的 MVC 模式——JTable是视图DefaultTableModel是模型DAO 层是数据源。setLocationRelativeTo(null)让窗口居中显示这是容易被忽略但体验差异明显的细节。4.2 事件监听线程模型为什么不能在 ActionListener 里睡线程按钮的事件绑定通过addActionListener完成。以下是查询按钮的监听器实现searchBtn.addActionListener(e - { String selectedType (String) typeBox.getSelectedItem(); ListPet petList new PetDao().findPetsByType( 全部.equals(selectedType) ? null : selectedType); tableModel.setRowCount(0); for (Pet pet : petList) { tableModel.addRow(new Object[]{ pet.getId(), pet.getPetName(), pet.getPetType(), pet.getAge(), pet.getHealthStatus(), pet.getAdoptStatus() 0 ? 待领养 : 已领养 }); } });查询按钮触发时事件分发线程EDTEvent Dispatch Thread会执行PetDao().findPetsByType()的数据库查询。注意setRowCount(0)先把表格清空再逐行addRow这个顺序保证刷新时不会出现旧数据残留。如果你的查询逻辑比较复杂不要在监听器里直接执行因为 EDT 是单线程模型——耗时操作会阻塞整个界面的重绘和按钮点击窗口看起来就像“卡死”了。这个现象在课设答辩演示时最容易翻车评委点击查询后如果转圈超过两秒会直接怀疑代码有问题。setRowCount(0)清空模型后表格列头不会重置因为列名是在DefaultTableModel构造时写死的。adopt_status 0 ? 待领养 : 已领养这行把数据库里的 0/1 整数翻译成界面上的用户友好文案属于典型的“数据展示层转换”不要在 SQL 里用CASE WHEN去做这件事——Java 端处理更直观改起来也更方便。4.3 领养申请状态流转从待审批到通过的界面反馈闭环领养申请不是点一下就结束的它有完整的状态机。用户在宠物列表选中一行点击“领养申请”按钮系统插入一条apply记录状态为 0待审批。管理员在另一个窗口看到申请列表点击“通过”后apply.status变为 1同时pet.adopt_status变为 1。这个联动操作跨两张表必须放在事务里public boolean approveApply(int applyId, int petId) { String updateApply UPDATE apply SET status 1 WHERE id ?; String updatePet UPDATE pet SET adopt_status 1 WHERE id ?; try (Connection conn MysqlConnector.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(updateApply); PreparedStatement ps2 conn.prepareStatement(updatePet)) { ps1.setInt(1, applyId); ps1.executeUpdate(); ps2.setInt(1, petId); ps2.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); return false; } } catch (SQLException e) { e.printStackTrace(); return false; } }setAutoCommit(false)是事务开启的标志。ps1.executeUpdate()执行申请状态更新ps2.executeUpdate()执行宠物状态更新任何一步抛出SQLException都会进入 catch 块执行rollback()让已执行的更新全部回滚。这段代码是课设报告里最有技术含金量的部分——它演示了跨表事务处理是“及格”和“优秀”的分水岭。领养申请按钮监听器还涉及表格行选中校验int selectedRow petTable.getSelectedRow(); if (selectedRow 0) { JOptionPane.showMessageDialog(this, 请先选中一条宠物记录, 提示, JOptionPane.WARNING_MESSAGE); return; }没有选中行就点击按钮时getSelectedRow()返回 -1这时弹出警告框拦截。这个校验动作能在答辩时展示你的工程意识。5. 从启动到验收演示脚本设计、MYSQL 环境变量与二次封装思路演示项目时最有效的准备方式是提前踩一遍自己的运行环境。先把 MySQL 服务启动用命令行或 Navicat 执行pets.sql确认库和表已建好然后用 IDEA 打开项目等 Maven 或 Gradle 依赖解析完成如果项目本身不带构建工具直接把src作为源码根目录导入运行包含main方法的启动类。这里有一个高频踩坑点MySQL 安装在命令行直接输入mysql报“不是内部或外部命令”这是环境变量没有配置C:\Program Files\MySQL\MySQL Server 8.0\bin导致的需要在系统 PATH 里追加该路径后重开终端验证mysql --version。演示时的操作路径建议固定为启动程序登录管理员账号 → 添加一只新宠物 → 切换用户视角浏览宠物列表 → 发起领养申请 → 切回管理员审批 → 确认宠物状态变为已领养。这条路径覆盖了增删改查的所有分支而且每一步都有界面反馈。如果演示过程网络波动或数据库卡顿优先检查MysqlConnector的 URL 是否写成了localhost而 MySQL 实际监听在127.0.0.1或者防火墙拦截了 3306 端口。一个通用排错指令是netstat -ano | findstr :3306这条命令用来验证 MySQL 是否在监听 3306 端口。如果输出有LISTENING状态但程序连不上接下来看ping 127.0.0.1是否通则确认端口未对外暴露——本地课设无需改 bind-address保持默认值即可。如果netstat无输出说明 MySQL 服务没起来在 Windows 服务管理器里启动MySQL80服务后再跑程序。提示IDEA 里运行项目时如果控制台输出Access denied for user rootlocalhost说明密码与MysqlConnector里的PASSWORD常量不一致优先排查这个值而不是重装 MySQL。答辩前的界面调优遵循“小而准”的改动策略。打开JFrame的构造函数把setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)保留这是窗口关闭即退出程序的标准行为。如果感觉表格显示太拥挤把petTable.setRowHeight(28)加上让每行高度更宽松。想进一步展示细节可以在Pet实体类里重写toString()方法将宠物 ID、名称、健康状态拼成一个可读字符串方便调试时直接打印。这些小的体验优化不需要引入额外的美化库纯 Swing 代码就能完成对“加分项”的贡献比追求花哨外观更实在。如果你拿到源码后想二次开发我建议改的方向是追加一个“宠物图片路径”字段——在pet表加image_path varchar(255)界面用ImageIcon加载本地图片显示在JLabel上。这个改动只涉及一张表、一个实体字段和一个界面组件却能显著提升完整度。注意图片路径不要存绝对路径用相对路径或项目根目录拼接避免换机器后路径失效。本文还有配套的精品资源点击获取