简介:这是一份基于 Java 开发的图书管理系统完整源码包,面向高校课程设计、Java Web 初学开发者以及需要快速搭建管理类项目的程序员。系统覆盖图书录入、查询、借阅、归还、续借等核心业务,代码采用 MVC 分层结构,并涉及数据库表设计、Spring 框架集成、ORM 映射、前端交互、用户认证、异常处理与日志记录等常见技术栈,适合对照项目学习完整开发流程。压缩包内共 212 个文件,包括 31 个 Java 源文件、112 个 class 编译文件、2 个数据库文件、2 个 jar 依赖包、56 张 JPG 图片,以及 Eclipse 项目配置、UML 模型和 myumldata 设计数据,整体大小仅 4.01MB。资源中图书添加、借阅、归还、读者管理等窗体模块均有对应源码,配合界面图片和可导入的数据库文件,能够直观还原系统各功能页面的实际运行效果。通过逐个模块阅读和运行调试,可以理解从界面操作到数据持久化的完整路径,尤其适合想要完成课程作业或毕业设计的开发者对照仿写。目前已有 1872 人学习下载,是一套轻量而完整的 Java 项目实战参考资料。
1. Java图书管理系统源代码:它不是简单课设,而是你面试前夜的查漏清单
每个 Java 学习者几乎都绕不开“图书管理系统”这个选题。这套源代码从功能上看就是用户登录、图书增删改查、借还书和超期记录,表面平平无奇,但打开项目后你会看到它把 Servlet、JSP、JDBC、会话管理、事务提交全部串在了一个完整流程里,恰好是 Java Web 初级岗位面试时最爱问的四件事。很多同事小伙子说,自己简历上写了“熟悉 Java Web”,但被问到“借书时库存扣减和借阅记录插入怎么保证一致性”就卡住,多半是当初课设只把代码跑通,没把事务边界看清楚。这篇拆解就按一套典型源码来拆:先看分层,再跑起来,再做二次开发,最后把最容易被坑的节点挨个点名。你可以直接下载一套源码跟着改,也可以只把自己需要的那几个模块抽走。
2. 拆开源码看架构:从 JSP/Servlet 到 JDBC 事务的分层逻辑
很多同学拿到代码第一件事是点“运行”,看到页面能开就觉得完事,其实架构上没捋清,后面改一处崩一处。常见的 Java 图书管理系统分成三层:浏览器请求先到 JSP 和 Servlet,Servlet 负责接收参数并调用业务对象,业务对象再通过 DAO 访问 MySQL。这套分层不一定复杂,但足以应付课程设计和初级面试。
2.1 技术栈选型:为什么这条链路最保险
先看一套点名率高的组合。
| 层 | 组件 | 建议版本 | 选型原因 |
|---|---|---|---|
| 表现层 | JSP + JavaScript + CSS | JSP 2.3 | 直接在页面写页面标签,改完重启即可看到效果 |
| 控制层 | Servlet | Servlet 3.1 | 支持@WebServlet注解,省去 web.xml 配置 |
| 业务层 | JavaBean / Service | JDK 8 | 把借书、还书、用户校验封装成方法 |
| 数据层 | JDBC + DAO | mysql-connector-java 5.1.x | 依赖最少,能手动控制连接和事务 |
| 容器 | Tomcat | Tomcat 8.5 | 与 Servlet 3.1 版本对应,IDE 内置更好 |
这里有一个实际考虑:如果你的源码已经是 Spring Boot 版本,那 Maven 依赖会少很多,但 Servlet 版本会变成 Servlet 4.0,连接池通常用 HikariCP,事务直接靠@Transactional。用哪种方案,直接决定你在面试时怎么回答“事务怎么实现”。我一般建议先把 JSP/Servlet 版本吃透,因为它把每一步暴露在代码里,没有 Spring 帮你擦屁股,理解反而更深刻。
为什么要用 Servlet 3.1 而不是旧的 web.xml 配置?因为@WebServlet注解让路由一目了然,你不用在 XML 里找 class 名。JDK 8 够用,代码里不会出现你还没搞懂的var或record。如果你机器里装了多个 JDK,务必确认JAVA_HOME指向的是 8 或 11,而不是最新的 21,否则 Tomcat 可能直接启动失败。
提示:如果源码里的角色字段用
role而不是type,说明它的权限判断写在 Servlet 层,而不是数据库层面,这一点不影响主流程,但面试时可以顺势聊到 RBAC 模型。
2.2 核心流程:借书的会话、事务与库存扣减
图书管理系统的核心操作是“借书”。流程并不复杂:用户登录后请求借阅页面,点击借书,后端先检查图书库存,再插入借阅记录,确认插入成功后,把库存减 1。这里的关键是“插入记录”和“减库存”必须在一个事务里。用 Java 业务方法解释就是:
// 业务层借书方法,参数为数据库连接、图书ID、用户ID public boolean borrowBook(Connection conn, int bookId, int userId) throws SQLException { boolean result = false; conn.setAutoCommit(false); // 关闭自动提交,开启事务 try (PreparedStatement ps1 = conn.prepareStatement( "UPDATE book SET stock = stock - 1 WHERE id = ? AND stock > 0")) { ps1.setInt(1, bookId); int updateRows = ps1.executeUpdate(); if (updateRows == 0) { throw new SQLException("库存不足或者图书不存在"); } try (PreparedStatement ps2 = conn.prepareStatement( "INSERT INTO borrow (user_id, book_id, borrow_time, status) VALUES (?, ?, NOW(), 1)")) { ps2.setInt(1, userId); ps2.setInt(2, bookId); ps2.executeUpdate(); } conn.commit(); result = true; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); // 恢复自动提交 conn.close(); } return result; }注意这里用stock > 0作为条件,如果更新的行数为 0,就说明没库存,直接抛异常并回滚,避免出现“库存负数”的尴尬局面。事务边界在 try 里,失败一定rollback(),最后一定close()。很多同学只在方法开头连个 Connection,方法里到处都是 new Statement,报错后流程中断,资源没关,这就是后面“连接池爆掉”的隐患。
会话管理同样值得拆开看。登录成功后,Servlet 会执行request.getSession().setAttribute("user", user),之后的借还书操作都从会话拿用户 ID,而不是每个页面都重新传。这个设计对应面试题里的“Cookie 和 Session 区别”,在代码里理解更简单:Cookie 存在客户端,Session 存在服务端,JSESSIONID 是它们之间的钥匙。
2.3 数据库表结构与初始化 SQL
拆源码免不了先看数据表。标准表三张:用户表、图书表、借阅记录表。下面是按常见源码提取的建表 SQL。
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(100) NOT NULL, `role` varchar(20) DEFAULT 'user', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `book` ( `id` int(11) NOT NULL AUTO_INCREMENT, `book_name` varchar(200) NOT NULL, `author` varchar(100) DEFAULT NULL, `publisher` varchar(100) DEFAULT NULL, `stock` int(11) DEFAULT '0', `total_stock` int(11) DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `borrow` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `book_id` int(11) NOT NULL, `borrow_time` datetime DEFAULT NULL, `return_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;说明一下:stock和total_stock分开设计很关键,一个管当前库存,一个管历史总量,不然“还书后恢复库存”会越算越乱。borrow.status用整型而不是字符串,1 表示借出,0 表示已还,这样查询超期记录时直接用索引,效率更好。如果源码里没有total_stock,你自己加一个也不影响主流程。
3. 本地跑起来:JDK、Tomcat、MySQL 环境配置与部署步骤
不少读者卡在第一步:Web 项目下载下来,代码没问题,但环境不对就是起不来。这套系统的坑大多不是语法,而是 Java 环境配置、Tomcat 版本、MySQL 驱动版本之间的匹配问题。
3.1 先说 Java 环境配置里的玄学:JDK 与编码
按步骤来。先把 JDK 装好,版本建议 JDK 8 或 JDK 11。JDK 8 对应 Tomcat 8.5,JDK 11 对应 Tomcat 9,但它俩在简单的 JSP/Servlet 项目上没有本质区别。打开命令行输入java -version确认环境变量已经生效:
java -version # 输出里有 1.8.0_202 或 11.0.21 均正常如果提示“找不到 java”,先检查JAVA_HOME是否指向安装目录,PATH里是否加了%JAVA_HOME%\bin。配置完重新开终端,因为部分系统不会动态刷新环境变量。这里多提一句:如果你用 IDE 工具启动项目,IDEA 或 Eclipse 有时不会读取系统环境变量,需要到 Run Configuration 里手动选 JDK,这个细节很容易让人怀疑人生。
更隐蔽的是编码问题。源码里的 JSP 文件如果按 UTF-8 保存,而 Windows 默认 GBK,IDE 没有自动转换,改一改文件再保存就会出现中文乱码。打开 IDE 的编码设置,把全局编码改到 UTF-8,这是一个容易忽略但必须做的步骤。靠“玄学”排查半天乱码,最后发现只是保存编码不对,这种情况我见过太多次。
3.2 导入项目、初始化数据库、配置连接
导入源码前先建好数据库:进入 MySQL 命令行或图形客户端,执行源码提供的book.sql。
mysql -u root -p < book.sql如果没有脚本,就按第 2 章的建表语句手动执行。执行完确认表已经创建:
mysql -u root -p -e "use book_db; show tables;"然后打开项目的db.properties或jdbc.properties:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/book_db?useUnicode=true&characterEncoding=UTF-8 jdbc.username=root jdbc.password=你的密码注意密码不要带多余空格,URL 里务必加上useUnicode=true&characterEncoding=UTF-8,否则页面中文全是问号。如果你用的是 MySQL 8.x,驱动类名会变成com.mysql.cj.jdbc.Driver,同时要补serverTimezone=Asia/Shanghai,这是很多新版 MySQL 跑老项目的第一道坎。另外,如果你的密码里有特殊字符,比如@或#,需要做 URL 转义,否则连接串会被截断,这个坑非常隐蔽。
3.3 部署到 Tomcat 并验证登录
通常有两种部署方式。一种是用 IDE 内置 Tomcat 直接运行,在 IDEA 中配置 Tomcat Server 选择本机安装目录,把项目打 war 包或者直接 artifacts。另一种是在命令行手工部署,把编译后的项目目录拷到 Tomcat 的webapps下面,再启动:
cd /path/to/apache-tomcat-8.5/bin sh startup.sh # Windows 下是 startup.bat然后访问http://localhost:8080/项目名/login.jsp。如果 404,先看 Tomcat 控制台的启动日志有没有报错,很多情况是context.xml里的数据源没配对。验证流程是登录一个管理员账号,进入图书列表,点一次借书再点一次还书,然后看 MySQL 里borrow表的status是否按预期变化。这一步跑通,说明你的环境已经就绪,后面改代码才有意义。
如果你使用的是 IDEA,启动失败时先看输出窗口最后几行有没有Exception或Error,而不是只看浏览器 404。很多老项目缺 jar 包,控制台会有java.lang.NoClassDefFoundError,这时候去Project Structure -> Libraries里面把lib目录加上就好。
4. 加上自己的功能:从图书排序到 MyBatis Plus 改造
源码能跑通只是第一步,真正让评委或面试官眼前一亮的通常是你在源码基础上有自己的改动。下面几个改造点按难度递增,都是这套系统上常见的加分项。
4.1 用 Java 排序给图书列表加“按库存排序”
项目里图书列表最常按id排序,但如果你想按“可借数量从低到高”排,SQL 里加ORDER BY stock ASC即可。不过我想提醒你一个关键:如果查询结果已经放到List<Book>里,在 Java 内存中怎么排序,这正是面试里反复问的Comparable和Comparator的区别。很多基础不牢的人背了题,但不会写实代码。
// 按库存排序,库存相同再按书名排序 List<Book> bookList = bookDao.listAll(); bookList.sort(Comparator.comparingInt(Book::getStock) .thenComparing(Book::getBookName));这段代码用到了Comparator.comparingInt和thenComparing,注意Book::getStock是方法引用,Java 8 才支持。如果是老版本 JDK 6,就写匿名类。面试时如果聊到这里,你顺手画出Comparator和Comparable的区别,比背十道面试题管用。实际开发中不建议像冒泡排序那样手写循环,因为排序逻辑交给 JDK 的TimSort,性能稳定且代码更短。
4.2 从 JDBC 到 MyBatis Plus:把数据层改成自动映射
如果你已经用上了 Spring Boot 版本,或者想把老项目改造成面向新团队的风格,替换 JDBC 是必经之路。图书管理系统表少、关系简单,最适合练习 MyBatis Plus。实体类按表结构写出:
@Data @TableName("book") public class Book { @TableId(type = IdType.AUTO) private Integer id; @TableField("book_name") private String bookName; private String author; private String publisher; private Integer stock; @TableField("total_stock") private Integer totalStock; }Mapper 接口可以写得非常薄:
public interface BookMapper extends BaseMapper<Book> { // BaseMapper 已经自带 insert/update/selectById 等通用方法 default List<Book> selectAvailable() { return selectList(new LambdaQueryWrapper<Book>().gt(Book::getStock, 0)); } }@TableField负责把book_name映射到bookName,否则 MyBatis Plus 会按驼峰转下划线去查字段。如果你在面试中说“我做过 MyBatis Plus 的实体映射”,这个细节是面试官最容易深挖的地方。另一个面试亮点是“根据实体类生成建表 SQL”:MyBatis Plus 的代码生成器可以根据数据库表反向生成实体,也可以让实体类配合 AutoGenerator 生成建表脚本。但注意自动生成 SQL 不会帮你处理索引和字段注释,真要上线还是得 DBA 过一遍。
4.3 前端页面传参与 Servlet 接收的对照关系
改前端功能时最常犯的错,是把 POST 参数和请求属性搞混。JSP 里提交一个表单:
<form action="bookServlet" method="post"> <input type="hidden" name="action" value="add"> <input type="text" name="bookName"> <input type="text" name="stock"> <button type="submit">提交</button> </form>Servlet 里这样接收:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); // 先处理请求编码 String action = request.getParameter("action"); String bookName = request.getParameter("bookName"); int stock = Integer.parseInt(request.getParameter("stock")); // 注意转换异常 }getParameter拿的是请求参数,getAttribute拿的是服务端塞进 request 的值,比如request.setAttribute("msg", "操作成功")。这个细节很基础,但我在排查同事代码时见过太多人拿getParameter去读setAttribute写的内容,结果一直是 null,然后改了半天。Java 基础里的数据类型转换在这里也有体现:stock如果用户不填,Integer.parseInt会抛NumberFormatException,所以上线前要补一个 try-catch 或者校验工具类。
5. 排查手记:Java 图书管理系统最常见的五个翻车现场
这一套系统我帮人调过不下二十次,很多问题的现象是一样的,但原因千奇百怪。为了让你少走弯路,写五条最典型的踩坑记录,每条都是“现象 -> 原因 -> 解决”的套路,建议你收藏后对照排查。
5.1 环境与编码问题排查
记录一:登录页全是乱码。现象:打开login.jsp,页面能显示,但标题和按钮全是??或乱码。 原因:JSP 文件保存编码和页面声明的编码不一致。例如文件用 GBK 保存,而 JSP 头部写的是<%@ page contentType="text/html; charset=UTF-8"%>。 解决:用 IDE 打开文件,点击右下角编码,选择 UTF-8 转换,再检查 JSP 头部是否也声明 UTF-8。同时把 Tomcat 的conf/server.xml里 Connector 加上URIEncoding="UTF-8",这样 GET 请求里的中文参数也不会乱码。
记录二:java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。现象:启动 Tomcat 后请求图书列表,控制台报找不到驱动类。 原因:mysql-connector 的 jar 没有放到WEB-INF/lib目录,或放错到了服务器全局 lib。旧代码用的类名com.mysql.jdbc.Driver,高版本 MySQL 驱动 8.x 会把主类换成com.mysql.cj.jdbc.Driver,老类名在 8.0.11 以后被移除。 解决:最省事的是用 5.1.49 版本的 mysql-connector-java,或者把db.properties里的驱动类名改成com.mysql.cj.jdbc.Driver。改完以后重新发布项目,并且清除 Tomcat 的缓存目录。
记录三:IDEA 里跑过一次以后,改代码再启动,页面还是老版本。现象:JSP 修改后,浏览器刷新还是旧页面。 原因:Tomcat 在 IDE 运行时用了target/classes或out目录,而你没有重新编译,甚至浏览器缓存了 JSP。 解决:IDEA 里执行 Build -> Rebuild Project,再把 Tomcat 的缓存清理干净。浏览器强制刷新用 Ctrl+Shift+R,或者隐身窗口访问。有些老项目用的是 JetBrains 的旧版插件,热部署经常失效,这时候不用纠结,直接重启 Tomcat 最可靠。
5.2 数据库连接与事务问题排查
记录四:运行两小时后页面超时,后台报Connection is not available。现象:系统刚启动一切正常,用过一段时间后突然所有数据库请求都失败。 原因:源码里每次 DAO 都DriverManager.getConnection(),用完只关了结果集,没有关闭 Connection,连接数被耗尽。 解决:给 DAO 方法统一入口,在finally中关闭 conn。一个偷懒但有效的做法是把 Connection 的关闭放在 finally 块:
Connection conn = null; try { conn = DBUtil.getConnection(); // 业务代码 } finally { if (conn != null) { conn.close(); // 而不是只关 ps } }如果源码已经用连接池,检查连接池的最大连接数和回收时间设置,通常maxWait不能设为 -1,否则请求会一直排队,看起来像“假死”。
记录五:借书成功后,图书列表的库存没变化,或者出现了“库存负数”。现象:借阅记录插入了,但book.stock还是原值,多借几本甚至变成负数。 原因:业务层没有开启事务,或者开启了事务但在 catch 里没有rollback()。MySQL 默认自动提交,所以 UPDATE 单独执行会立即生效,但如果同一个方法里有两步操作,第二步报错不会让第一步退回。 解决:参照第 2.2 节的代码,把两步操作放进同一个事务,conn.setAutoCommit(false)以后,所有操作要么全部提交,要么全部回滚。同时在书表加检查:WHERE id=? AND stock>0,被影响行数为 0 时直接抛异常,这样库存负数这个坑从根本上堵死。不要指望数据库里的触发器,逻辑放业务层更好排查。
6. 把借书事务抽成一个可测试的方法:给源码留一道后悔药
系统能跑通之后,我建议你做一次重构:把BorrowServlet里的业务代码抽到一个独立的BorrowService类。很多源码把 SQL 直接写在 Servlet 的doPost()里,这样做演示没问题,但你要想在面试中讲清楚,或者以后复用到别的项目,模块化是必须的。
6.1 从一个 Servlet 到可复用的 Service
把borrowBook方法单独拎出来,传参时不要传 HttpServletRequest,而是传 int 类型,这样方法不依赖 Web 容器,任何地方都能调用。这步做完,你本地验证只需要一个小 main 方法:
public class LocalTest { public static void main(String[] args) throws Exception { Connection conn = DBUtil.getConnection(); BorrowService service = new BorrowService(); boolean ok = service.borrowBook(conn, 1, 101); System.out.println(ok ? "借书成功" : "借书失败"); conn.close(); } }别小看这个 main,它比 JUnit 更适合新手。不需要加载 Spring,不需要 mock,直接连真实数据库,你能直观看到事务提交后book.stock的变化。想测库存不足就传一个很大的 bookId,或者把库存改成 0;想测回滚就在方法中间手动抛异常,观察记录是否写入。整套流程跑完,你对事务的理解会超过多数只会“背八股”的面试者。
6.2 验证完以后,把你学到的写进简历
验证通过后,把这段代码提交到自己的 Git 仓库,提交信息写“refactor: 抽取借书事务到 Service 层”。将来投简历时,你可以在项目经历里写一句“独立重构图书管理系统,将借书逻辑抽离为可测试 Service,并通过异常模拟验证事务回滚”,这句话比“熟悉 SSM”有用得多。从那以后,我每次拿到别人的 Java 项目源码,都强制自己先画一遍请求到数据库的调用链,再想怎么测试它的核心逻辑。希望帮到你。
本文还有配套的精品资源,点击获取