ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JavaWeb图书管理系统源码实战:SSM分层、JDBC事务与并发借书防重

JavaWeb图书管理系统源码实战:SSM分层、JDBC事务与并发借书防重

简介:这是一套面向JavaWeb初学者与课程设计学习者的图书管理系统完整项目资料,围绕HTML、CSS、JavaScript、JSP、Servlet、JavaBean、JDBC与DAO等技术栈,构建了一个功能完备的在线图书管理应用,可用于课程设计、毕业设计参考或JavaWeb综合实战练习。资源包共3个文件,包含1个docx系统设计报告、1个zip项目源码包和1个sql数据库脚本,分别对应需求分析与架构说明、可运行工程代码以及图书信息表、用户信息表等建表数据,压缩包整体约4.52MB,结构紧凑便于快速导入与部署。目前已有19488人学习下载,热度较高。通过该资料,读者可以完整走通前后端交互、数据库增删查改与MVC分层设计的开发流程,理解Servlet控制器、DAO数据访问层与JavaBean数据封装的协作方式,并借助设计报告梳理需求分析、数据库设计与功能模块实现思路,适合作为JavaWeb入门到进阶的实践案例。

1. 从一份 JavaWeb 图书管理系统源码说起:它到底能跑通什么

很多做 Java 后端的朋友,简历上写“熟悉 SSM”“做过管理系统”,面试官一追问“借阅超期怎么算”“并发借书怎么防重”,就卡壳了。这份 JavaWeb 图书管理系统源码,恰好能补上这个缺口——它不是那种只有增删改查的玩具,而是把图书、读者、借阅记录三张核心表串成了一条完整业务链,登录鉴权、分页检索、借还书、超期罚金计算都有对应实现。适合两类人:一是刚学完 Servlet/JSP 想找个完整项目练手的在校生,二是工作一两年、想回头把 Web 基础打牢的初中级开发者。你拿到手能直接部署到 Tomcat 跑起来,也能顺着代码把 MVC 分层、JDBC 事务、Session 管理这些老生常谈的东西真正看懂。下面我按“先跑通、再拆解、后避坑”的顺序,把这份资源拆开讲。

2. 环境搭建与数据库初始化:把项目从压缩包跑到登录页

2.1 技术栈确认与依赖清单

这份源码走的是经典 JavaWeb 路线,没有上 Spring Boot,对新手反而友好——你能看清每个请求是怎么从浏览器走到 Servlet 再落到数据库的。核心依赖如下:

组件版本建议作用
JDK1.8编译与运行环境
Tomcat8.5 或 9.0Servlet 容器
MySQL5.7 或 8.0数据存储
JDBC 驱动mysql-connector-java 5.1.x / 8.0.x数据库连接
JSTL1.2JSP 页面标签
Servlet API3.1+控制器基础

常见做法是把这些 jar 包统一丢进WEB-INF/lib目录,而不是只挂在 IDE 的 build path 上——后者在部署到独立 Tomcat 时经常报 ClassNotFound,这是血泪经验。如果你用 Maven 管理,pom.xml里把 scope 设成provided的只有 servlet-api 和 jsp-api,其余都要打进 war 包。

2.2 建库建表:三张核心表与初始化脚本

系统围绕图书、读者、借阅记录展开,建表语句我整理成可直接执行的版本。注意字符集用utf8mb4,否则书名里的特殊符号会变问号。

-- 创建数据库 CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library_db; -- 图书表 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, title VARCHAR(100) NOT NULL, author VARCHAR(50), total INT DEFAULT 1, -- 馆藏总数 available INT DEFAULT 1, -- 可借数量 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 读者表 CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(30) NOT NULL, password VARCHAR(64) NOT NULL, status TINYINT DEFAULT 1 -- 1正常 0冻结 ); -- 借阅记录表 CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE, fine DECIMAL(8,2) DEFAULT 0, status TINYINT DEFAULT 0 -- 0借出 1已还 );

available字段是并发控制的关键,借书时对它做UPDATE ... SET available = available - 1 WHERE id = ? AND available > 0,靠数据库行锁保证不会超借。due_date一般设成borrow_date + 30天,超期罚金按每天 0.2 元在归还时结算。初始化完记得插一条管理员账号,否则登录页进不去。

2.3 配置数据源与部署运行

数据库连接参数集中在src/db.properties(或c3p0-config.xml,看源码用的是哪种连接池)。改完账号密码后,用 IDEA 的 Artifacts 打 war 包,丢进 Tomcat 的webapps目录,启动后访问http://localhost:8080/图书系统名/login.jsp。

# db.properties 示例 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/library_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 jdbc.username=root jdbc.password=你的密码

serverTimezone这个参数在 MySQL 8.0 下必须加,否则启动就抛时区异常,这是新手翻车率最高的一处。部署后如果登录页样式丢失,检查静态资源是不是被WEB-INF挡住了——WEB-INF下的文件浏览器不能直接访问,CSS/JS 要放在和WEB-INF同级的static目录。

3. 借还书业务逻辑拆解:从 Servlet 到 JDBC 事务

3.1 请求流转:一个借书动作经过哪些层

以“借书”为例,完整链路是:borrow.jsp表单提交 →BorrowServlet接收 → 调BookService.borrow()→BookDao执行两条 SQL(扣减可借数、插入借阅记录)→ 返回结果给 JSP 渲染。分层的好处是 Servlet 只管收参和跳转,业务规则沉在 Service,SQL 沉在 Dao。

// BorrowServlet 核心片段 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int bookId = Integer.parseInt(req.getParameter("bookId")); Reader reader = (Reader) req.getSession().getAttribute("currentReader"); if (reader == null) { resp.sendRedirect("login.jsp"); return; } try { boolean ok = bookService.borrow(bookId, reader.getId()); req.setAttribute("msg", ok ? "借阅成功" : "库存不足或已达借阅上限"); } catch (Exception e) { req.setAttribute("msg", "系统异常,请稍后重试"); e.printStackTrace(); } req.getRequestDispatcher("borrowResult.jsp").forward(req, resp); }

这里currentReader从 Session 取,而不是每次从表单传 readerId——否则用户改个隐藏域就能替别人借书,这是权限校验的基本功。bookService.borrow()里要先查该读者当前未还数量,超过 5 本直接拒绝,再执行扣减。

3.2 事务控制:两条 SQL 必须同生共死

借书涉及“扣库存”和“写记录”两步,任何一步失败都得回滚,否则会出现库存扣了但没借阅记录的脏数据。JDBC 原生事务写法如下:

public boolean borrow(int bookId, int readerId) throws SQLException { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 1. 扣减可借数量,available > 0 作为乐观锁条件 String sql1 = "UPDATE book SET available = available - 1 WHERE id = ? AND available > 0"; PreparedStatement ps1 = conn.prepareStatement(sql1); ps1.setInt(1, bookId); if (ps1.executeUpdate() == 0) { conn.rollback(); return false; // 库存不足,回滚 } // 2. 插入借阅记录 String sql2 = "INSERT INTO borrow_record(book_id, reader_id, borrow_date, due_date, status) VALUES(?,?,CURDATE(),DATE_ADD(CURDATE(), INTERVAL 30 DAY),0)"; PreparedStatement ps2 = conn.prepareStatement(sql2); ps2.setInt(1, bookId); ps2.setInt(2, readerId); ps2.executeUpdate(); conn.commit(); // 两步都成功才提交 return true; } catch (SQLException e) { if (conn != null) conn.rollback(); throw e; } finally { DBUtil.close(conn); } }

setAutoCommit(false)是事务起点,commit()是终点,中间任何异常都走rollback()。available > 0写在 UPDATE 的 WHERE 里,比先 SELECT 再判断更安全——高并发下两个线程同时查到 available=1,都去扣就会变负数,而带条件的 UPDATE 只有一个能成功。这是这份源码里最值得抠的一处细节。

3.3 超期罚金与归还逻辑

归还时先算罚金:逾期天数 = 实际归还日 - 应还日,大于 0 就乘日罚金。代码里用ChronoUnit.DAYS.between()算天数,比手动减时间戳清爽。

LocalDate due = record.getDueDate().toLocalDate(); LocalDate now = LocalDate.now(); long overdue = ChronoUnit.DAYS.between(due, now); BigDecimal fine = overdue > 0 ? BigDecimal.valueOf(overdue * 0.2) : BigDecimal.ZERO;

归还同样要事务:更新借阅记录状态和归还日期、把available加回去、写入罚金。三件事绑一起,缺一个都会让账对不上。罚金金额建议存DECIMAL而不是DOUBLE,浮点累加会出现 0.30000000000000004 这种玄学值。

4. 常见问题排查:部署和运行阶段最容易翻车的五处

4.1 启动报 404,登录页都打不开

现象:Tomcat 启动无报错,访问项目路径返回 404。原因通常是 war 包名和访问路径不一致,或者web.xml里welcome-file没配login.jsp。解决:看 Tomcat 控制台打印的 context path,按实际路径访问;检查web.xml的<welcome-file-list>是否包含登录页。

4.2 中文乱码,书名全变问号

现象:页面输入的中文书名存进库变成???。原因有三层:JSP 页面没声明pageEncoding="UTF-8"、请求体没设request.setCharacterEncoding("UTF-8")、数据库连接串缺characterEncoding=utf8。解决:三处一起改,缺一处都不行。GET 请求的中文还要在 Tomcat 的server.xml里给 Connector 加URIEncoding="UTF-8"。

4.3 数据库连接池耗尽,页面卡死

现象:用一会儿就报Cannot get a connection。原因是 Dao 里拿了 Connection 没在 finally 里 close,连接被占满。解决:所有getConnection()必须配try-finally关闭,或者用 try-with-resources。连接池最大连接数别设太大,本地测试 10 个足够,设 100 反而拖慢启动。

4.4 借书成功但库存没减

现象:借阅记录插进去了,available还是原值。原因是两条 SQL 没放在同一事务里,或者第一条 UPDATE 的 WHERE 条件写错导致影响行数为 0 却没判断。解决:按 3.2 的写法,executeUpdate()返回值必须判断,为 0 立即回滚并返回失败。

4.5 部署到独立 Tomcat 报 ClassNotFound

现象:IDE 里跑得好好的,打成 war 丢进 Tomcat 就找不到 MySQL 驱动。原因是驱动 jar 只加在了 IDE 的 Libraries 里,没进WEB-INF/lib。解决:确认WEB-INF/lib下有mysql-connector-java-xxx.jar,Maven 项目检查该依赖 scope 不是provided。

5. 进阶改造与验证:把这份源码用出面试价值

跑通只是起点,真正让它变成你简历上的亮点,得动手改几处。第一,把 JDBC 原生连接换成 Druid 或 HikariCP 连接池,观察maxActive、maxWait参数对并发的影响,这是面试常问的调优点。第二,给借书接口加一层 Redis 缓存图书库存,用decrement原子操作替代数据库行锁,对比两种方案在 100 并发下的响应差异——不用真压测,用 JMeter 跑 50 线程循环 20 次就能看出区别。第三,把超期罚金规则抽成策略模式,日罚金、封顶金额、宽限期都做成可配置,这样面试官问“规则变了怎么办”你有话可说。

验证改造是否成功,我一般走三步:先看功能,借一本还一本,确认库存和记录都对;再看异常,手动把数据库停掉,页面应该给友好提示而不是抛堆栈;最后看并发,用两个浏览器同时借同一本只剩 1 册的书,必须一个成功一个提示库存不足。这三步走完,这套系统才算真正吃透。

从那以后我每次拿到一份管理系统源码,都强制先跑通登录、再压一遍核心业务、最后改一处架构,不然就只是“看过”而不是“做过”。希望这份拆解帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进