
简介一套面向Java实训的图书馆管理系统完整项目包适合正在学习Java编程、需要完成课程设计或期末项目的计算机专业学生也可作为毕业设计或自学练手的参考。项目覆盖图书信息管理、读者管理、借阅归还、查询统计等核心模块从数据库ER图与SQL建表、JDBC数据操作到Swing/JavaFX界面布局和MVC业务分层均有可运行的代码与工程结构可参照并包含图书可借状态判断、超期未还检测等业务规则示例。压缩包共1988个文件以Eclipse工程中的xmi模型文件、class编译产物、prefs配置及30份Java源码为主附带XML、properties等资源文件整体约6MB适合直接导入IDE查看学习。目前已有229人学习下载。通过该项目可完整理解图书馆系统的设计思路掌握JDBC连接、界面交互、权限校验、异常处理等实训重难点为后续软件工程开发打下扎实基础。1. 图书馆管理系统Java 实训里最值得啃下来的完整 CRUD 项目图书馆管理系统在 Java 实训任务里几乎年年出现不是因为题目老而是它把 Java 基础里最该练的东西全串起来了JDBC 操作 MySQL、三层架构、事务、模糊查询、日期计算、Swing 或 JSP 界面。你把这套跑通等于把 Java 后端入门阶段的 CRUD 闭环走了一遍后面写 spring boot mybatis 的商用项目时很多思路是直接平移过去的。这份资源适合两类人一类是正在做 Java 实训、被“图书管理系统的设计与实现”卡住的学生另一类是带学生的老师或组长需要一套能讲清楚设计思路、能验收、能答辩的参考实现。本文按“设计→建表→核心代码→事务→避坑→答辩提升”的顺序拆尽量把每一步的为什么和参数含义都写明白。2. 表结构与三层架构把图书、借阅、用户拆成能落地的模块2.1 数据表怎么拆三张核心表加一张用户表实训题目的业务边界很清晰管理员登录、图书录入与修改、读者借书还书、逾期算罚款。很多人第一反应是“一张图书表、一张读者表、一张借阅表”这个方向对但字段设计上容易出问题。图书和读者是多对多关系一个读者能借多本书一本书也能被不同读者先后借走所以中间必须有独立的借阅表而不是把借阅信息塞进图书表或读者表里。我一般会拆成四张表book图书、reader读者、borrow借阅记录、admin管理员。关键字段设计如下表名核心字段说明bookid, book_name, author, publisher, isbn, category, stock, total_stockstock 是当前可借数量total_stock 是总库存删除和借书时靠这两个字段判断readerid, reader_no, name, phone, max_borrowreader_no 是读者证号max_borrow 限制最多可借几本borrowid, book_id, reader_id, borrow_time, due_time, return_time, status, finestatus 用 0/1/2 区分借出中、已归还、逾期未还fine 存罚款金额adminid, username, password管理员登录凭证isbn建议多加一个唯一索引因为实训场景里同一本书可能有多条采购记录用 isbn 做查重比用书名靠谱。borrow表里status字段不要用字符串用数字状态码后面做统计报表时WHERE status 1比WHERE status 归还高效得多也避免中文乱码问题。2.2 分层结构与包命名别把代码全塞在 main 方法里很多实训源码的通病是打开只有一个 Main 类一万行代码从头写到尾。这种代码跑起来没问题但答辩时老师说“你讲讲分层”你就很难自圆其说。合格的做法是分成entity、dao、service、view、util五个包对应关系是entityBook、Reader、Borrow、Admin 实体类字段与表一一对应。daoData Access Object只做 SQL 的增删改查不做业务判断。service业务逻辑层比如借书时先查库存、再判断读者是否超限再调 DAO。view界面层Java 实训大多用 Swing少数用 JSPview 只负责接收输入和展示结果。utilDBUtil 数据库连接工具、DateUtil 日期计算工具、ResultSet 转对象的工具方法。这个结构在答辩时的好处是老师任何一句“你这个功能怎么实现的”你都能迅速定位到对应层。借书流程说“service 层先校验再调 dao”追加库存说“先 update book 再 insert borrow包在事务里”回答思路非常清晰。资源包里的源码一般也是按这个结构组织的如果你拿到的版本是单文件的建议自己先按这个包结构拆一遍再跑拆的过程比跑通代码本身更有收获。2.3 数据库初始化直接可执行的建表 SQL我先给出这套系统最常用的建表 SQL注意字符集一定要用 utf8mb4否则后面插入书名里的生僻字或表情符号会报 Incorrect string value 错误。-- 图书表 CREATE TABLE book ( id int NOT NULL AUTO_INCREMENT, book_name varchar(128) NOT NULL COMMENT 书名, author varchar(64) DEFAULT NULL COMMENT 作者, publisher varchar(64) DEFAULT NULL COMMENT 出版社, isbn varchar(32) DEFAULT NULL COMMENT ISBN号, category varchar(32) DEFAULT NULL COMMENT 分类, stock int NOT NULL DEFAULT 0 COMMENT 当前可借数量, total_stock int NOT NULL DEFAULT 0 COMMENT 总库存, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_isbn (isbn) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; -- 读者表 CREATE TABLE reader ( id int NOT NULL AUTO_INCREMENT, reader_no varchar(32) NOT NULL COMMENT 读者证号, name varchar(64) NOT NULL COMMENT 姓名, phone varchar(20) DEFAULT NULL, max_borrow int NOT NULL DEFAULT 5 COMMENT 最大借书数量, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_reader_no (reader_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT读者表; -- 借阅记录表 CREATE TABLE borrow ( id int NOT NULL AUTO_INCREMENT, book_id int NOT NULL, reader_id int NOT NULL, borrow_time datetime DEFAULT NULL COMMENT 借书时间, due_time datetime DEFAULT NULL COMMENT 应还时间, return_time datetime DEFAULT NULL COMMENT 实际归还时间, status tinyint NOT NULL DEFAULT 0 COMMENT 0借出中 1已归还 2逾期, fine decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 罚款金额, PRIMARY KEY (id), KEY idx_book_id (book_id), KEY idx_reader_id (reader_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅记录表;status的默认值是 0插入借阅记录时只需要写 book_id、reader_id、borrow_time、due_time归还时再通过主键把 status 改成 1同时更新 book 表的 stock 加一。由于 borrow 表的外键不是必建的实训里我建议不加物理外键只保留普通索引因为加了物理外键后删除图书会被约束拦住实训阶段反而增加排错成本业务上在代码层用 status 判断即可。3. 核心模块实现从 DBUtil 到图书 CRUD 与借阅归还3.1 数据库连接工具DBUtil 与 properties 配置数据库连接不能每次 new 一个 Connection 后裸写在代码里。实训阶段虽然不用连接池但至少要把连接信息抽到db.properties文件里用DBUtil统一管理。这样换数据库环境时只改配置文件不用动编译代码。先看db.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password123456serverTimezoneAsia/Shanghai是 MySQL 8.x 连接时的必配项很多实训生卡在这一步报The server time zone value Öйú±ê׼ʱ¼ä原因就是没指定时区。useSSLfalse是减少本机连接时的握手警告本地实训环境没必要开 SSL。再写 DBUtil 工具类package com.library.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ResourceBundle; public class DBUtil { private static String driver; private static String url; private static String username; private static String password; // 静态代码块类加载时读一次配置 static { ResourceBundle bundle ResourceBundle.getBundle(db); driver bundle.getString(jdbc.driver); url bundle.getString(jdbc.url); username bundle.getString(jdbc.username); password bundle.getString(jdbc.password); try { Class.forName(driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(JDBC驱动加载失败请检查 mysql-connector 是否导入); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }ResourceBundle.getBundle(db)读取的是 classpath 下的db.properties注意名字不能带后缀。Class.forName(driver)在 JDBC 4.0 之后其实可以省略但写上是保险做法兼容老版本驱动。我用静态代码块是因为连接参数在整个 JVM 生命周期内只需要初始化一次每次调用getConnection()时直接拿配置开连接就够了。3.2 图书 DAO增删改查与模糊搜索图书模块的核心是按关键字模糊搜索、新增图书、修改图书、删除图书。先写 BookDao 的查询方法我用一个findByKeyword覆盖书名、作者、出版社三个字段的模糊搜索这是实训报告里常说的“多功能检索”。public ListBook findByKeyword(String keyword) { ListBook list new ArrayList(); String sql SELECT id, book_name, author, publisher, isbn, category, stock, total_stock FROM book WHERE book_name LIKE ? OR author LIKE ? OR publisher LIKE ? ORDER BY id DESC; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { String like % keyword %; ps.setString(1, like); ps.setString(2, like); ps.setString(3, like); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Book book new Book(); book.setId(rs.getInt(id)); book.setBookName(rs.getString(book_name)); book.setAuthor(rs.getString(author)); book.setPublisher(rs.getString(publisher)); book.setIsbn(rs.getString(isbn)); book.setCategory(rs.getString(category)); book.setStock(rs.getInt(stock)); book.setTotalStock(rs.getInt(total_stock)); list.add(book); } } } catch (SQLException e) { e.printStackTrace(); } return list; }用PreparedStatement而不是直接拼接字符串是因为 LIKE 条件里的关键字可能带单引号直接拼 SQL 会报错甚至被注入。try-with-resources写法在 JDK 7 才可用实训环境如果还在用 JDK 6需要改成手动 close。搜索条件用三个 OR 是简化做法实训场景完全够用如果图书量超过十万才需要考虑全文索引或改用 category 精确匹配。新增图书时要处理 isbn 重复的问题所以插入前最好先查一次同 isbn 且同名就只加库存不新增记录这是资源包源码里常见的“去重插入”逻辑。删除图书不能一刀切如果stock total_stock说明有书借出去了直接 DELETE 会导致借阅记录变成孤儿数据。正确做法是先查这本书的借出数量如果大于 0提示“该书尚有借出记录无法删除”否则才执行删除。3.3 借书归还主流程参数校验与状态变更借书是整个系统的业务核心它的逻辑链路是读者号查读者 → 图书 id 查库存 → 校验读者未还数量是否达到 max_borrow → 校验图书是否还有可借库存 → 插入借阅记录 → 图书 stock 减一。任何一步失败都要回滚。public boolean borrowBook(String readerNo, int bookId) { String checkReaderSql SELECT id, max_borrow FROM reader WHERE reader_no ?; String countBorrowSql SELECT COUNT(*) FROM borrow WHERE reader_id ? AND status 0; String checkBookSql SELECT stock FROM book WHERE id ?; String insertBorrowSql INSERT INTO borrow(book_id, reader_id, borrow_time, due_time, status) VALUES (?,?,?,?,0); String updateStockSql UPDATE book SET stock stock - 1 WHERE id ? AND stock 0; Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 查询读者 // ... 执行 checkReaderSql拿到 readerId 和 maxBorrow // 查询未还数量 // ... 执行 countBorrowSql若 maxBorrow 则 throw new RuntimeException(借书数量已达上限) // 查询图书库存 // ... 执行 checkBookSql若 stock 0 则 throw new RuntimeException(库存不足) // 插入借阅记录due_time 为 borrow_time 30 天 PreparedStatement psInsert conn.prepareStatement(insertBorrowSql); // ... set 参数后 executeUpdate // 库存减一 PreparedStatement psUpdate conn.prepareStatement(updateStockSql); int rows psUpdate.executeUpdate(); if (rows 0) { throw new RuntimeException(库存更新失败可能已被并发借出); } conn.commit(); return true; } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); } catch (SQLException e) { e.printStackTrace(); } DBUtil.close(conn, null, null); } } }这段代码里最能体现 Java 基础的地方有三个setAutoCommit(false)打开手动事务所有业务校验在同一个 Connection 里做任何异常都回滚并重置自动提交。stock 0写在 UPDATE 的 WHERE 条件里是防止并发场景下两条请求同时把库存扣成负数后面第 4 章专门讲这个。归还流程则是镜像操作把 borrow 表的 status 从 0 改成 1填写 return_time再按“是否超过 due_time”算罚款最后把 book 表的 stock 加一。罚款的计算要注意只对逾期天数计价当天还书罚款为 0且总罚款保留两位小数。4. 借阅超期与库存事务让还书、罚款、更新库存不翻车4.1 还书算罚款不要直接减日期用毫秒差换算逾期罚款是实训报告里最有可写性的功能点也是老师喜欢追问的地方。常见错误是returnTime - dueTime得到的是毫秒差直接除以 86400000 取整这样会把当天归还且差几小时到期的情况算成逾期因为负数取整后是 0 没问题但正数不足一天的会被截断读者实际晚还了 20 小时罚款却按 0 天算逻辑上说不通。我一般这么算public static int calcOverdueDays(Date returnTime, Date dueTime) { if (returnTime null || dueTime null) { return 0; } long diffMs returnTime.getTime() - dueTime.getTime(); if (diffMs 0) { return 0; // 未逾期 } // 超过1毫秒也算逾期1天向上取整 return (int) Math.ceil(diffMs / (1000.0 * 60 * 60 * 24)); }用Math.ceil向上取整只要过了到期时间点哪怕只超了 1 分钟也按 1 天计。实训题目的规则一般会写“每超 1 天罚款 0.5 元”所以罚款金额就是calcOverdueDays * 0.5保留两位小数。注意1000.0必须写浮点数否则整数除法会把不足一天的毫秒差直接截成 0这是最常见的翻车点。4.2 事务边界减少库存、插入记录必须同生共死借书流程跨两张表操作如果不开启事务就会出现“借阅成功但库存没减”或“插入记录失败但库存已经减掉”的脏数据。实训答辩时老师一定会问“为什么用事务”标准答案只有一句保证一组 SQL 要么全部成功、要么全部不生效。事务边界划分有一个经验所有 SQL 操作放在一个 Connection 里conn.setAutoCommit(false)之后连续执行最后统一 commit。中间任何一步抛异常直接rollback()回滚。还要注意事务提交后别忘了把autoCommit恢复成 true 再归还连接否则连接池复用时会把上一个事务的状态带到下一个请求里。4.3 并发借阅库存超借问题与 WHERE 条件兜底单机实训课设一般碰不到高并发但如果你的系统里使用了多线程模拟多个读者同时借同一本书就会踩到库存超借的坑两个请求同时读到stock 1都认为可以借结果两条记录插入成功、库存扣成 -1。解决思路有两个层面。上层在 service 用 synchronized 锁住 borrowBook 方法同 JVM 内有效底层更稳的做法是把扣库存的 UPDATE 写成UPDATE book SET stock stock - 1 WHERE id ? AND stock 0用受影响行数判断是否真的扣到了。如果影响行数为 0说明库存已经被别人扣完此时抛异常回滚就能避免超借。我建议实训代码里两层都写方法加synchronized是答辩时能讲“线程安全”WHERE stock 0是兜底方案。这两招同时上就能理直气壮回答“并发情况下你怎么保证库存不超”的问题。5. 实训高频踩坑排查驱动缺失、中文乱码、主键冲突与编译内存5.1 启动报 ClassNotFoundException驱动 jar 根本没进 classpath现象运行 DBUtil 时抛出java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver或者 IDEA 里编译通过但一启动就报错。原因mysql-connector-java的 jar 包虽然 import 了但项目结构里没有把它加入 Libraries。用 IntelliJ IDEA 时常见问题是 jar 放在了某个文件夹却没右键 Add as Library用 Eclipse 则是没 Build Path - Configure Build Path - Add JARs。解决在 IDEA 中按CtrlShiftAltS打开 Project Structure选 Modules - Dependencies点加号把驱动 jar 加进去。另外注意 MySQL 8.x 的驱动类名是com.mysql.cj.jdbc.Driver老版本是com.mysql.jdbc.Driver驱动包版本和类名要匹配写错也会报 ClassNotFoundException。5.2 图书管理页面中文全是问号URL 缺字符集参数现象插入的“三体”在控制台显示正常从数据库客户端看也是好的但界面表格里显示???。原因JDBC URL 少了characterEncodingutf8。MySQL 服务端字符集是 utf8mb4客户端连接默认用 latin1两边不一致中文在传输层就丢了。解决把 db.properties 里的 URL 改成jdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse同时确认建表 SQL 里DEFAULT CHARSETutf8mb4两者匹配后重启程序即可。血泪经验只改 URL 不改表结构或者只改表结构不改 URL都会白折腾。5.3 删除图书报外键约束错误书架上有借出的书现象删除一本写死有借阅记录的图书时SQL 语句直接报Cannot delete or update a parent row: a foreign key constraint fails或者没报错但借阅记录查出来 book_id 已经指向不存在的数据。原因表结构里若建了物理外键MySQL 会拒绝删除被子表引用的父表记录如果没建外键就会出现数据孤儿。两种都属于设计上没考虑业务约束。解决删书之前先查SELECT COUNT(*) FROM borrow WHERE book_id ? AND status 0如果借出数量大于 0在界面上提示“该书尚有未归还记录请先处理还书”。这也是我在 2.3 里建议实训项目不要加物理外键的原因——用代码判断逻辑更清晰展示给老师看也更直观。5.4 还书后库存没增加事务没提交或更新了错误的记录现象还书流程提示成功但打开图书列表发现 stock 还是原样或者所有书的库存都加了 1。原因第一种是 UPDATE 语句执行了但连接没 commit 就 closeMySQL 默认自动提交被setAutoCommit(false)关掉了没手动提交就全回滚。第二种是 UPDATE 的 WHERE 条件忘了带id写成UPDATE book SET stock stock 1把全表库存都改了。解决抽出一个独立方法returnBook(int borrowId)内部拿着 borrowId 反查 book_id再UPDATE book SET stock stock 1 WHERE id ?最后统一 commit。养成一个习惯连接关闭前打印受影响行数rows ! 1时立刻能发现问题。5.5 IDEA 编译报 GC overhead 或 OutOfMemoryError堆内存不够现象IDEA 编译项目时报java.lang.OutOfMemoryError: GC overhead limit exceeded或者提示Compilation failed: internal java compiler error把编译堆调到 8000 仍然报错。原因IDEA 的编译进程和 JVM 运行进程是分开的报 GC overhead 通常是因为项目里有大量依赖 jar 或源码文件过多编译器堆设置不够调到 8000 还报错则可能是同时启动了太多 Gradle/Maven 守护进程内存被占满。解决IDEA 里Help - Change Memory Settings把 IDE 堆调大同时Build Tools - Maven - Runner - VM Options里的编译堆加上-Xmx2048m。更直接的做法是关掉不用的 Maven 工程或删掉项目里的 target/out 目录重新构建。这种问题在实训环境里很玄学遇到就按“先清缓存重启再调堆内存最后升级 IDEA 版本”的顺序试基本都能解决。6. 实训验收前最后的提升从能跑到能答辩的四个改动找一个答辩会追问的薄弱点下手比狂堆功能有用得多。我的习惯是把这四个改动放进去成本低且十分加分。第一个是批量导入图书。用PreparedStatement的批处理直接处理一个文本文件每行是一本书的 isbn、书名、作者用addBatch()executeBatch()一次性插入比循环单条插入快一个数量级。注意每 500 条执行一次clearBatch()避免 SQL 命令积压占用内存。然后是近 7 天借阅量统计。不用复杂的 SQL借用分组聚合即可SELECT DATE(borrow_time), COUNT(*) FROM borrow WHERE borrow_time ? GROUP BY DATE(borrow_time)把结果画成柱状图或表格展示在统计面板。这个功能虽然未必在原始需求里但它能让老师看到你有数据意识而且只要 30 行代码。第三个是密码存储。实训默认admin表存明文密码我建议用MessageDigest.getInstance(SHA-256)加盐存哈希。答辩时可以主动说一句“生产环境不会存明文”就这一个改动就把你的代码和课设模板区分开了。最后是准备一张表结构说明文档把每张表的字段、类型、注释列清楚打印出来放桌面答辩时老师扫一眼就知道你系统设计完整能少问五分钟刁钻问题。从那以后我每次带实训都会强制走一遍“先跑通基础流程、再加事务和并发兜底、最后做答辩预演”把上面这几个改动当成最低标准而不是加分项。这套资源里的源码直接跑通只要半天但你把它拆开、重写、加自己的注释和边界处理才是真的把知识装进脑子里。希望帮到你。本文还有配套的精品资源点击获取