
简介这是一份基于Java的图书管理系统完整源代码采用MVC设计模式覆盖图书录入、查询、借阅、归还、续借及状态跟踪等核心业务适合Java初学者、课程设计或毕业设计参考。压缩包共212个文件包含31个Java源文件、112个编译后的class文件、56张JPG界面截图、数据库文件db以及依赖jar包等整体大小4.01MB目录结构清晰便于按模块对应学习。系统采用分层架构模型层负责数据存储视图层负责界面展示控制器协调交互并整合关系型数据库进行图书、读者及借阅记录的管理。已有1872人学习下载。源码涵盖数据库设计、后端业务逻辑、前端交互、用户认证等完整链路并附有界面截图和UML模型文件可帮助读者从代码和设计文档两个维度理解图书借阅流程与Java工程化开发思路。1. 为什么每个 Java 初学者都应该自己重写一遍图书管理系统源码搜“Java图书管理系统源代码”的人多半是卡在课程设计或毕业设计的第一关网上随便一搜就有整套包下载解压、配个数据库、点一下运行功能齐全甚至带答辩PPT。但真正带着这套源码走进答辩教室时很多人会被一个问题问住“借书时如果两个人同时点库存怎么保证不变成负数”我见过太多拿着现成源码却讲不清三层架构的候选人而图书管理系统恰好是讲清这件事的最小载体。它把登录鉴权、CRUD、分页查询、状态流转、事务控制这五块 Java 后端面试必考的内容揉进了一个两百页不到的小项目里。这篇笔记不打算替你做选择题而是把一套能跑通、能讲清、能二次开发的源码方案拆给你看适合正在做课程设计的学生、准备软考或蓝桥杯的考生以及想系统补一遍 Java Web 基础的一线工程师。2. 把功能模块先拆对三层架构、四张核心表与包结构2.1 图书管理系统的标准功能清单不止增删改查很多从网上下载的源码功能列表长得吓人但真正经得起追问的只有几条主线。图书管理系统最核心的闭环是“图书—读者—借还记录”这三者的关系功能上可以收敛成五组管理员登录与会话管理图书的添加、编辑、下架与库存维护读者的注册、禁用与借阅上限控制借书和还书两条带状态流转的业务链路以及图书查询和借阅记录查询的分页列表。至于图书分类树、滞纳金计算、公告栏都属于锦上添花不建议在第一次做的时候塞进核心链路。我建议你在动手前先画一张简单的数据流草图管理员登录后维护图书和读者读者发起借书请求系统检查读者状态、借阅数量和图书库存然后扣减库存并生成一条待归还记录还书时反向操作恢复库存并把记录状态置为已归还。这张草图的价值在于它能直接映射出后面你要建的四张表和写的那几个事务方法。如果一上来就照着网上的源码抄你大概率会漏掉“读者借阅上限”和“图书上下架状态”这两个隐藏需求而它们恰恰是答辩时老师最爱追问的点。2.2 四张核心表的设计字段、约束与索引数据库表设计决定了后面所有 SQL 的写法。常见的做法是用四张表管理员表 admin、图书表 book、读者表 reader、借阅记录表 borrow_record。不要把读者和用户混成一张表也别把借阅记录嵌进图书表里否则分页查询和统计会非常痛苦。下面是建表语句我把字段注释和关键约束直接写在 SQL 里CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(255) NOT NULL COMMENT 推荐存MD5或BCrypt摘要, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT管理员表; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE COMMENT ISBN重复录入拦截, book_name VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(100), category VARCHAR(30) COMMENT 分类可做简单分组, total_stock INT NOT NULL DEFAULT 0 COMMENT 总库存, available_stock INT NOT NULL DEFAULT 0 COMMENT 当前可借库存, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_book_name (book_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, real_name VARCHAR(50), max_borrow INT NOT NULL DEFAULT 5 COMMENT 最大可借数量, status TINYINT DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT读者表; CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, reader_id INT NOT NULL, book_id INT NOT NULL, borrow_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, due_time DATETIME NOT NULL COMMENT 应还时间由业务层计算, return_time DATETIME DEFAULT NULL, status TINYINT DEFAULT 0 COMMENT 0借出 1已还 2逾期归还, KEY idx_reader_status (reader_id, status), KEY idx_book_id (book_id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅记录表;这段 SQL 里值得注意的有三处。一是所有表都显式指定了 InnoDB 和 utf8mb4前者保证后续借书事务里的行锁生效后者避免生僻字和 Emoji 存不进去。二是在 borrow_record 上建了 (reader_id, status) 联合索引因为“查询某个读者当前未还的借阅数量”会高频出现走这个索引比全表扫描快得多。三是密码字段我注释了“推荐存摘要”明文存密码在答辩里属于硬伤哪怕你不写注册功能也建议把这条规范带上。2.3 代码包结构怎么分从 controller 到 dao 的依赖方向拿到源码先把包结构看明白比自己从头建要快得多。标准的 Java Web 图书管理系统会按三层架构拆包web/servlet 层负责接收请求和返回响应service 层放业务规则dao 层封装 JDBC 或 MyBatis 的数据访问。实体类单独放在 entity/pojo 包工具类和过滤器放在 util/filter 包。依赖方向必须是 servlet → service → dao禁止反向调用这是答辩时最容易讲清也最容易被挑刺的地方。com.library ├── web # Servlet、Filter、监听器 ├── service # 业务接口和实现类 ├── dao # 数据访问接口和实现类 ├── entity # Book、Reader、BorrowRecord 等实体 └── util # DBUtil、MD5Util、分页工具类如果你选的技术栈是 Spring Boot MyBatis包结构会变成 controller、service、mapper、entity 四层职责边界和传统三层是等价的。我的建议是课程设计优先用 JSP Servlet JDBC 的经典组合因为它能把 HTTP、会话、JDBC 这些底层机制暴露在你面前面试官问起来你有的说。如果项目要求里写了“采用 SSM 或 Spring Boot”再切换也不难业务逻辑代码基本可以平移。3. 从 0 到 1 搭出可运行的最小系统版本选型、登录链路与关键配置3.1 版本选型JDK、Tomcat、MySQL 的常见搭配与兼容性环境配置是整条链路里最玄学的一环很多源码跑不起来不是代码问题而是 JDK 和 Tomcat 版本打架。下面的表格是我整理过的常见稳定搭配照着选能少踩一半坑技术栈推荐版本说明JDK1.8 或 11老项目源码用 JDK 8 最稳JDK 11 也兼容大部分旧代码Tomcat8.5JDK8 / 9.0Tomcat 10 把 javax 改成了 jakarta旧源码会直接编译报错MySQL5.7 或 8.05.7 轻量8.0 的 Connector/J 驱动名和时区参数有变化MySQL Connector/J5.1.49配5.7/ 8.0.33配8.0驱动别混用8.0 驱动连 5.7 会有 SSL 警告但不影响功能代码结构Maven 或纯 Web 项目有 Maven 就用 Maven没有就手动导 jar别两个都来这里最容易翻车的是 Tomcat 10 和 javax.servlet 包名不兼容网上下载的老源码用了 javax.servlet.http.HttpServlet放到 Tomcat 10 下编译直接报错。我的习惯是写任何 Java Web 项目之前先在 pom.xml 或 lib 目录里确认 Servlet API 的版本来源。如果是纯 Servlet 项目直接建一个 Dynamic Web Project 然后选 Tomcat 8.5 运行时能省掉一晚上的排查时间。3.2 登录链路怎么写从浏览器请求到数据库校验登录功能是图书管理系统里唯一涉及会话管理的模块也是最能体现“三层架构”的演示片段。我见过不少源码为了省事在 JSP 里直接写 JDBC 连接和 SQL 查询看起来也能跑但答辩时老师一眼就能看出你没理解分层。这里给出一个标准的登录链路从 Servlet 接收参数开始// web/LoginServlet.java WebServlet(/login) public class LoginServlet extends HttpServlet { private AdminService adminService new AdminServiceImpl(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); // 基础校验空值直接打回避免无效请求打到数据库 if (username null || username.trim().isEmpty() || password null || password.isEmpty()) { req.setAttribute(error, 用户名和密码不能为空); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } // 业务层校验这里应该包含密码摘要比对和状态检查 Admin admin adminService.login(username, password); if (admin ! null) { HttpSession session req.getSession(); session.setAttribute(admin, admin); resp.sendRedirect(req.getContextPath() /book/list); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }这段代码的逻辑分三段先做参数校验再调 service 层完成业务校验最后根据结果决定是跳转成功页还是返回登录页。注意我用了 req.getRequestDispatcher().forward() 而不是 sendRedirect() 返回错误信息因为 forward 可以携带 request 作用域里的 error 属性到 JSP 页面渲染而 redirect 会丢掉。如果你想让失败时地址栏不变这种写法是标准做法。成功路径使用 sendRedirect 是避免表单重复提交的常见手段。对应地service 层的 login 方法只做一件事调用 dao 查询比对密码返回实体或 null。这里有一个值得养成习惯的细节密码比对不要在 Servlet 里做而是放在 service 层因为“密码摘要算法如果从 MD5 换成 SHA-256只有 service 层需要改动”。JSP 页面永远不要出现“数据库”三个字。3.3 分页与模糊查询两条最常用 SQL 的写法与参数绑定图书列表是系统的门面而分页是几乎每一个课程设计都会被追问的点。分页 SQL 本身不难难的是参数计算和总条数查询的封装。常见的做法是先查 totalCount再算 offset 查当前页数据两条 SQL 放在同一个 dao 方法里返回一个 Page 对象// dao/BookDao.java public PageBook findBooksByPage(int pageNum, int pageSize, String keyword) { // 1. 计算偏移量注意 pageNum 从 1 开始 int offset (pageNum - 1) * pageSize; // 2. 查询总条数关键词使用 CONCAT 拼接 % 符号防止用户输入通配符 String countSql SELECT COUNT(*) FROM book WHERE status 1 AND book_name LIKE CONCAT(%, ?, %); int totalCount queryForInt(countSql, keyword); // 3. 查询当前页数据按创建时间倒序 String dataSql SELECT * FROM book WHERE status 1 AND book_name LIKE CONCAT(%, ?, %) ORDER BY create_time DESC LIMIT ?, ?; ListBook list queryForList(dataSql, keyword, offset, pageSize); return new Page(pageNum, pageSize, totalCount, list); }这里推荐用 CONCAT(%, ?, %) 而不是直接在传参时拼 % keyword %原因是前者不会把用户输入里的 % 和 _ 当成通配符能减少一类 SQL 注入和模糊查询结果异常的问题。LIMIT 后面两个参数都用占位符 ? 传 int 值offset 必须在 SQL 层计算好别在 SQL 里写 LIMIT (page-1)*pageSize, pageSizeMySQL 不支持这种表达式语法那会导致每次翻页都报语法错误。4. 借书与还书链路状态机、并发和行锁的三个考验4.1 借书时的“先查后扣”为什么会有并发翻车借书的业务规则看起来就四步查读者是否存在且状态正常、查读者未还数量是否达到上限、查图书是否上架且库存大于零、扣库存并插入借阅记录。但没有任何并发保护的实现在两个人同时借同一本书的最后一本时会双双通过库存检查然后都把库存减一结果是库存变成负一货架上凭空多出一本“负数”的书。这就是经典的丢失更新问题也是面试官最爱让你手写代码的场景。4.2 用状态字段控制借阅记录的生命周期借阅记录表里的 status 字段承担了状态机的角色0 表示借出、1 表示已还、2 表示逾期归还。状态流转的方向是 0 - 1 或 0 - 2不允许从 1 回到 0。还有一个容易被忽略的约束同一本书在相同读者手里只能有一条 status0 的记录否则读者可以“重复借同一本书”库存会被重复扣。这个约束可以在 SQL 层面用部分唯一索引解决但 MySQL 不支持部分索引所以常见做法是查重之后再插入配合事务保证原子性。4.3 事务与行锁三行代码保住库存不为负借书方法必须放在同一个事务里任何一步失败都要回滚全部操作。在 Spring 中使用注解是最简洁的写法如果是纯 Servlet JDBC 项目就得手动管理 Connection 的提交和回滚// service/impl/BorrowServiceImpl.java Transactional(rollbackFor Exception.class) public void borrowBook(int readerId, int bookId) { // 1. 查读者并锁定该行防止并发修改读者状态 Reader reader readerDao.selectByIdForUpdate(readerId); if (reader null || reader.getStatus() 0) { throw new BusinessException(读者不存在或已被禁用); } // 2. 统计未还数量达到上限则拒绝 int borrowingCount borrowDao.countByReaderAndStatus(readerId, 0); if (borrowingCount reader.getMaxBorrow()) { throw new BusinessException(已达到最大借阅数量); } // 3. 查图书并加行锁只锁这一行而不是锁全表 Book book bookDao.selectByIdForUpdate(bookId); if (book null || book.getStatus() 0 || book.getAvailableStock() 0) { throw new BusinessException(图书不存在、已下架或库存不足); } // 4. 扣库存 插入借阅记录两步一起成功 bookDao.decreaseStock(bookId); borrowDao.insert(new BorrowRecord(readerId, bookId, new Date(), dueTime(readerId), null, 0)); }这段代码的关键在 selectByIdForUpdate 这两个方法。SELECT ... FOR UPDATE 是 MySQL InnoDB 提供的行级排他锁事务提交或回滚时才释放。这样两个并发请求同时进来时第二个会阻塞在第一步的行锁上等第一个事务提交后再执行它的库存检查发现库存已经是零就会被业务规则拒绝。这里要注意 FOR UPDATE 必须和事务在同一线程、同一连接里生效Spring 的声明式事务能保证这一点但手动 JDBC 时如果开连接的代码没包住整个方法锁是锁不住的。另外一个常见误用是把整个图书表锁住SELECT * FROM book FOR UPDATE 会触发 InnoDB 对多条记录加锁在表没有索引的情况下甚至退化成表锁借任何一本书都会阻塞其他所有书的借阅操作。正确的做法是 WHERE id ? 精确锁定一行并有主键索引。5. 真正常见的 5 个坑数据库连接、中文乱码、端口占用与内存溢出5.1 数据库连不上驱动、URL 和防火墙的排错顺序现象Tomcat 启动后访问页面报空白或 500控制台出现 Communications link failure 或 Access denied for user看起来是数据库没连上。原因分三类驱动版本和 MySQL 版本不匹配、连接串参数不对、账号权限或 IP 访问受限。驱动不匹配最典型的报错是 ClassNotFoundException 或 No suitable driver连接串不对最常见的坑是没带时区参数MySQL 8.0 驱动会直接拒绝 serverTimezone 为空的连接账号权限问题则表现为 Access denied。解决按顺序排查。先用命令行工具 mysql -u root -p 确认账号密码能登录第二步在代码里打印连接串确认 jdbc:mysql://localhost:3306/library_db?characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai 这段地址、端口、库名没有拼错第三步检查驱动 jar 是不是真的被塞进了 WEB-INF/lib很多 Maven 项目里依赖声明了但没刷新导致运行时找不到类。最后再考虑防火墙拦截远端数据库连接的情况本地开发一般遇不到。5.2 控制台和页面全是中文乱码三处编码要统一现象查询结果在控制台打印正常但在 JSP 页面上显示成问号或乱码或者反过来控制台乱码但页面正常。更隐蔽的是从浏览器传到后端的中文参数在数据库里变成乱码。原因编码不一致的根源是“请求解码字符集、响应编码字符集、数据库连接字符集、数据库表字符集”四者没有统一成 UTF-8。Tomcat 8 以后请求体的解码默认就是 UTF-8 吗不是这取决于你是否设置了 CharacterEncodingFilter而 GET 请求的 URI 解码还受 server.xml 的 URIEncoding 控制。解决三处设置一次到位。第一处是所有 Servlet 或 Filter 里设置 request.setCharacterEncoding(UTF-8)并且必须放在读取任何参数之前第二处是 JSP 页面顶部写 % page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %第三处是 MySQL 连接串末尾带 characterEncodingutf8 并确认表结构是 utf8mb4而不是把数据库建完就动用 ALTER TABLE 一边一边改。这里有条血泪经验如果你只改了 JSP 页面编码而连接串没带字符集参数数据库里会存进已经乱码的数据这时候再改什么都救不回已经坏掉的行。5.3 端口被占用导致启动失败别急着改端口现象Tomcat 启动报错 Port 8080 required by Tomcat v9.0 Server is already in use常见于之前没关干净服务或者别的开发工具占了端口。原因端口被上一个残留在后台运行的系统进程占住也可能是你用 IDE 启动过多个 Tomcat 实例其中一个没有完全停止。直接把 Tomcat 默认端口改成 8081 是最常见的“绕过”手法能用但很丑而且会给后面所有拼接了 8080 地址的代码和演示带来麻烦。解决先定位是谁占了端口。Linux 和 macOS 上执行 lsof -i:8080Windows 上执行 netstat -ano | findstr 8080拿到 PID 后在任务管理器或使用 kill 命令结束它。然后回到 IDE 看 Servers 视图里是不是有旧实例还在 Running全部 Stop 后重新启动。如果问题反复出现检查你是否同时安装了多个版本的 Tomcat 且都配置成了启动时自动运行。5.4 404 和 500 的差别路径写错与空指针的定位方向现象访问 http://localhost:8080/library/book/list 返回 404而调用登录接口返回 500 并伴有 NullPointerException 堆栈。原因404 八成是请求路径和 Servlet 的 WebServlet 注解值对不上比如注解是 /book/list 但 JSP 里表单 action 写了 /bookList这里还涉及项目上下文路径ContextPath的问题如果你没有用 req.getContextPath() 拼路径部署后的根路径一变就全乱。500 则九成是空指针session 里取不到值、查询结果返回 null 后直接调用了属性的 getter、或者请求参数名和 Servlet 里 getParameter 的名字拼写不一致。解决404 先看 IDE 控制台的启动日志里 Context path 是什么把地址栏 URL 和它对齐再看 JSP 里所有 action 和链接统一用 ${pageContext.request.contextPath} 作为前缀。500 就看堆栈里第一行 java.lang.NullPointerException 下面的 “at com.library.dao.BookDaoImpl.xxx” 指向哪个方法往回找是哪条链路传了 null。记住一个总原则先看 URL 和注解是否匹配再看日志里由前端到后端的传参链路。5.5 JVM 内存溢出调大堆大小只是后悔药现象系统运行一段时间后页面响应越来越慢最后日志里出现 java.lang.OutOfMemoryError: Java heap space。原因代码里存在对象得不到释放的泄漏点最常见的是 ResultSet、Statement、Connection 没有在 finally 里关闭或者开启 session 后没有提交和关闭另一个常见场景是每次请求都 new 一个很大的对象放进 session。调大 JVM 的 -Xmx 参数只能推迟崩溃时间不能根治问题。解决把项目里所有获取连接和查询的代码过一遍确保 Connection、PreparedStatement、ResultSet 全部在 finally 块中关闭Java 7 之后可以用 try-with-resources 语法自动关闭。若是 Spring 工程数据源和 MyBatis 框架会替你把连接放回池里但你自己手写的 JDBC 代码仍然是泄漏重灾区。排查时可以用 jstat -gcutil 观察每次 Full GC 后的内存曲线如果持续上升而不回落就是有对象一直没被回收。6. 从能跑到能答辩三个扩展方向、一份自测清单和 Git 提交习惯6.1 三个值得做的扩展逾期计算、热门榜单和批量导入如果基础功能已经跑通我建议你按“成本递增”的顺序挑一个扩展做。最简单的是逾期计算借阅记录表里 already 有 due_time加一个定时任务或在还书方法里计算超过应还天数的记录并把状态置为 2。其次是热门图书榜在借阅记录表上按 book_id 分组、COUNT 取 Top 10这条 SQL 简单到只有五行却能引出 GROUP BY 和 ORDER BY 的组合用法适合放进简历的“技术亮点”一栏。成本最高的是批量导入用 Apache POI 读 Excel 后逐条调用新增图书接口它涉及文件解析、异常回滚和数据校验工作量翻倍但如果你的项目描述需要“企业级”三个字这是最能撑场面的一项。6.2 提交前必做的自测清单按正常路径和异常路径分别过一遍答辩演示最怕的不是功能缺失而是演示时现场翻车。我把自己踩过坑总结成一张清单提交前照着过一遍。正常路径管理员登录成功、新增图书成功、分页翻页和跳页正常、借书成功后库存减一、还书成功后库存加一且记录状态变化。异常路径登录密码错误、借一本书超过读者上限、库存为零时借书、重复借同一本书、修改图书库存为负数、查询关键字为空和包含特殊字符。每条路径都要在演示前手动走一次因为代码里最常见的隐藏 bug 是“异常路径被吞掉”捕获了异常却没有处理界面显示成功但数据没变。6.3 一个我后来才养成的习惯从第一天就用 Git 管理源码多数人从网上下载的源码包里只有一堆解压后的文件和一份数据库脚本没有提交历史。我后来自己重写图书管理系统时第一件事是 git init每完成一个功能模块就提交一次比如 feat(book): 实现图书分页查询。这个习惯的回报在答辩时体现得最明显老师问“这个功能你写了几版”你可以直接调出 Git 历史展示从硬编码到参数绑定的演进过程这份“看得到的过程”比成品本身更能证明代码是你自己写出来的而不是从某个压缩包里解压出来的。当然你也该为它加上一个亿级用户的幻想这个两三万行的项目不会真的上线但它能帮你把“从需求到表结构、从表结构到接口、从接口到页面”的整条链路在面试中讲得滴水不漏。希望帮到你。本文还有配套的精品资源点击获取