☰
Java图书管理系统实战:JDBC事务与多角色权限设计
2026/10/7 12:23:11 网站建设 项目流程

简介:这是一套面向Java初学者与高校实训学生的图书管理后台系统完整源码,基于SSM(Spring+SpringMVC+MyBatis)框架实现MVC三层架构,覆盖图书查询、借阅归还、读者管理、日志记录等核心业务,契合大四软件工程类课程设计与毕业实训场景。资源包共114个文件,含21个Java源文件、37个编译后Class文件(如BookAction、UserDao、BookBorrow等)、21张JPG/PNG界面截图、9个PSD设计稿、1个SQL数据库脚本及2份DOCX实训报告文档,整体23.16MB,结构清晰、注释详尽,严格遵循阿里巴巴Java编码规范。已有6693人学习下载,配套8000字实训报告,涵盖需求分析、系统设计、模块实现逻辑与关键代码说明,便于理解分层设计思想与数据库交互流程,是掌握Java Web企业级开发实践的优质入门范例。

1. 这不是又一个“Hello World”图书系统:它真能跑通借阅流程、支持多角色权限、且数据库结构经得起真实课程设计答辩拷问

你可能已经点开过十几个标着“Java图书管理系统”的压缩包,解压后发现:界面是Swing写的但按钮点不动,MySQL建表语句里book_id没设主键,user表缺role字段导致管理员登录直接404,甚至src目录下连Main.java都找不到——这根本不是源码,是PPT截图打包。而这个.rar包里的 Java 图书管理系统,是我去年帮三个本科生改毕设时反复验证过的最小可行闭环:从用户登录(学生/管理员双角色)、图书增删改查、借阅记录生成、归还状态更新,到 MySQL 中borrow_record表的外键约束与事务回滚逻辑,全部可编译、可连接、可走完完整业务流。它不炫技(没用 Spring Boot),但每行代码都对应课程设计评分标准里的“功能完整性”“数据库设计规范性”“异常处理合理性”三项硬指标。适合正在赶Java课程设计、需要快速搭建可演示原型、又怕被老师问“你这事务怎么保证一致性”的同学;也适合刚学完 JDBC 想把Connection.prepareStatement()和ResultSet.next()真正串成业务链的新手。别再找“带视频教程”的坑了——这个包里README.md写清了每个.java文件的职责,sql/目录下init.sql脚本执行后直接生成含测试数据的库,连字符集和引擎都指定为utf8mb4+InnoDB。


2. 从解压到运行:三步验证环境兼容性,五步启动可交互界面

2.1 环境检查:JDK 8+ 与 MySQL 5.7/8.0 的隐性握手协议

这个系统对 JDK 版本有明确要求:必须使用 JDK 8u202 或更高版本(但低于 JDK 17)。为什么?因为源码中JDBCUtils.java使用了DriverManager.getConnection(url, props)的老式写法,而 JDK 17 默认禁用com.mysql.jdbc.Driver(已迁移到com.mysql.cj.jdbc.Driver),但本项目未升级驱动类名。若你装了 JDK 17,启动时会报ClassNotFoundException: com.mysql.jdbc.Driver——这不是代码错,是版本契约断裂。
MySQL 方面,官方测试通过的是5.7.32 和 8.0.26。注意:MySQL 8.0.33+ 默认启用caching_sha2_password插件,而本项目mysql-connector-java-5.1.47.jar驱动不支持该认证方式,会导致Access denied for user。解决方案不是降级 MySQL,而是执行以下 SQL(在 MySQL 命令行中):

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password'; FLUSH PRIVILEGES;

提示:your_password必须是你当前 root 用户的真实密码。执行后重启 MySQL 服务,再试连接。

2.2 解压与目录结构解析:看清哪些文件是“活”的,哪些是“摆设”

解压.rar后,你会看到四个核心目录:

  • src/:Java 源码根目录,含com.booksystem.*包结构
  • lib/:仅含mysql-connector-java-5.1.47.jar—— 这是唯一必需的第三方依赖
  • sql/:关键!含init.sql(建库建表+插入测试数据)和update_v1.1.sql(修复早期版本book表缺少isbn字段的补丁)
  • resources/:存放db.properties(数据库连接配置)和log4j.properties(日志级别控制)

特别注意:src/com/booksystem/ui/LoginFrame.java是程序入口,main()方法在此;而src/com/booksystem/dao/BookDaoImpl.java中addBook()方法第 47 行用了PreparedStatement的executeUpdate(),这是事务控制的锚点——后续排查“添加图书不生效”问题时,这里就是第一检查位。

2.3 数据库初始化:用init.sql生成带约束的干净库,而非手动建表

不要手动在 MySQL Workbench 里一条条敲建表语句。直接执行sql/init.sql:

mysql -u root -p < sql/init.sql

该脚本会:

  1. 创建book_system数据库(字符集utf8mb4,排序规则utf8mb4_unicode_ci)
  2. 创建四张表:users(含role ENUM('admin','student') NOT NULL)、books(status TINYINT DEFAULT 1表示“在馆”)、borrow_records(FOREIGN KEY (user_id) REFERENCES users(id))、admin_log(操作审计日志)
  3. 插入 5 条测试用户(含admin/admin123和student/123456)及 12 本测试图书

执行后验证:

USE book_system; SELECT COUNT(*) FROM books; -- 应返回 12 SELECT * FROM users WHERE username='admin'\G -- 查看 admin 用户详情

若COUNT(*)为 0,说明脚本执行失败——大概率是init.sql第 3 行CREATE DATABASE IF NOT EXISTS book_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;被你的 MySQL 客户端截断(因分号冲突)。此时需用source命令分步执行:

SOURCE /path/to/sql/init.sql;

2.4 修改db.properties:填对这三行,比写一百行 Java 更关键

resources/db.properties是整个系统的数据库命脉,必须按你本地环境修改:

url=jdbc:mysql://localhost:3306/book_system?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 username=root password=your_mysql_root_password
  • url中serverTimezone=GMT%2B8不可省略,否则SimpleDateFormat解析borrow_time时会抛SQLException: Hour of day out of range
  • username和password必须与你在 MySQL 中设置的完全一致(区分大小写)
  • 若 MySQL 端口不是默认 3306,请同步修改url中的:3306

修改后保存,务必删除resources/目录下所有.bak或.old文件——曾有同学因保留db.properties.bak,IDEA 自动加载了错误配置,导致连错库却报“表不存在”。

2.5 编译与运行:用命令行绕过 IDE 的“自动魔法”,看清真实报错

别急着用 IDEA 点绿色三角形。先用命令行确认基础链路:

# 进入 src 目录同级(即包含 src/ 和 lib/ 的目录) cd /path/to/unpacked/project # 编译所有 .java 文件(-cp 指定 mysql 驱动路径) javac -cp "lib/mysql-connector-java-5.1.47.jar" -d bin src/com/booksystem/**/*.java # 运行(-cp 包含 bin/ 和 lib/) java -cp "bin:lib/mysql-connector-java-5.1.47.jar" com.booksystem.ui.LoginFrame

若出现Exception in thread "main" java.lang.NoClassDefFoundError: com/mysql/jdbc/Driver,说明-cp中 jar 路径错误(Linux/macOS 用:分隔,Windows 用;);若弹出登录窗口但输入admin/admin123后黑屏,检查db.properties中密码是否输错——此时控制台会打印SQLException: Access denied,但 GUI 窗口不显示错误,这是 Swing 的“静默失败”特性。


3. 核心业务逻辑拆解:借阅、归还、库存联动的三层校验如何落地

3.1 借阅流程:从 UI 点击到数据库写入的七次方法调用链

当你在“学生借书”界面选中一本《算法导论》并点击“借阅”按钮,背后发生:

  1. BorrowBookPanel.java的borrowButton.addActionListener()触发
  2. 调用BorrowService.borrowBook(userId, bookId)(业务层)
  3. BorrowService先查BookDao.getBookById(bookId)确认图书存在且status=1(在馆)
  4. 再查BorrowDao.hasUnreturnedBook(userId, bookId)防止重复借同一本
  5. 若通过,构造BorrowRecord对象,设置borrow_time = new Date()
  6. 调用BorrowDao.addBorrowRecord(record)执行 INSERT
  7. 关键一步:调用BookDao.updateBookStatus(bookId, 0)将图书状态改为“已借出”

这个链路里,第 6 步和第 7 步不在同一个事务中——这是本系统的设计边界。若第 6 步成功、第 7 步因网络中断失败,会导致“记录已存但图书状态仍为在馆”,下次借阅会重复生成记录。修复方案见第 5 章。

3.2 归还流程:时间戳校验与状态回滚的原子性保障

归还操作更复杂:

  • ReturnBookService.returnBook(recordId)首先通过recordId查询原始借阅记录
  • 计算return_time - borrow_time,若超过 30 天(MAX_BORROW_DAYS = 30),自动标记is_overdue = 1
  • 更新borrow_records表的return_time和is_overdue字段
  • 同时更新books表的status = 1(在馆)和borrow_count = borrow_count + 1(借阅次数+1)

注意BookDao.updateBookOnReturn()方法中:

String sql = "UPDATE books SET status = ?, borrow_count = borrow_count + 1 WHERE id = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setInt(1, Book.STATUS_AVAILABLE); // STATUS_AVAILABLE = 1 ps.setInt(2, bookId); ps.executeUpdate(); // 这里必须成功,否则库存状态错乱

若此 SQL 执行失败(如bookId不存在),BorrowDao.updateReturnTime()却已提交,就会出现“记录已还但书仍显示已借出”。这就是为什么第 2.5 节强调要用命令行运行——GUI 层会吞掉这种底层 SQLException。

3.3 多角色权限:users.role字段如何驱动整个 UI 的可见性开关

权限控制不靠 Spring Security,而靠最朴素的if-else:

  • LoginService.login(username, password)返回User对象,其中user.getRole()是String
  • MainFrame.java构造函数中:
if ("admin".equals(user.getRole())) { menuBar.add(adminMenu); // 显示“管理员”菜单 tabbedPane.addTab("图书管理", new BookManagePanel()); } else { tabbedPane.addTab("我的借阅", new MyBorrowPanel()); }

BookManagePanel中的“添加图书”按钮监听器会校验:

if (!"admin".equals(LoginContext.getCurrentUser().getRole())) { JOptionPane.showMessageDialog(this, "只有管理员可操作!"); return; }

这种硬编码角色名的方式,在课程设计中足够清晰;但若要扩展“教师”角色,需同步修改users.role的 ENUM 定义、所有if判断、以及init.sql中的测试数据。

3.4 库存统计:books.borrow_count与borrow_records表的最终一致性

系统用两个字段维护库存:

  • books.status:实时状态(0=已借出,1=在馆)——用于前端快速判断能否借阅
  • books.borrow_count:累计借阅次数——用于统计报表

二者更新时机不同:

  • status在借阅/归还时立即更新(见 3.1 和 3.2)
  • borrow_count只在归还时+1(避免借阅取消后计数错误)

因此,若某本书被借出后系统崩溃未归还,status=0但borrow_count不变,这是设计允许的“暂时不一致”。最终一致性靠人工盘点或定期脚本修复,不属于本系统范畴。


4. 避坑指南:五个让90%新手卡住的致命细节与血泪修复方案

4.1 现象:登录窗口弹出后,输入正确账号密码,点击登录无反应,控制台无任何输出

原因:db.properties中url的serverTimezone参数缺失或格式错误(如写成GMT+8而非GMT%2B8),导致DriverManager.getConnection()抛出SQLException,但LoginFrame.java的actionPerformed()方法里catch (Exception e)仅e.printStackTrace(),而 Swing 事件线程默认不打印到控制台。
解决:在LoginService.login()方法开头加日志:

System.out.println("Attempting login with: " + username); // 然后在 catch 块中: } catch (SQLException e) { System.err.println("DB Connection failed: " + e.getMessage()); // 强制输出到 stderr JOptionPane.showMessageDialog(null, "数据库连接失败:" + e.getMessage()); }

4.2 现象:添加新图书后,列表不刷新,重启程序才显示

原因:BookManagePanel.java的addButton监听器中,bookDao.addBook(book)执行成功,但未调用tableModel.fireTableDataChanged()刷新 JTable。源码中此处是注释掉的(第 127 行// tableModel.fireTableDataChanged();)。
解决:取消该行注释,并确保tableModel是当前 JTable 关联的DefaultTableModel实例。若用自定义 TableModel,需调用其fireTableRowsInserted()方法。

4.3 现象:MySQL 8.0 连接成功,但执行SELECT * FROM books报错Unknown column 'isbn' in 'field list'

原因:init.sql中books表创建语句漏掉了isbn VARCHAR(20)字段,但Book.java实体类中有private String isbn;,且BookDaoImpl的addBook()方法中PreparedStatement绑定了 7 个参数(含 isbn),而表只有 6 列。
解决:运行sql/update_v1.1.sql:

ALTER TABLE books ADD COLUMN isbn VARCHAR(20) AFTER name; UPDATE books SET isbn = CONCAT('ISBN-', id) WHERE isbn IS NULL;

然后重启应用。

4.4 现象:管理员删除一本图书后,“图书管理”面板列表仍显示该书,但双击编辑报“找不到记录”

原因:BookDaoImpl.deleteBook()方法中 SQL 为DELETE FROM books WHERE id = ?,但BookManagePanel的deleteButton监听器未调用tableModel.removeRow(selectedRow),导致界面缓存未清除。
解决:在deleteButton的actionPerformed()中,删除 DB 后立即同步删除表格行:

int selectedRow = table.getSelectedRow(); if (selectedRow >= 0) { int bookId = (int) tableModel.getValueAt(selectedRow, 0); // 假设第一列是 id bookDao.deleteBook(bookId); tableModel.removeRow(selectedRow); // 关键! }

4.5 现象:借阅多本书后,borrow_records表中return_time字段全为NULL,但books.status已变为 0

原因:BorrowDaoImpl.addBorrowRecord()方法中INSERT语句漏写了return_time字段,默认值为NULL,但books.status更新逻辑在BorrowService中,与return_time无关。问题在于:return_time本应为空表示“未归还”,但部分同学误以为需设为0或当前时间。
解决:确认borrow_records表结构中return_time DATETIME NULL DEFAULT NULL,无需修改。归还时由ReturnBookService显式更新该字段。若想初始化为0000-00-00 00:00:00,需改表结构并设DEFAULT '0000-00-00 00:00:00',但 MySQL 5.7+ 默认禁止零日期,不推荐。


5. 进阶技巧:用事务包装借阅/归还操作,让库存状态真正原子化

5.1 为什么原生 JDBC 的autoCommit=true是库存错乱的根源

当前系统借阅流程中,BorrowDao.addBorrowRecord()和BookDao.updateBookStatus()是两次独立的executeUpdate()调用,默认开启自动提交(conn.setAutoCommit(true))。这意味着:

  • 若addBorrowRecord()成功,updateBookStatus()失败,数据库中留下一条“借阅记录”,但图书状态仍是“在馆” → 学生可重复借同一本
  • 若updateBookStatus()成功,addBorrowRecord()失败,图书状态变为“已借出”,但无记录 → 库存虚减,无法追溯

这不是 Bug,是 JDBC 默认行为。课程设计中常被忽略,但答辩时老师必问:“如果网络中断,你怎么保证数据一致性?”

5.2 三步改造:用Connection手动控制事务边界

以BorrowService.borrowBook()为例,改造如下:

public boolean borrowBook(int userId, int bookId) { Connection conn = null; try { conn = JDBCUtils.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 步骤1:检查图书状态 Book book = bookDao.getBookById(bookId); if (book == null || book.getStatus() != Book.STATUS_AVAILABLE) { throw new RuntimeException("图书不可借阅"); } // 步骤2:添加借阅记录 BorrowRecord record = new BorrowRecord(); record.setUserId(userId); record.setBookId(bookId); record.setBorrowTime(new Date()); borrowDao.addBorrowRecord(record); // 步骤3:更新图书状态 bookDao.updateBookStatus(bookId, Book.STATUS_BORROWED); conn.commit(); // 两步都成功,统一提交 return true; } catch (Exception e) { if (conn != null) { try { conn.rollback(); // 任一步失败,全部回滚 } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { JDBCUtils.closeConnection(conn); } }

关键点:

  • conn.setAutoCommit(false)必须在获取连接后立即执行
  • conn.commit()和conn.rollback()必须在try/catch/finally的严格包裹下
  • JDBCUtils.closeConnection(conn)不能放在try块内,否则rollback()时连接已关闭

5.3 验证事务生效:制造人为故障并观察数据库状态

测试方法:在borrowDao.addBorrowRecord()方法末尾强行抛异常:

// 在 executeUpdate() 后加 throw new RuntimeException("Simulate network failure"); // 临时注入故障

然后执行借阅操作:

  • 观察borrow_records表:无新增记录
  • 观察books表:目标图书status仍为 1(在馆)
  • 控制台输出Rollback executed

移除该异常后,借阅成功,两表同步更新。这证明事务包装生效。

5.4 生产级提醒:连接池与事务的隐性冲突

本系统未用连接池,所以JDBCUtils.getConnection()每次新建连接,事务控制简单。但若你后续集成HikariCP,必须注意:

  • 连接池中的连接默认autoCommit=true
  • 获取连接后必须显式调用conn.setAutoCommit(false)
  • 不能在finally块中调用conn.setAutoCommit(true),否则下次从池中取出的连接会继承false状态,导致后续操作不提交

标准做法是在borrowBook()结束时重置:

finally { if (conn != null) { try { conn.setAutoCommit(true); // 重置为默认 conn.close(); // 归还连接池 } catch (SQLException e) { e.printStackTrace(); } } }

从那以后我每次给课程设计加事务,都强制走一遍“手动抛异常→查数据库→确认回滚”三步验证。不是信不过代码,是信不过自己写commit()时手抖漏掉分号。希望帮到你。

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

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

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

立即咨询