简介:这是一套基于Java Swing与MySQL开发的GUI图书管理系统完整工程,面向高校计算机专业学生课程设计与Java GUI开发初学者,解决双角色(学生/管理员)场景下的图书借阅、入库、读者管理等核心业务需求。资源包共184个文件,含35个带完整注释的Java源码、101个编译后Class文件、18张界面图标(PNG)、15张功能截图(JPG)、3个MySQL建库建表SQL脚本及1份配套课程报告(DOCX),整体压缩后24.47MB,结构清晰,便于按模块理解MVC分层逻辑与Swing事件驱动机制。已有601人学习下载,提供可直接导入Eclipse运行的完整项目(兼容MyEclipse),含登录、借还书、书籍增删改查、读者管理等9个核心视图及6张数据库表,代码规范、注释详实,适合作为Java桌面应用开发的实践范例与课程设计参考。
1. 为什么现在还有人用 Java + Swing + MySQL 做图书管理系统?不是早该淘汰了吗?
这不是怀旧,而是现实约束下的理性选择。高校计算机专业大三、大四学生做课程设计或毕业设计时,常被明确要求“必须使用桌面端技术栈”“不得基于 Web 框架”“需体现数据库交互与 GUI 控件联动能力”——这时 Java + Swing + MySQL 就成了最稳妥、最易交付、最能覆盖考核点的组合。它不追求高并发或云原生,但要求:启动即用、界面可操作、增删改查闭环、数据持久化可靠、代码结构清晰可答辩。Swing 虽无现代化 UI 框架的响应式能力,却胜在 JDK 自带、零依赖、调试直观;MySQL 则因安装成熟、语法标准、Navicat 可视化友好,成为教学场景下最易上手的关系型数据库。本文面向真实开发场景:从零搭建一个可运行、可调试、可扩展的图书管理桌面应用,重点讲清 Swing 组件如何绑定 MySQL 数据、事务怎么控制、中文乱码怎么根治、JTable 怎么动态刷新、以及毕业答辩常被追问的“为什么不用 JavaFX 或 Spring Boot”。
2. 搭建最小可运行环境:JDK 17 + MySQL 8.0 + IDE 配置要点
2.1 JDK 与 MySQL 版本协同逻辑必须理清
Java 17 是当前 LTS 版本,Swing 在 JDK 17 中仍完整保留(未被移除),且支持模块化隔离;MySQL 8.0 提供了更严格的默认认证方式(caching_sha2_password)和更规范的 SQL 模式,但与 JDBC 8.0.33+ 兼容性极佳。关键点在于:JDBC 驱动版本必须与 MySQL 服务端版本严格匹配。若使用 MySQL 8.0.33,必须选用 mysql-connector-java-8.0.33.jar(或更高补丁版),不可混用 5.x 驱动——否则连接时会抛出java.sql.SQLException: Unknown system variable 'query_cache_size'。下载地址应为官方 Maven 仓库(https://repo1.maven.org/maven2/mysql/mysql-connector-java/),而非第三方镜像站,避免驱动包被篡改。
提示:不要用
mysql-connector-java-8.0.33.jar的“免安装版”概念。所谓“免安装”仅指无需 Windows 安装程序,但 jar 包本身必须显式添加到项目 classpath,IDE 中需手动配置 Build Path,不能靠环境变量自动加载。
2.2 MySQL 初始化:字符集、用户权限、初始库表一步到位
图书管理系统对中文支持是刚需,必须在创建数据库时指定utf8mb4字符集,而非utf8(MySQL 的utf8实际只支持 3 字节 UTF-8,无法存储 emoji 和部分生僻汉字)。执行以下 SQL:
CREATE DATABASE book_management DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; USE book_management; CREATE TABLE books ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(17) NOT NULL UNIQUE, title VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), publish_date DATE, price DECIMAL(10,2), stock INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE USER 'bookapp'@'localhost' IDENTIFIED BY 'Book@2024'; GRANT SELECT, INSERT, UPDATE, DELETE ON book_management.* TO 'bookapp'@'localhost'; FLUSH PRIVILEGES;注意COLLATE utf8mb4_0900_ai_ci是 MySQL 8.0 默认排序规则,比utf8mb4_general_ci更准确支持中文排序;GRANT语句中明确限定主机为'localhost',避免开放远程访问权限——这是教学环境安全底线。
2.3 IntelliJ IDEA / Eclipse 工程配置实操步骤
以 IntelliJ IDEA 2023.3 为例(Eclipse 同理):
- 新建 Project → 选择 “Java” → SDK 选 JDK 17;
- 右键项目 →
Open Module Settings→Modules→Dependencies→+→JARs or directories→ 选中下载好的mysql-connector-java-8.0.33.jar; - 在
src/main/resources下新建db.properties文件,内容如下:
jdbc.url=jdbc:mysql://localhost:3306/book_management?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=bookapp jdbc.password=Book@2024注意:
serverTimezone=Asia/Shanghai必须显式设置,否则publish_date字段读写会出现 14 小时偏差;allowPublicKeyRetrieval=true是 MySQL 8.0+ 连接必需参数,用于绕过公钥交换验证(教学环境可接受,生产环境需配置 SSL)。
3. Swing 界面与 MySQL 数据层的双向绑定实现
3.1 设计分层架构:为什么不能把 SQL 写死在 ActionListener 里?
常见错误是直接在按钮点击事件中拼接 SQL 字符串并执行,导致代码不可测试、事务难控制、SQL 注入风险高。正确做法是划分三层:
- View 层:JFrame + JPanel + JButton + JTextField + JTable,只负责展示和触发事件;
- Controller 层:BookController 类,接收 View 事件,调用 Service 方法,更新 View 状态;
- Model 层:BookService + BookDAO + Book 实体类,封装业务逻辑与数据库操作。
这种分层让JTable的数据刷新、新增校验、删除确认等逻辑可独立单元测试,也便于答辩时解释“MVC 思想落地”。
3.2 BookDAO 的核心实现:PreparedStatement 防注入 + 事务控制
public class BookDAO { private static final String INSERT_SQL = "INSERT INTO books (isbn, title, author, publisher, publish_date, price, stock) VALUES (?, ?, ?, ?, ?, ?, ?)"; private static final String SELECT_ALL_SQL = "SELECT id, isbn, title, author, publisher, publish_date, price, stock FROM books ORDER BY created_at DESC"; public void insert(Book book) throws SQLException { try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(INSERT_SQL, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, book.getIsbn()); ps.setString(2, book.getTitle()); ps.setString(3, book.getAuthor()); ps.setString(4, book.getPublisher()); ps.setDate(5, new java.sql.Date(book.getPublishDate().getTime())); ps.setBigDecimal(6, book.getPrice()); ps.setInt(7, book.getStock()); int affectedRows = ps.executeUpdate(); if (affectedRows == 0) { throw new SQLException("Insert failed, no rows affected"); } try (ResultSet rs = ps.getGeneratedKeys()) { if (rs.next()) { book.setId(rs.getInt(1)); } } } } public List<Book> findAll() throws SQLException { List<Book> books = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(SELECT_ALL_SQL)) { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book book = new Book(); book.setId(rs.getInt("id")); book.setIsbn(rs.getString("isbn")); book.setTitle(rs.getString("title")); book.setAuthor(rs.getString("author")); book.setPublisher(rs.getString("publisher")); book.setPublishDate(rs.getDate("publish_date")); book.setPrice(rs.getBigDecimal("price")); book.setStock(rs.getInt("stock")); books.add(book); } } } return books; } }关键参数说明:
Statement.RETURN_GENERATED_KEYS:让PreparedStatement返回自增主键,避免二次查询;ps.setDate(5, new java.sql.Date(...)):java.util.Date不能直接传给setDate(),必须转为java.sql.Date;try-with-resources:自动关闭 Connection/PreparedStatement/ResultSet,防止连接泄漏;ps.setString()替代ps.executeUpdate("INSERT INTO...'" + title + "'"):彻底杜绝 SQL 注入。
3.3 JTable 动态刷新机制:DefaultTableModel 的正确用法
Swing 中 JTable 与数据源解耦,必须通过DefaultTableModel中转。错误做法是每次查询后table.setModel(new DefaultTableModel(...))——这会导致列宽重置、排序状态丢失。正确做法是复用模型并清空后填充:
private void loadBooksToTable() { try { List<Book> books = bookService.findAll(); DefaultTableModel model = (DefaultTableModel) table.getModel(); model.setRowCount(0); // 清空现有行,保留列定义 for (Book book : books) { model.addRow(new Object[]{ book.getId(), book.getIsbn(), book.getTitle(), book.getAuthor(), book.getPublisher(), book.getPublishDate(), book.getPrice(), book.getStock() }); } } catch (SQLException e) { JOptionPane.showMessageDialog(this, "加载图书失败:" + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } }注意:
model.setRowCount(0)是关键,它保留JTable的列标题、列宽、渲染器等 UI 状态,仅清除数据行。若用new DefaultTableModel(...)重建,则所有列宽需重新table.getColumnModel().getColumn(i).setPreferredWidth(...)设置,答辩时极易被问“为什么每次刷新界面都变窄”。
4. 图书管理核心功能落地:增删改查 + 中文输入全流程验证
4.1 新增图书:ISBN 格式校验与重复拦截
ISBN-13 是当前国际标准,格式为 13 位数字,可能含连字符(如978-7-04-050694-6),但数据库字段应存纯数字。校验逻辑需在 Controller 层完成:
private boolean isValidIsbn(String isbn) { if (isbn == null) return false; String digitsOnly = isbn.replaceAll("\\D", ""); // 移除非数字字符 return digitsOnly.length() == 13 && digitsOnly.chars().allMatch(Character::isDigit); } private void handleAddBook() { String isbn = isbnField.getText().trim(); if (!isValidIsbn(isbn)) { JOptionPane.showMessageDialog(this, "ISBN 格式错误,请输入 13 位数字(可含连字符)", "输入错误", JOptionPane.WARNING_MESSAGE); return; } try { if (bookService.existsByIsbn(isbn)) { JOptionPane.showMessageDialog(this, "该 ISBN 已存在,请勿重复录入", "重复提示", JOptionPane.INFORMATION_MESSAGE); return; } Book book = new Book(); book.setIsbn(isbn.replaceAll("\\D", "")); book.setTitle(titleField.getText().trim()); book.setAuthor(authorField.getText().trim()); book.setPublisher(publisherField.getText().trim()); book.setPublishDate(Date.valueOf(publishDateField.getText().trim())); // 输入格式:2023-01-01 book.setPrice(new BigDecimal(priceField.getText().trim())); book.setStock(Integer.parseInt(stockField.getText().trim())); bookService.add(book); JOptionPane.showMessageDialog(this, "添加成功!"); loadBooksToTable(); // 刷新表格 } catch (NumberFormatException e) { JOptionPane.showMessageDialog(this, "价格或库存必须为数字", "输入错误", JOptionPane.WARNING_MESSAGE); } catch (SQLException e) { JOptionPane.showMessageDialog(this, "数据库操作失败:" + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } }参数说明:
isbn.replaceAll("\\D", ""):正则\\D匹配所有非数字字符,确保入库为纯数字;Date.valueOf("2023-01-01"):java.time.LocalDate不被 JDBC 4.2 直接支持,此处用java.sql.Date.valueOf()转换;bookService.existsByIsbn():DAO 层需实现SELECT COUNT(*) FROM books WHERE isbn = ?,避免先查后插的竞态条件。
4.2 删除图书:带事务回滚的级联确认
删除操作必须二次确认,且需捕获异常回滚事务。Swing 中不能阻塞 EDT(事件调度线程),因此确认对话框必须同步弹出:
private void handleDeleteBook() { int selectedRow = table.getSelectedRow(); if (selectedRow == -1) { JOptionPane.showMessageDialog(this, "请先选择要删除的图书", "提示", JOptionPane.INFORMATION_MESSAGE); return; } Long id = (Long) table.getValueAt(selectedRow, 0); // 假设第一列为 id int confirm = JOptionPane.showConfirmDialog( this, "确定要删除 ID 为 " + id + " 的图书吗?此操作不可撤销。", "确认删除", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE ); if (confirm != JOptionPane.YES_OPTION) return; try { bookService.delete(id); JOptionPane.showMessageDialog(this, "删除成功!"); loadBooksToTable(); } catch (SQLException e) { JOptionPane.showMessageDialog(this, "删除失败:" + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } }注意:
JOptionPane.showConfirmDialog()是模态对话框,会阻塞当前线程直到用户点击,这是 Swing 单线程模型下的正确交互方式。切勿用SwingWorker异步弹窗——会导致 UI 线程错乱。
4.3 修改图书:编辑态与保存态的状态管理
JTable 默认不可编辑,需重写isCellEditable()并监听单元格编辑结束事件:
table.getModel().addTableModelListener(e -> { if (e.getType() == TableModelEvent.UPDATE) { int row = e.getFirstRow(); int column = e.getColumn(); if (column >= 0 && column <= 6) { // 仅允许编辑 title/author/publisher/price/stock try { Book book = getBookAtRow(row); // 根据 row 从 model 获取原始 Book 对象 String newValue = (String) table.getValueAt(row, column); // 根据 column 更新 book 对应字段... bookService.update(book); } catch (SQLException ex) { JOptionPane.showMessageDialog(this, "保存失败:" + ex.getMessage()); loadBooksToTable(); // 回滚显示 } } } });实际项目中建议采用“双击进入编辑模式”的交互:点击某行后,弹出JDialog表单修改,而非直接编辑表格——更符合图书管理业务逻辑,也避免用户误操作。
5. 排查高频故障:中文乱码、连接超时、JTable 不刷新的根因与修复
5.1 中文乱码三重校验清单
乱码问题 90% 出现在字符集未统一,需逐层检查:
| 层级 | 检查项 | 正确值 | 验证命令 |
|---|---|---|---|
| MySQL 服务端 | character_set_server | utf8mb4 | SHOW VARIABLES LIKE 'character_set_server'; |
| 数据库 & 表 | DEFAULT CHARSET | utf8mb4 | SHOW CREATE DATABASE book_management;SHOW CREATE TABLE books; |
| JDBC URL | characterEncoding参数 | 不加此项(由utf8mb4自动推导) | URL 中禁止出现&characterEncoding=utf8 |
注意:JDBC URL 中显式加
characterEncoding=utf8会强制使用 MySQL 的utf8(3 字节),反而导致插入生僻字时报错Incorrect string value。正确做法是仅靠utf8mb4字符集 +Collation驱动自动适配。
5.2 连接池缺失导致的“Too many connections”错误
教学项目常忽略连接复用,每操作一次就new Connection(),MySQL 默认最大连接数 151,10 个用户同时操作即爆。解决方案不是调高max_connections,而是用 HikariCP:
<!-- pom.xml --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.0.1</version> </dependency>public class DBUtil { private static HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/book_management?useSSL=false&serverTimezone=Asia/Shanghai"); config.setUsername("bookapp"); config.setPassword("Book@2024"); config.setDriverClassName("com.mysql.cj.jdbc.Driver"); config.setMaximumPoolSize(10); // 教学环境 10 足够 config.setMinimumIdle(2); config.setConnectionTimeout(30000); dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }5.3 JTable 不刷新的 4 个隐藏原因与验证方法
当loadBooksToTable()执行后表格无变化,按顺序排查:
- 数据源为空:在
findAll()返回前加System.out.println("Found " + books.size() + " books");,确认 DAO 是否真查到数据; - EDT 线程违规:
loadBooksToTable()是否在非 EDT 线程调用?用SwingUtilities.isEventDispatchThread()检查,非 EDT 必须包装SwingUtilities.invokeLater(...); - TableModel 列数不匹配:
model.addColumn()与model.addRow()的 Object[] 长度必须一致,差 1 位就会静默失败; - JTable 被重复 add():检查
getContentPane().add(table)是否执行多次,导致新 table 覆盖旧 table,旧 table 仍持有老数据。
验证方法:在loadBooksToTable()结尾加System.out.println("Table row count: " + table.getRowCount());,若输出0但 DAO 返回非空列表,问题必在model.setRowCount(0)后未addRow(),或addRow()的 Object[] 有 null 值触发异常被捕获。
6. 毕业答辩加分技巧:ER 图绘制、JUnit 测试覆盖率、打包成可执行 jar
6.1 用 draw.io 绘制符合评分标准的 ER 图
答辩要求“提供系统 ER 图”,不能手绘拍照,需用工具生成矢量图。核心实体只有books表,但需体现:
- 主键
id(PK)、外键(本系统无外键,故不画); - 属性类型标注:
isbn: VARCHAR(17)、price: DECIMAL(10,2); - 中文注释:每个字段旁加小号中文说明(如“ISBN 编号”“图书定价”);
- 关系线:
books自身无关联,但可加注释框:“本系统暂未实现借阅记录、读者信息等扩展模块”。
提示:draw.io 导出为 PNG 时勾选 “Transparent background”,插入 Word 文档后无白边;导出 PDF 用于打印,分辨率更高。
6.2 为 BookDAO 编写 JUnit 5 测试用例(覆盖增删查)
@ExtendWith(MockitoExtension.class) class BookDAOTest { @Mock private Connection connection; @Mock private PreparedStatement preparedStatement; @Mock private ResultSet resultSet; @Test void testFindAll_ReturnsBooks() throws SQLException { // Given BookDAO dao = new BookDAO(); // 使用 H2 内存数据库替代 MySQL,避免依赖外部服务 Connection h2Conn = DriverManager.getConnection("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "sa", ""); h2Conn.createStatement().executeUpdate("CREATE TABLE books(id INT PRIMARY KEY, title VARCHAR(100))"); h2Conn.createStatement().executeUpdate("INSERT INTO books VALUES(1, 'Java编程思想')"); // When List<Book> books = new BookDAO().findAll(); // Then assertEquals(1, books.size()); assertEquals("Java编程思想", books.get(0).getTitle()); } }使用 H2 内存数据库(h2依赖)替代真实 MySQL,测试不依赖环境,mvn test即可运行,答辩时可现场演示测试通过率。
6.3 打包成双击运行的 jar:MANIFEST.MF 与资源路径陷阱
IntelliJ 打包时,src/main/resources/db.properties必须在 jar 包根目录,否则ClassLoader.getSystemResourceAsStream("db.properties")返回 null。正确配置:
File → Project Structure → Artifacts → + → JAR → From modules with dependencies;- 在
Output Layout中,确认db.properties出现在root directory下(而非lib/或resources/子目录); Manifest中设置Main-Class: com.example.BookApp(你的主类全路径);- 构建后执行
java -jar book-system.jar,若报Could not find or load main class,说明 Manifest 缺失或路径错误。
验证 jar 内部结构:jar -tf book-system.jar | grep properties应输出db.properties,且无前缀路径。
本文还有配套的精品资源,点击获取