ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Java的图书借阅管理系统:从数据模型到JDBC事务与部署全解析

基于Java的图书借阅管理系统:从数据模型到JDBC事务与部署全解析 简介包含完整源代码、数据库脚本与部署视频的图书借阅管理系统毕业设计资料包面向需要完成Java Web课程设计或毕业设计的本专科学生。系统涵盖图书管理、用户管理、借阅记录等核心模块涉及Java、MVC模式、Spring、Hibernate、Servlet/JSP及前端等主流技术能帮助学习者从零搭建并理解一个可运行的企业级项目。压缩包共6个文件包含3个mp4演示与部署视频、1个rar项目源码、1个sql数据库脚本和1个docx部署文档整体约105.85MB。视频分别演示项目运行效果、部署步骤及数据库表结构文档则对系统架构、环境配置和操作流程做了详细说明。目前已有1800人学习下载适合作为毕业设计参考、项目实战练手或技术面试准备材料能够有效缩短从需求分析到部署上线的学习路径。1. 为什么基于 Java 的图书借阅管理系统是毕业设计里最耐问的选题每年毕业设计选题表里“基于 Java 的图书借阅管理系统”都会占据一席。它不新潮但足够稳图书、读者、借阅记录能干净地映射成数据库表Java 代码刚好覆盖增删改查、事务、权限控制这几条核心链路工作量适合一个人独立完成答辩时每个设计点都能讲出因果。真正拿到中途接手这个题目时多数同学面对的不只是“写代码”而是把一份“源代码数据库部署文档部署视频”从解压到能跑、能演示、能回答老师追问的完整过程。这篇文章不只讲 CRUD而是把数据模型怎么设计、JDBC 连接怎么配置、借阅归还事务怎么保证一致、部署视频没提到的坑怎么填全部按顺序讲清照着做可以复现也能在答辩现场解释清楚每个步骤的取舍。2. 从 ER 图到建表图书借阅管理系统的数据模型设计2.1 图书、读者、借阅记录三张核心表怎么拆不管界面用什么框架底层一定是关系型数据库。常见的最小可用模型有四张表读者表t_reader、图书表t_book、分类表t_category、借阅记录表t_borrow。分类表看起来可以不做但加上之后图书检索和统计会顺手很多画 ER 图时也多一层关系数据库课程设计说明能写得更完整。CREATE DATABASE IF NOT EXISTS book_manager DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE book_manager; CREATE TABLE t_reader ( id INT PRIMARY KEY AUTO_INCREMENT, reader_no VARCHAR(20) NOT NULL UNIQUE COMMENT 读者编号如 R2024001, reader_name VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL COMMENT 存 MD5 或 SHA-256 后的值, role TINYINT NOT NULL DEFAULT 0 COMMENT 0读者1管理员, max_borrow INT NOT NULL DEFAULT 5 COMMENT 最大可借数量, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, category_name VARCHAR(50) NOT NULL UNIQUE ); CREATE TABLE t_book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, book_name VARCHAR(100) NOT NULL, author VARCHAR(50), category_id INT, total_count INT NOT NULL DEFAULT 1, available_count INT NOT NULL DEFAULT 1, FOREIGN KEY (category_id) REFERENCES t_category(id) ); CREATE TABLE t_borrow ( id INT PRIMARY KEY AUTO_INCREMENT, borrow_no VARCHAR(32) NOT NULL UNIQUE, reader_id INT NOT NULL, book_id INT NOT NULL, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP, due_time DATETIME NOT NULL, return_time DATETIME DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0借出1已还2超期未还, FOREIGN KEY (reader_id) REFERENCES t_reader(id), FOREIGN KEY (book_id) REFERENCES t_book(id) );建表语句里值得注意的参数有三个。第一字符集用utf8mb4不是utf8否则生僻字或表情符号写入时报错或者变成问号。第二t_book把total_count和available_count分开存是为了在图书列表页直接展示“馆藏/剩余”不用每次用借阅记录反查算库存。第三t_borrow里的due_time设为NOT NULL借出时就确定应还日期避免后面判断超期时出现空值。status字段用 0/1/2 表示三种状态而不是直接删除借阅记录这样历史留痕超期统计也简单。四张表的关系整理成表结构摘要方便答辩时讲表名作用关键字段关联关系t_reader读者与登录账号合表reader_no、role、max_borrow一对多借阅t_category图书分类category_name一对多图书t_book图书与库存total_count、available_count多对一分类t_borrow借阅流水due_time、return_time、status多对一读者与图书2.2 借阅记录里做冗余字段还是每次都 join 三张表软件工程课程会反复强调范式但课程设计代码追求的是在可维护的前提下少写几层 join。借阅列表页要显示“读者姓名、书名、借出日期、状态”如果严格三范式每次查询都要 joint_reader和t_bookSQL 长索引消耗也大。更常见、也更适合这个体量的做法是在t_borrow里冗余两个快照字段reader_name和book_name在插入借阅记录时从当前读者、图书信息里带过来。这两个冗余字段带来的收益很实际借阅列表查询从三表 join 降成单表查询报表统计也快读者或图书改名后历史借阅记录保留的是借出时刻的现场反而更符合“当时谁借了哪本书”的事实。代价是如果做了“读者修改姓名”功能历史记录不会跟着变。这个取舍在答辩时正好能讲出理由日志型数据保留的是历史快照不以最新数据为准。相比“完全遵守范式但代码到处 join”或者“完全不建外键”这种中间态是课程设计里最常见的可靠方案。如果你看到原始包里的t_borrow没有冗余字段也可以用视图解决CREATE VIEW v_borrow_list AS SELECT b.id, r.reader_name, bk.book_name, b.borrow_time, b.due_time, b.status FROM t_borrow b JOIN t_reader r ON b.reader_id r.id JOIN t_book bk ON b.book_id bk.id;视图在映射工具不拆开 SQL 的情况下能少改 Java 代码。但它不是物理表适合展示不适合在 where 里再叠复杂条件。2.3 初始化脚本与字符集参数从 SQL 文件到 MySQL 实例拿到数据库脚本后第一步不是双击而是用命令行导入并确认字符集。推荐的方式是mysql -u root -p --default-character-setutf8mb4 -e source /path/to/book_manager.sql这条命令的关键点是--default-character-setutf8mb4。很多部署文档只写source结果脚本里中文注释或初始化的中文数据变成乱码。如果脚本文件本身就包含CREATE DATABASE用source方式没问题如果脚本只有CREATE TABLE就得先用CREATE DATABASE book_manager建库再USE。两种方式在部署视频里经常被含糊带过实际差别就在报错ERROR 1046 (3D000): No database selected时能否反应过来。如果手头只有 MySQL Workbench可以用 Server 菜单里的 Data Import但导入前要把目标 schema 选对。初始化完成后做个快速检查USE book_manager; SHOW TABLES; SELECT COUNT(*) AS category_count FROM t_category;SHOW TABLES确认四张表都建齐COUNT(*)确认初始分类数据没有因为字符集问题丢失。到这一步数据库层已经立住了再接 Java 就只是连接细节。3. Java 代码落点登录校验、图书增删改查与借阅归还事务3.1 JDBC 连接封装驱动类名、时区参数与释放资源Java 连接 MySQL 最常见的报错来自三处驱动 jar 没放进 lib、URL 里缺少时区参数、资源没关闭导致连接耗尽。以 MySQL 8.x 为例最小可用的连接工具类是这样import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DbUtil { private static final String URL jdbc:mysql://localhost:3306/book_manager?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD root; 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(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception e) { e.printStackTrace(); } } } } }连接串里的serverTimezoneAsia/Shanghai不能省MySQL 8.x 驱动默认要求显式时区否则直接抛The server time zone value异常。characterEncodingutf8与数据库端utf8mb4配合保证中文写入不乱码。驱动类的全限定名要严格匹配 jar 版本MySQL Connector/J 8.x 是com.mysql.cj.jdbc.Driver5.x 是com.mysql.jdbc.Driver这个问题在换 jar 时最容易踩。close(AutoCloseable...)用可变参数接收Connection、Statement、ResultSet调用时按顺序传入即可。3.2 登录校验PreparedStatement 参数绑定而不是拼接字符串图书借阅系统的登录入口通常只校验读者编号和密码但这一处恰恰是 SQL 注入的重灾区。课程设计阶段养成参数绑定习惯答辩被问“怎么防止 SQL 注入”时就能直接回答。推荐的做法是用PreparedStatement占位符public boolean checkLogin(String readerNo, String password) throws SQLException { String sql SELECT id FROM t_reader WHERE reader_no ? AND password ?; try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, readerNo); ps.setString(2, encrypt(password)); try (ResultSet rs ps.executeQuery()) { return rs.next(); } } }这段代码的关键在ps.setString(1, readerNo)和ps.setString(2, encrypt(password))。占位符里的值由驱动做转义用户输入里的单引号不会破坏 SQL 结构。密码不能存明文encrypt方法常见做法是 MD5但课程设计如果想要加分可以用 SHA-256 加盐private static String encrypt(String raw) throws Exception { MessageDigest digest MessageDigest.getInstance(SHA-256); byte[] hash digest.digest((raw book_manager_salt).getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : hash) { sb.append(String.format(%02x, b)); } return sb.toString(); }注意登录校验通过后要把role字段同时查出来放进 session 或内存对象后续图书管理功能靠这个字段判断当前操作者是管理员还是普通读者。如果只返回id界面上还得再查一次数据库。3.3 借阅与归还事务原子扣减库存两条 SQL 要同时成功借书操作涉及两处写操作t_book.available_count减一t_borrow插入一条借阅记录。如果库存减了但流水没插上数据就不一致反过来也不行。解决办法是放在同一个数据库事务里public void borrow(int bookId, int readerId) throws SQLException { Connection conn null; try { conn DbUtil.getConnection(); conn.setAutoCommit(false); String updateSql UPDATE t_book SET available_count available_count - 1 WHERE id ? AND available_count 0; try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setInt(1, bookId); if (ps.executeUpdate() 0) { throw new RuntimeException(库存不足扣减失败); } } String insertSql INSERT INTO t_borrow(borrow_no, reader_id, book_id, due_time) VALUES(?,?,?,DATE_ADD(NOW(), INTERVAL 30 DAY)); try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setString(1, B System.currentTimeMillis()); ps.setInt(2, readerId); ps.setInt(3, bookId); ps.executeUpdate(); } conn.commit(); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw e; } finally { if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这段代码里最值得讲的是UPDATE ... WHERE available_count 0。它把“查询库存是否充足”和“库存减一”合并成一条原子 SQL避免先 SELECT 再 UPDATE 造成的并发超借。setAutoCommit(false)之后必须手动conn.commit()任何一个环节抛异常就conn.rollback()。DATE_ADD(NOW(), INTERVAL 30 DAY)把应还日期定在借出后 30 天这个 30 可以从参数配置不要写死在 SQL 里。归还操作是同一套路先UPDATE t_borrow SET status 1, return_time NOW()再UPDATE t_book SET available_count available_count 1。归还时判断是否超期只用在更新状态前比较due_time与当前时间把超期部分也放进同一个事务即可。3.4 图书查询的模糊匹配LIKE 占位符的坑图书检索通常按书名模糊匹配很多初写代码的同学会这样写String sql SELECT * FROM t_book WHERE book_name LIKE %?%;这行执行时一定报参数数量错误因为?被写进了字符串字面量PreparedStatement 认为没有参数。正确写法有两种一种是用CONCAT拼接参数String sql SELECT id, book_name, author, available_count FROM t_book WHERE book_name LIKE CONCAT(%, ?, %); PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, keyword);另一种是查询条件里先拼好带百分号的字符串再传参。两者都能用但CONCAT写法更清晰索引行为上两者一致前置通配符都不走最优索引。搜索框如果允许输入书名、作者、ISBN 三个条件可以在 Java 侧动态拼接 WHEREStringBuilder sql new StringBuilder(SELECT * FROM t_book WHERE 11 ); ListObject params new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { sql.append(AND (book_name LIKE CONCAT(%, ?, %) OR author LIKE CONCAT(%, ?, %)) ); params.add(keyword); params.add(keyword); }WHERE 11是一种习惯写法方便动态拼接条件而不必判断是否要加 AND。参数按出现顺序放入List循环绑定能减少大量重复代码。注意模糊查询结果集如果直接展示给用户最好限制返回条数在 SQL 尾部加LIMIT 100防止SELECT *扫出全表导致 Swing 界面卡顿。错误写法报错现象正确写法LIKE %?%PreparedStatement 参数不匹配LIKE CONCAT(%, ?, %)Statement字符串拼接中文内容易注入且转义麻烦PreparedStatement参数绑定不带LIMIT全表扫描界面卡顿加LIMIT 100或分页4. 部署这一关环境变量、MySQL 导入与部署文档没写透的细节4.1 Java 环境变量配置与 JDK 版本对齐拿到压缩包第一步先确认本机 Java 环境。在命令行里执行java -version javac -version两个版本必须一致。常见问题是java -version显示 JDK 17javac -version显示 JDK 8原因是 PATH 里前面某个目录的 java.exe 和后面某个目录的 javac.exe 不是同一套。Windows 下部署文档里最常用的设置方式set JAVA_HOMEC:\Program Files\Java\jdk1.8.0_202 set PATH%JAVA_HOME%\bin;%PATH%JAVA_HOME是给 Maven、Tomcat、IDEA 用的PATH让命令行直接认识java和javac。这里容易踩的坑是环境变量配好后必须重开命令行窗口才生效因为环境变量只在进程启动时读取一次。课程设计通常不需要太高版本如果项目里没有用到高版本新特性JDK 8 和 JDK 11 选一个稳定的即可。项目里如果用了var、List.of这类语法就必须按对应版本的 JDK 编译。4.2 MySQL 导入数据库脚本与字符集对齐部署文档里最含糊的一句话往往是“导入数据库”。实际导入时推荐完整操作mysql -u root -p --default-character-setutf8mb4 source /path/to/book_manager.sql如果脚本文件里已经有CREATE DATABASE book_manager那么导入完成后执行USE book_manager;再查表如果脚本里只有建表语句需要先手动建库mysql -u root -p -e CREATE DATABASE book_manager DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;导入过程中出现ERROR 1366 (HY000): Incorrect string value就是这个表或连接设置的字符集不对优先检查连接命令有没有加--default-character-setutf8mb4。导入完成后做一个快速验证mysql -u root -p -e USE book_manager; SHOW TABLES; SELECT COUNT(*) FROM t_book;SHOW TABLES应该看到四张表SELECT COUNT(*)的结果应该和脚本里的初始数据数一致。如果这里结果不对后续 Java 登录一定报“表不存在”或空结果问题在数据库层先别改代码。4.3 打包可执行 jar依赖 jar 要一起打进去Java 项目交付时不能只交 class 文件或源码最常见的是打成可执行 jar。在 IDEA 里通过 File 菜单的 Project Structure 做 Artifacts或者用 Maven 打包mvn clean package java -jar target/book-manager.jar关键点在于 javase 项目里 mysql 驱动是第三方依赖普通 jar 打包默认不会把依赖打进去。运行时报ClassNotFoundException: com.mysql.cj.jdbc.Driver就是缺依赖。两个解决办法把 jar 放进项目同级的lib目录用java -cp lib/*;book-manager.jar com.bookmanager.Main启动或者用 Maven 的maven-assembly-plugin打进一个 fat jar。部署文档里如果只写了java -jar大概率是已经打过 fat jar 的版本。工作目录也要注意。如果代码里有相对路径读取配置文件或数据库初始化文件比如./conf/db.properties那么 jar 放在哪个目录运行时的相对路径就基于哪个目录不一定是 jar 所在的目录。这就是很多用户把 jar 复制到桌面运行后突然连不上数据库的原因。可以这样做cd /path/to/deploy java -jar book-manager.jar -Ddb.config./conf/db.properties先把所有外部配置明确放进conf目录再用-D指定部署时只要整个目录一起拷贝。4.4 部署视频里经常被跳过的三个隐藏坑现象原因处理MySQL 启动失败3306 端口被占用netstat -ano | findstr 3306查占用进程连接报 Public Key Retrieval is not allowedMySQL 8 的 caching_sha2_password 插件URL 加allowPublicKeyRetrievaltrue调试显示“当前不会命中断点”运行的是打包产物而非编译输出目录改用 Debug 模式启动确认 Artifacts 输出目录allowPublicKeyRetrievaltrue对本地开发和课程设计是安全的但部署到别人电脑上时建议在连接串里同时保留useSSLfalse避免协议协商阶段报出无关警告。端口占用问题在 Windows 上最常见的元凶是之前装的另一个 MySQL 服务没停或者mysqld以服务形式自动启动视频里不会说排查时先看服务列表。5. 答辩演示顺序与一个可复现的功能自检清单5.1 按角色路径演示10 分钟讲完答辩现场不要漫无目的地乱点界面按两条路径演示先普通读者登录执行图书查询、借书、还书再管理员登录新增图书、修改库存、查询借阅流水。数据库表结构与刚才讲的增删改查动作一一对应演示时顺手打开 MySQL 客户端查一次t_book.available_count在借书后的变化这个操作比口头解释更有说服力。演示环节操作动作对应代码/表管理员登录输入 admin 账号t_reader.role 1新增图书填 ISBN、书名、库存t_book INSERT读者借书扫码借出t_borrow INSERT available_count - 1读者还书确认归还t_borrow UPDATE available_count 1借阅查询按读者编号查流水t_borrow SELECT 按 reader_id5.2 用 EXPLAIN 验证查询与一个 10 分钟自检脚本答辩被问“数据库有没有优化”时不要只回答“建了索引”可以用 EXPLAIN 展示一个实际执行计划EXPLAIN SELECT * FROM t_borrow WHERE reader_id 1 AND status 0;关注输出里的type列和possible_keys。如果type是ALL说明全表扫描建了idx_reader_status(reader_id, status)之后再看type会变成ref这就是联合索引的直观效果。部署交付前跑一遍下面的自检脚本可以避开 80% 的现场事故echo 1. Java 版本 java -version echo 2. 数据库连接 mysql -u root -p -e SELECT 1 echo 3. 数据表完整性 mysql -u root -p -e USE book_manager; SHOW TABLES; echo 4. 依赖 jar 完整性 jar tf book-manager.jar | grep -i mysql脚本第 4 步的jar tf是查看 jar 包内部文件列表的命令如果输出里没有 mysql 驱动相关类名说明这个 jar 不是 fat jar换一台没装驱动的机器就会跑不起来。自检通过后再录部署视频视频里每一步对应的就是上面这几个命令用户照着走不会遇到“文档说点下一步但我这里报错”的情况。本文还有配套的精品资源点击获取
返回列表