Java+MySQL图书管理系统开发实战:Swing界面与JDBC数据层设计
2026/9/13 4:36:24 网站建设 项目流程

简介:基于Java与MySQL实现的GUI图书管理系统课程设计资源,面向需要完成类似选题的高校学生及Java初学者,解决管理员登录、图书增删改查、用户检索、借阅归还等核心模块的落地问题。包内共187个文件,包含48个Java源文件、64个编译后的class文件、58个PNG界面截图与4个SQL数据库脚本,另有docx/doc说明文档和MD笔记,压缩包整体16.25MB,目录结构清晰,便于按需求查看对应代码与资料。已有998人学习下载。借助完整源码、数据库建表脚本和图形界面截图,可快速理解Swing组件与MySQL交互的编程思路,也能直接参考课程设计文档结构,适合用于期末项目、毕业设计或自学练手。结合博主s1t16整理,内容体量适中,可帮助学习者在较短时间内掌握图书管理系统的开发全流程,是一份兼具代码参考与报告示例的实用资源。

1. 从课程设计到线上工具:Java+MySQL 图书管理系统在解决什么

把“基于 Java+MySQL 实现(GUI)图书管理系统”翻译成大白话:用 Java 写一个带界面的桌面应用,数据落在 MySQL,把图书的录入、查询、借还、统计跑通。这个组合占据了高校课程设计和中小型内部工具的半壁江山——不是因为新,而是因为稳:Java 的类型安全、MySQL 的轻量部署、GUI 的直观操作,三者叠加,两周内可交付一个能演示、能答辩、能扩展的系统。

这套方案的三个关键点是表结构怎么设计才不返工、JDBC 连接层参数怎么配才不埋坑、Swing 界面怎么和事件模型解耦。目标是你拿到标题后,不管做答辩、演示还是接真实业务,都有一条可复现的路径。

2. 分层架构与 MySQL 表设计:先定边界再写代码

任何一个有 GUI 的 Java 项目,最容易翻车的地方不是界面写不出来,而是数据和界面耦合在一起,改一个字段要动三个文件。我一般会先按三层结构把目录立起来:view放界面类,controller处理按钮事件和窗口跳转,servicedao负责业务与数据库访问。对于上千行的图书管理系统,这个分层能让你在答辩时清楚说出“某次借书操作经过哪几个类”,而不是被问到时在 IDE 里乱翻。

2.1 GUI 框架选型:Swing 还是 JavaFX

标题里写的是 GUI,没有限定框架。到 2025 年这个时间点,Swing 依然是课程设计和轻量内部工具的主流选择,原因很实际:JDK 自带、不需要额外模块、资料存量极大,十年前的老案例换个 JDK 版本几乎都能直接跑。JavaFX 在动画和样式上更现代,但要额外引入javafx.controls等模块,对“演示完就交付”的场景反而增加了环境变量配置的成本。

如果你的目标是快速跑通图书管理系统,我会选 Swing 配GridBagLayoutGroupLayout;如果你打算在界面上做图表统计(比如月度借阅量柱状图),JavaFX 的BarChart组件会更省力。下面这张表帮你快速决策:

维度SwingJavaFX
JDK 自带是,javax.swing否,需引入javafx.*模块
资料存量极大相对少,官方文档清晰
表格/树组件JTable功能够用TableView更现代
样式定制手动设置LookAndFeelCSS 直接控制
打包体积略大,需带 runtime

我在实际项目里用 Swing 的另一个理由是JTable配合DefaultTableModel可以直接把ResultSet的行数据映射进表格,一个循环就够。JavaFX 的TableView需要为每列写cellValueFactory,代码量直接翻倍。不是说 JavaFX 不好,而是“图书管理系统”这个粒度的事务,Swing 的性价比更高。

2.2 MySQL 表结构设计:四张表比三张表更抗折腾

很多课程设计的数据库只有bookuser两张表,借书还书行为通过修改book表的“是否被借”字段表达。这能跑通演示,但审计和统计做不了。我一般会上四张表:bookuserborrow_recordcategorycategory把分类字段从book表拆出来,避免“文学”“计算机”这种字符串在每本书里重复存储。

CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE ); CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, author VARCHAR(100), isbn VARCHAR(20) UNIQUE, category_id INT, total INT NOT NULL DEFAULT 1, available INT NOT NULL DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES category(id) ); CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, role TINYINT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP, return_time DATETIME, status TINYINT NOT NULL DEFAULT 0, FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (book_id) REFERENCES book(id) );

book表的totalavailable分开存,是为了支持同一本书有多个副本的场景。借书时先检查available > 0,再执行UPDATE book SET available = available - 1 WHERE id = ? AND available > 0,用单条 SQL 的原子性避免并发把库存扣成负数。borrow_record表的status字段用 0 表示借出、1 表示已还,后续按时间范围做借阅统计时,只查这张表就够了。

注意isbn加了UNIQUE约束。图书系统里同一本书的不同版本 ISBN 通常不同,这个字段如果不加唯一索引,录入时重复数据会越来越多。如果你们的馆藏真的存在同 ISBN 多册的物理副本,那要把isbn的唯一约束去掉,靠id区分记录,或者单独建一张book_copy表记录每本实体书。大多数管理系统不会有这个需求,但设计时想清楚这一点,能避免后期改表。

2.3 JDBC 连接参数:连接串里的每个参数都值得写明白

数据层最容易被忽略的是DriverManager.getConnection()那一行。很多人用jdbc:mysql://localhost:3306/library这种裸连接串,本地跑没问题,换到别人的电脑上就报乱码和时区错误。推荐模板是:

String url = "jdbc:mysql://localhost:3306/library" + "?useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai" + "&useSSL=false&allowPublicKeyRetrieval=true"; String user = "root"; String password = "yourpassword";

characterEncoding=utf8解决中文乱码;serverTimezone必须和本机时区一致,否则 MySQL 8.x 会直接拒绝连接;useSSL=false是本地开发禁用加密连接,减少握手开销;allowPublicKeyRetrieval=true是 MySQL 8.0 默认认证插件caching_sha2_password在非 SSL 场景下首次连接时必须设置的参数,不配会偶发Public Key Retrieval is not allowed异常。

注意:不要忽略serverTimezone。中国时区环境里如果配成UTC,写入时间会差 8 个小时,而且这个差异在测试阶段往往发现不了,等统计报表上线才暴露。

这三组参数建议写成一个private static final String,放在单独的DBUtil类里,并在静态块中执行Class.forName("com.mysql.cj.jdbc.Driver")。MySQL 8.x 的驱动类名是com.mysql.cj.jdbc.Driver,和 5.x 时代的旧类名不同,这是升级驱动后最常见的坑,下一章细说。

3. 从 DAO 到 Swing 界面:把增删改查变成可交互的窗口

表结构和连接参数定了之后,接下来的问题就是:代码组织成什么样,才不至于在加了十几个按钮之后把actionPerformed写成一坨几百行的 if 判断。这一章把“图书管理”这个模块的完整链路走一遍,从 DAO 到界面事件,每一步贴出可抄的最小实现。

3.1 DAO 层:用 PreparedStatement 而不是 Statement

图书查询在系统里出现频率最高,按名字模糊查询、按 ISBN 精确查询、按分类浏览是三个最典型的入口。下面这段代码实现了按条件动态拼接查询:

public List<Book> searchBooks(String keyword, Integer categoryId) { List<Book> result = new ArrayList<>(); StringBuilder sql = new StringBuilder( "SELECT b.id, b.title, b.author, b.isbn, c.name AS category_name, b.total, b.available "); sql.append("FROM book b LEFT JOIN category c ON b.category_id = c.id WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (keyword != null && !keyword.trim().isEmpty()) { sql.append("AND (b.title LIKE ? OR b.author LIKE ? OR b.isbn LIKE ?) "); String like = "%" + keyword.trim() + "%"; params.add(like); params.add(like); params.add(like); } if (categoryId != null) { sql.append("AND b.category_id = ? "); params.add(categoryId); } sql.append("ORDER BY b.id DESC"); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql.toString())) { for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); } try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book book = new Book(); book.setId(rs.getInt("id")); book.setTitle(rs.getString("title")); book.setAuthor(rs.getString("author")); book.setIsbn(rs.getString("isbn")); book.setCategoryName(rs.getString("category_name")); book.setTotal(rs.getInt("total")); book.setAvailable(rs.getInt("available")); result.add(book); } } } catch (SQLException e) { e.printStackTrace(); } return result; }

这里有两个关键设计。第一,用StringBuilder拼接 SQL 骨架,用ArrayList收集参数,本质是把动态查询参数化——WHERE 1=1看着有点丑,但省掉了判断WHEREAND谁先谁后的逻辑,是动态查询拼接的常见手法。第二,PreparedStatement的参数绑定用setObject,由 JDBC 驱动自动处理类型转换,比手动拼字符串安全得多,也顺带免疫了 SQL 注入。

注意connectionpreparedStatementresultSet都用了 try-with-resources,不需要手动close()。连接泄漏在 GUI 程序里特别隐蔽:界面上操作看不出问题,但连接池会慢慢耗尽,跑一晚上第二天必挂。

3.2 Swing 界面的监听器设计:一个 actionCommand 走天下

界面部分最常见的反面教材是给每个按钮写一个匿名内部类监听器,十几个按钮就有十几个new ActionListener(),代码不好读,出错了也不知道去哪个块里找。我的做法是让主窗体实现ActionListener,在actionPerformed里用e.getActionCommand()分发事件:

public class MainFrame extends JFrame implements ActionListener { private JTable bookTable; private DefaultTableModel tableModel; private BookDao bookDao = new BookDao(); private JTextField searchField; private JButton addBtn; private JButton deleteBtn; public MainFrame() { setTitle("图书管理系统"); setSize(1000, 600); setDefaultCloseOperation(EXIT_ON_CLOSE); initUI(); refreshBookTable(null, null); } private void initUI() { JPanel topPanel = new JPanel(); searchField = new JTextField(18); JButton searchBtn = new JButton("查询"); searchBtn.setActionCommand("SEARCH"); searchBtn.addActionListener(this); addBtn = new JButton("新增"); addBtn.setActionCommand("ADD"); addBtn.addActionListener(this); // 省略布局:把 searchField、searchBtn、addBtn 放进 topPanel tableModel = new DefaultTableModel( new Object[]{"ID", "书名", "作者", "ISBN", "分类", "总册数", "可借"}, 0); bookTable = new JTable(tableModel); // 省略布局:JScrollPane 包裹 bookTable 放入 contentPane deleteBtn = new JButton("删除选中"); deleteBtn.setActionCommand("DELETE"); deleteBtn.addActionListener(this); // 省略布局:deleteBtn 放入 bottomPanel } @Override public void actionPerformed(ActionEvent e) { switch (e.getActionCommand()) { case "SEARCH": refreshBookTable(searchField.getText(), null); break; case "ADD": new AddBookDialog(this, bookDao).setVisible(true); refreshBookTable(null, null); break; case "DELETE": int row = bookTable.getSelectedRow(); if (row == -1) { JOptionPane.showMessageDialog(this, "请先选中一行"); return; } int id = (int) tableModel.getValueAt(row, 0); if (bookDao.deleteById(id) > 0) { JOptionPane.showMessageDialog(this, "删除成功"); refreshBookTable(null, null); } break; default: break; } } }

setActionCommand是 Swing 里一个容易被新手跳过的方法。如果不显式设置,getActionCommand()返回的是按钮文本,代码里一旦把按钮文字从“查询”改成“搜索图书”,整个事件分发就静默失效。显式指定动作命令相当于给事件起了个稳定的逻辑名,是界面文案和业务逻辑解耦的第一步。

DefaultTableModel的构造参数里,第一个数组是列名,第二个0是初始行数。刷新表格数据时不要直接操作JTable,而是先清空tableModel.setRowCount(0),再逐行addRow()JTable不持有数据,它只是TableModel的视图。理解了这一点,表格刷新闪烁的问题也就理解了。

3.3 借书还书的事务边界:一条 SQL 不足以表达一个业务

借书在业务上要同时做两件事:写一条borrow_record,同时把book.available减一。这两件事必须在一个事务里,否则会出现“记录写了但库存没减”的数据不一致。

public boolean borrowBook(int userId, int bookId) { String updateSql = "UPDATE book SET available = available - 1 WHERE id = ? AND available > 0"; String insertSql = "INSERT INTO borrow_record(user_id, book_id, status) VALUES(?, ?, 0)"; Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); PreparedStatement ps1 = conn.prepareStatement(updateSql); ps1.setInt(1, bookId); int affected = ps1.executeUpdate(); if (affected == 0) { conn.rollback(); return false; } PreparedStatement ps2 = conn.prepareStatement(insertSql); ps2.setInt(1, userId); ps2.setInt(2, bookId); ps2.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { try { if (conn != null) conn.setAutoCommit(true); } catch (SQLException e) { e.printStackTrace(); } } }

UPDATE book SET available = available - 1 WHERE id = ? AND available > 0这个写法值得单独说。它不是先SELECT available再在 Java 里判断是否大于 0,而是把条件直接写进 UPDATE 的 WHERE 子句。MySQL 的锁机制会把满足条件的行锁住,在同一行上并发的两次扣减不会都成功,executeUpdate()返回 0 说明没抢到锁或者库存已经为 0。这是处理库存类字段的典型手法,比“查出来-判断-更新”的三步走少一次网络往返,也没有竞态窗口,这条在 Java 面试八股文里也是高频考点。

事务结束后要把setAutoCommit(true)恢复,否则连接归还给连接池时还带着false状态,下一个使用者会莫名发现自己的 SQL 不生效。这也是很多初学者查不出原因的问题。

4. MySQL 驱动与 JDBC 参数:图书系统最常见的十个坑

代码能跑通和“在任何一台机器上都跑得通”是两回事。这一章集中处理环境相关的硬问题:MySQL 驱动怎么选、连接池要不要上、中文乱码和常见报错的排查顺序。这些都是做图书管理系统时最容易卡住的点。

4.1 MySQL 驱动与版本匹配:8.x 的驱动不能只看类名

网上的老教程还在用com.mysql.jdbc.Driver,如果你用的 MySQL 是 8.x,并且引入的是新驱动,旧类名虽然还能用但会输出弃用警告,满屏的 deprecation 提示容易让人误判代码有错。真正的ClassNotFoundException一般不是类名写错,而是 jar 没放进 classpath,或者 Maven 坐标写错。

<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency>

注意mysql-connector-j是从 8.0.31 开始替代旧坐标mysql-connector-java的。版本号不用追新,8.0.33 是兼容性很好的稳定版本,配合 MySQL 5.7 或 8.0 都可以。如果你的 JDK 还是 8,这个驱动也支持;但如果用 HikariCP 连接池,5.x 要求 Java 11,Java 8 环境请把 HikariCP 降到 4.0.3。

如果你不用 Maven,手工下载 jar 放到项目 lib 目录,记得在 IDE 里把 jar 加到项目构建路径。NoClassDefFoundError的报错里有一半是 jar 没真正放进 classpath,而不是 jar 本身有问题。

4.2 连接池选型与参数:HikariCP 的最小配置

课程设计轮不到连接池,但如果这个系统要接真实用户,DriverManager每次新建连接的代价就不可忽略。MySQL 的连接建立要经过 TCP 握手、认证、初始化会话,一次约几十到一百多毫秒,高频借还操作下这个开销会被直接放大。HikariCP 是目前 Java 生态里吞吐量最优的连接池,裸 JDBC 项目也可以单独引入:

<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.1.0</version> </dependency>
HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/library" + "?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"); config.setUsername("root"); config.setPassword("yourpassword"); config.setDriverClassName("com.mysql.cj.jdbc.Driver"); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); config.setIdleTimeout(600000); HikariDataSource dataSource = new HikariDataSource(config);

maximumPoolSize对桌面 GUI 应用来说 10 就够,一个界面最多同时开几个窗口,连接数需求远低于高并发服务端。minimumIdle=2让池子至少保留两个空闲连接,避免频繁建连。connectionTimeout=30000是等 30 秒拿不到连接就报错,本地一般不触发;idleTimeout是空闲连接回收时间,配 10 分钟即可。记住一个原则:连接池的配置不是越大越好,池子太大反而浪费内存。

换成连接池之后,DBUtil.getConnection()的实现从DriverManager.getConnection(url, user, password)变成dataSource.getConnection(),接口不变,业务层代码一行都不用改,这就是面向接口编程的红利。

4.3 高频报错排查速查表

以下把基于 Java + MySQL + Swing 做图书管理系统时遇到频率最高的报错列出来,每条都带排查顺序。

报错最常见原因排查步骤
ClassNotFoundException: com.mysql.cj.jdbc.Driver依赖没引入或坐标写错检查 jar 是否在 classpath;确认 Maven 坐标是mysql-connector-j
Public Key Retrieval is not allowedMySQL 8 默认认证插件需要公钥连接 URL 加allowPublicKeyRetrieval=true
Communications link failureMySQL 未启动、端口不通或连接串错误ping 127.0.0.1,再telnet 127.0.0.1 3306
Unknown database 'library'库没建执行CREATE DATABASE library DEFAULT CHARACTER SET utf8mb4;
中文显示为?连接 URL 未指定编码或库字符集不对确认 URL 参数、数据库字符集、IDE 文件编码三者一致

Communications link failure是最容易踩的。有个容易忽略的盲区:MySQL 8.x 默认监听 3306,但如果机器上装了多个 MySQL 实例,或者用 Docker 起的容器没映射端口,3306 不通的概率比想象中大。排查顺序永远是先确认服务在跑、再确认端口通、最后才怀疑代码。

中文显示成?这个坑,绝大多数情况不是代码能解决的。先看SHOW VARIABLES LIKE 'character_set%',确认数据库字符集是utf8mb4再回头改连接串。如果数据库已经建好但用的是旧字符集,更省事的方案是导出数据重建库,别试图用ALTER TABLE ... CONVERT TO CHARACTER SET去逐列救,字段多的时候容易顾此失彼。排查字符集我一般直接打开 MySQL Workbench,在 Navigator 里右键数据库选 Alter Database 看默认字符集,比命令行敲 SHOW 语句直观。

5. Java 端角色权限与统计报表:让图书系统从演示到可用

系统能跑通之后,花半天时间做三件事:加上管理员和普通用户两种角色、补一个简单的借阅统计、把连接配置外置。这三件事不改动已经写好的功能逻辑,但会让系统在答辩或交付时的观感完全不同。

5.1 基于 role 字段的权限控制

user表里已经预留了role TINYINT字段,默认 0 表示普通读者,1 表示管理员。权限控制的粒度不需要做到按钮级,只在两个位置判断即可:登录后把当前用户角色存在静态上下文里;界面初始化时根据角色决定哪些按钮可见。

if ("ADMIN".equals(currentUser.getRole())) { addBtn.setVisible(true); deleteBtn.setVisible(true); } else { addBtn.setVisible(false); deleteBtn.setVisible(false); }

这个方案够用,比引入 Spring Security 轻得多。后续如果要做借阅网点,role可以扩展为枚举映射,注意别用字符串直接存角色名,字符串在 SQL 里做判断总是比= 1多一步类型转换的心智负担。

5.2 借阅量统计:一条 GROUP BY 就完成

统计功能最基础的是“按分类统计在馆量”和“按月统计借阅量”。后者一张图放答辩 PPT 上效果很好,SQL 只需要聚合borrow_record

SELECT DATE_FORMAT(borrow_time, '%Y-%m') AS month, COUNT(*) AS borrow_count FROM borrow_record WHERE borrow_time >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH) GROUP BY DATE_FORMAT(borrow_time, '%Y-%m') ORDER BY month;

这个查询用DATE_FORMATborrow_time截断到月份,DATE_SUB取最近 6 个月,GROUP BY按月聚合。界面层用JTable展示结果;如果当初选了 JavaFX,换成BarChart只需把查询结果两列喂给XYChart.Series,半小时能完成。

5.3 验证:从“能跑”到“经得起问”

演示前的验证建议按这个顺序走一遍:第一,用不存在的书名搜索,确认不报错、返回空表;第二,把某本书库存借到 0 再借一次,确认弹出“库存不足”提示;第三,关掉 MySQL 服务再启动系统,确认弹窗提示数据库连接失败而不是界面卡死;第四,连续新增两本 ISBN 相同的书,确认第二条被唯一约束拦下来。这四条覆盖了空数据、边界、外部依赖、数据冲突四类最常见问题。

连接信息不要硬编码在DBUtil里,放到db.properties文件,用Properties类加载。换数据库机器时只需要改配置文件,不用重新编译,这对演示环境迁移能省掉一次打包时间。把db.properties独立出来,后续接配置中心接口也容易——这是 Java 桌面程序走向正式部署时要养成的习惯。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询