☰
JavaWeb图书管理系统源码调试与避坑指南:从环境搭建到事务优化
2026/10/31 6:11:23 网站建设 项目流程

简介:这是一套基于JavaWeb、JSP、Tomcat与MySQL实现的图书管理系统完整源码,面向计算机相关专业正在做毕业设计的学生,以及需要项目实战练习的Java学习者,可直接作为毕设或课程设计参考。资源包共202个文件,以50个jsp页面、33个java源文件与33个class编译文件为核心,配合15个jar依赖、11个js脚本、7个css样式及若干gif、jpg图片资源,另含数据库sql脚本与properties、xml等配置文件,压缩包约4.57MB,结构完整、层次清晰。系统涵盖管理员设置、参数与书架设置、读者类型与档案管理、图书类型与档案管理、图书借阅续借归还、图书档案与借阅查询、借阅到期提醒及口令修改等模块,功能贴近真实图书馆业务场景。项目经过严格调试,可运行于Tomcat7及以上版本、MySQL5及以上环境,搭配Eclipse或IDEA与Navicat即可导入使用。目前已有241人学习,适合希望快速掌握JavaWeb分层开发与数据库交互的读者参考借鉴。

1. 从一份能跑起来的图书管理系统源码说起

很多同学做课程设计或毕设时,最头疼的不是写代码,而是找不到一套结构清晰、能直接跑通的 JavaWeb 项目。网上搜到的要么缺数据库脚本,要么跑起来一堆 500 错误,要么代码写得像一锅粥。这次拆的这套基于 JavaWeb+jsp+tomcat+mysql 的图书管理系统,从目录结构看就规整得多——BorrowDAO、ManagerDAO、BookDAO、ReaderDAO 这些类名一看就知道是按 DAO 模式分层写的,实体类 Borrow、Manager、Book、Reader 也都在,说明作者至少是按标准 MVC 思路走的。它解决的核心问题就一个:给你一套能直接导入 IDE、配好 Tomcat 和 MySQL 就能运行的完整项目,省去从零搭框架的时间。适合正在做毕设的计算机专业学生,也适合想拿一个完整案例练手 JavaWeb 的初学者。下面我从环境搭建、数据库还原、核心模块调试到常见翻车点,一步步拆给你看。

2. 环境搭建与项目导入:Tomcat 和 JDK 版本怎么选

2.1 为什么这套项目对 Tomcat 版本有要求

项目正文写的是 Tomcat7 及以上,但实际跑的时候你会发现,Tomcat 8.5 和 Tomcat 9 的兼容性最好。Tomcat 10 开始把 javax.servlet 包名改成了 jakarta.servlet,这套老项目里的 JSP 和 Servlet 全是 javax 开头的,直接扔进 Tomcat 10 会报 ClassNotFoundException,连登录页都出不来。JDK 方面,JDK 8 是最稳的,JDK 11 也能跑,但 JDK 17 以上可能会遇到反射相关的模块访问警告,虽然不一定报错,但控制台会刷一堆红字,看着心烦。我一般建议直接用 JDK 8 + Tomcat 8.5 的组合,这是经过无数 JavaWeb 老项目验证过的黄金搭档。

数据库用 MySQL 5.7 或 8.0 都行,但要注意驱动包版本。MySQL 5.7 用 mysql-connector-java-5.1.x,MySQL 8.0 用 mysql-connector-java-8.0.x,驱动类名从 com.mysql.jdbc.Driver 变成了 com.mysql.cj.jdbc.Driver,连接 URL 也要加时区和 SSL 参数。这个坑后面避坑章节会细说。

2.2 在 IDEA 里配置 Tomcat 的完整步骤

打开 IDEA,File → New → Project from Existing Sources,选中项目根目录,导入类型选 Eclipse 或 Maven 看项目实际结构。导入后先检查 Project Structure 里的 SDK 是不是 JDK 8,Language Level 调到 8。然后配置 Tomcat:Run → Edit Configurations → 左上角 + 号 → Tomcat Server → Local。

# 检查 JDK 版本,确保是 1.8 java -version # 输出应为 java version "1.8.0_xxx" # 检查 Tomcat 版本 cd /path/to/tomcat/bin ./version.sh # Linux/Mac # 或 version.bat # Windows

配置 Tomcat 时,在 Server 标签页里指定 Tomcat 安装目录,端口默认 8080。关键一步在 Deployment 标签页:点 + 号 → Artifact → 选项目编译输出,Application context 设成 /book 或 /,看你的访问习惯。很多人导入后访问 404,就是因为 Deployment 里没加 Artifact,Tomcat 根本不知道要部署什么。

<!-- 如果项目是 Maven 结构,检查 pom.xml 里的 servlet-api 依赖 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> <!-- 必须加 provided,否则和 Tomcat 自带的冲突 --> </dependency>

这个 provided 作用域是血泪经验:不加的话,项目里会打包一份 servlet-api.jar,和 Tomcat 容器自带的版本打架,启动时报 NoSuchMethodError 或者 ClassCastException,排查半天找不到原因。

2.3 导入后编译报错的排查顺序

导入项目后先别急着跑,按这个顺序检查:第一,看 Project Structure → Modules → Dependencies 里有没有红色报错,缺哪个 jar 就补哪个;第二,看 Web 目录下 WEB-INF/lib 里的 jar 包是否完整,mysql 驱动、jstl、standard 这几个是必须的;第三,检查 src 下的包结构是否和 web.xml 里的 servlet 映射对得上。常见情况是导入后包名变了,比如原本是 com.book.dao,导入后变成 src.com.book.dao,这时候要么改包名,要么改 web.xml 里的 servlet-class。

3. 数据库还原与 DAO 层配置:从 SQL 脚本到连接池

3.1 数据库脚本导入与字符集设置

项目里一般会带一个 .sql 文件,用 Navicat 或者命令行导入都行。命令行方式更干净:

# 登录 MySQL mysql -u root -p # 创建数据库,字符集必须用 utf8mb4 CREATE DATABASE book_manage DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 导入 SQL 脚本 USE book_manage; SOURCE /path/to/book_manage.sql;

字符集这里有个玄学问题:如果数据库用 utf8 而不是 utf8mb4,中文书名、读者姓名可能显示成问号。更隐蔽的是,JSP 页面里如果没写<%@ page contentType="text/html;charset=UTF-8" %>,表单提交的中文到后台就是乱码。我一般会在数据库连接 URL 里直接加上 characterEncoding=utf8,双保险。

3.2 数据库连接配置的三种方式

这套项目大概率用的是 JDBC 直连或者简单的连接池。先看配置文件在哪,通常在 src 下的 db.properties 或者直接在 DAO 类里硬编码。如果是硬编码,找到 DriverManager.getConnection 那一行,改成你的数据库地址、用户名、密码。

// 典型的 JDBC 连接代码,在 DAO 基类或工具类里 public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/book_manage?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "your_password"; public static Connection getConnection() throws SQLException { try { Class.forName("com.mysql.cj.jdbc.Driver"); // MySQL 8.0 用这个 // Class.forName("com.mysql.jdbc.Driver"); // MySQL 5.7 用这个 } catch (ClassNotFoundException e) { e.printStackTrace(); } return DriverManager.getConnection(URL, USER, PASSWORD); } }

URL 里的参数逐个说:useUnicode=true 和 characterEncoding=utf8 解决中文乱码;useSSL=false 关掉 SSL 警告,本地开发不需要加密连接;serverTimezone=Asia/Shanghai 是 MySQL 8.0 必须加的,不加会报时区错误。如果你用的是 MySQL 5.7,serverTimezone 可以不加,但加了也不影响。

3.3 DAO 层各模块的功能拆解

从项目正文列出的类名看,这套系统的 DAO 层分得很清楚:BookDAO 管图书的增删改查,ReaderDAO 管读者档案,BorrowDAO 管借阅记录,ManagerDAO 管管理员账号,ReaderTypeDAO 管读者类型。每个 DAO 里通常有 insert、delete、update、queryById、queryAll 这几个方法。调试的时候,先单独测 BookDAO 的 queryAll,能查出数据说明数据库连通了;再测 insert,能插进去说明写操作没问题。如果 queryAll 返回空但数据库里明明有数据,检查 SQL 语句里的表名是不是和数据库里的一致,MySQL 在 Linux 下默认区分表名大小写,Windows 下不区分,跨平台迁移时容易翻车。

// BookDAO 里典型的查询方法 public List<Book> queryAll() { List<Book> list = new ArrayList<>(); String sql = "SELECT * FROM book_info"; // 注意表名大小写 try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book book = new Book(); book.setBookId(rs.getInt("book_id")); book.setBookName(rs.getString("book_name")); book.setAuthor(rs.getString("author")); list.add(book); } } catch (SQLException e) { e.printStackTrace(); } return list; }

这段代码里用了 try-with-resources,连接和语句会自动关闭,比手动 close 靠谱。如果你拿到的源码里是手动 close 的,注意检查 finally 块里有没有判空,否则一旦前面抛异常,close 又会抛 NullPointerException,把真正的错误盖掉。

4. 核心功能调试:借阅、归还与查询的联调逻辑

4.1 图书借阅功能的完整链路

借阅功能涉及三张表:book_info(图书库存)、reader_info(读者信息)、borrow_info(借阅记录)。点击借阅按钮后,后台要做的事:先查读者有没有超期未还的书,再查这本书库存够不够,然后往 borrow_info 插一条记录,最后把 book_info 的库存减一。这四步必须在一个事务里,否则中间任何一步失败都会导致数据不一致。

// BorrowDAO 里的借阅方法,用事务保证一致性 public boolean borrowBook(int readerId, int bookId) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 检查读者是否有超期未还 String checkSql = "SELECT COUNT(*) FROM borrow_info WHERE reader_id=? AND return_date IS NULL AND due_date < NOW()"; PreparedStatement ps1 = conn.prepareStatement(checkSql); ps1.setInt(1, readerId); ResultSet rs1 = ps1.executeQuery(); if (rs1.next() && rs1.getInt(1) > 0) { conn.rollback(); return false; // 有超期,不允许借 } // 2. 检查库存 String stockSql = "SELECT stock FROM book_info WHERE book_id=? FOR UPDATE"; PreparedStatement ps2 = conn.prepareStatement(stockSql); ps2.setInt(1, bookId); ResultSet rs2 = ps2.executeQuery(); if (rs2.next() && rs2.getInt("stock") <= 0) { conn.rollback(); return false; // 库存不足 } // 3. 插入借阅记录 String insertSql = "INSERT INTO borrow_info(reader_id, book_id, borrow_date, due_date) VALUES(?,?,NOW(),DATE_ADD(NOW(), INTERVAL 30 DAY))"; PreparedStatement ps3 = conn.prepareStatement(insertSql); ps3.setInt(1, readerId); ps3.setInt(2, bookId); ps3.executeUpdate(); // 4. 扣减库存 String updateSql = "UPDATE book_info SET stock=stock-1 WHERE book_id=?"; PreparedStatement ps4 = conn.prepareStatement(updateSql); ps4.setInt(1, bookId); ps4.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn != null) try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { if (conn != null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }

注意FOR UPDATE那行,它是行级锁,防止两个读者同时借同一本书时库存扣成负数。如果你的项目里没加这个,并发测试时可能会看到库存变成 -1,这就是典型的超卖问题。借阅期限 30 天是写死的,实际项目里应该从系统参数表里读,这套源码里如果有参数设置模块,可以改成动态读取。

4.2 归还与续借的日期计算

归还逻辑比借阅简单:更新 borrow_info 的 return_date 为当前时间,同时把 book_info 的库存加一。续借则是更新 due_date,往后延 30 天,但通常限制只能续借一次。日期计算建议在 SQL 里用 DATE_ADD 完成,不要在 Java 里算,因为 Java 的日期 API 在 JDK 8 之前又臭又长,容易算错月份天数。

-- 归还操作 UPDATE borrow_info SET return_date = NOW() WHERE borrow_id = ?; UPDATE book_info SET stock = stock + 1 WHERE book_id = ?; -- 续借操作,限制只能续借一次 UPDATE borrow_info SET due_date = DATE_ADD(due_date, INTERVAL 30 DAY), renew_count = renew_count + 1 WHERE borrow_id = ? AND renew_count < 1;

如果归还后库存没变,检查 book_id 是不是从 borrow_info 里正确取出来的。常见错误是前端传的是 borrow_id,后台却拿它当 book_id 用,结果更新了错误的图书记录。

4.3 查询与到期提醒的实现

系统查询模块一般包括图书档案查询、借阅查询和到期提醒。到期提醒的 SQL 通常是查 due_date 在三天内且未归还的记录:

SELECT r.reader_name, b.book_name, bi.due_date FROM borrow_info bi JOIN reader_info r ON bi.reader_id = r.reader_id JOIN book_info b ON bi.book_id = b.book_id WHERE bi.return_date IS NULL AND bi.due_date BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 3 DAY);

这个查询在数据量大的时候会慢,因为 return_date IS NULL 和 due_date 的范围查询可能走不到索引。优化方案是给 borrow_info 表加一个联合索引:CREATE INDEX idx_borrow_status ON borrow_info(return_date, due_date);。如果项目里没加,自己补上,查询速度能从秒级降到毫秒级。

5. 避坑与常见问题排查

5.1 启动报 404:Deployment 里没配 Artifact

现象:Tomcat 启动成功,控制台没报错,但浏览器访问 http://localhost:8080/ 显示 404。 原因:IDEA 的 Tomcat 配置里,Deployment 标签页是空的,或者 Application context 设成了 /book 但你访问的是根路径。 解决:Run → Edit Configurations → Deployment → 点 + 号添加 Artifact,Application context 设成 / 或者 /book,然后访问对应路径。改完重启 Tomcat。

5.2 数据库连不上:驱动版本和时区参数不匹配

现象:启动时报com.mysql.cj.jdbc.Driver找不到,或者The server time zone value '?D1ú±ê×?ê±??' is unrecognized。 原因:MySQL 8.0 的驱动类名变了,且必须指定 serverTimezone;或者项目里放的是 5.1 的驱动包,但连的是 8.0 的数据库。 解决:确认 MySQL 版本,8.0 用 mysql-connector-java-8.0.x,驱动类写 com.mysql.cj.jdbc.Driver,URL 加 serverTimezone=Asia/Shanghai。5.7 用 5.1.x 驱动,类名 com.mysql.jdbc.Driver。

5.3 中文乱码:从 JSP 到数据库的字符集链路

现象:页面输入中文书名,保存后变成问号,或者查询时查不到。 原因:JSP 页面没声明 UTF-8,或者数据库连接 URL 没加 characterEncoding,或者数据库表本身的字符集是 latin1。 解决:JSP 头部加<%@ page contentType="text/html;charset=UTF-8" language="java" %>;连接 URL 加 useUnicode=true&characterEncoding=utf8;数据库和表都改成 utf8mb4。三处都对了才不会乱码。

5.4 事务不回滚:自动提交没关

现象:借阅时库存扣了,但借阅记录没插进去,或者反过来。 原因:Connection 的 autoCommit 默认是 true,每条 SQL 执行完自动提交,事务根本没生效。 解决:在事务开始前conn.setAutoCommit(false),所有操作完成后conn.commit(),catch 块里conn.rollback(),finally 里恢复 autoCommit 并关闭连接。

5.5 JSP 页面加载后不刷新:缓存和转发混淆

现象:修改了数据库里的数据,刷新页面还是旧数据。 原因:可能是浏览器缓存,也可能是 JSP 里用了 request 转发而不是重定向,导致页面没有重新请求。 解决:在 JSP 头部加<meta http-equiv="Cache-Control" content="no-cache">,或者在 Servlet 里用response.sendRedirect()代替request.getRequestDispatcher().forward()。重定向会发起新请求,数据自然是最新的。

6. 进阶技巧:用连接池和分页查询提升项目质量

如果你想让这套系统在答辩时更有亮点,有两个方向可以改:连接池和分页。现在项目里大概率是每次请求都 DriverManager.getConnection,频繁创建销毁连接,性能很差。换成 Druid 或者 HikariCP,配置好初始连接数和最大连接数,QPS 能翻好几倍。

<!-- 引入 Druid 依赖 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency>
// Druid 连接池配置 public class DBUtil { private static DataSource dataSource; static { DruidDataSource ds = new DruidDataSource(); ds.setUrl("jdbc:mysql://localhost:3306/book_manage?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"); ds.setUsername("root"); ds.setPassword("your_password"); ds.setInitialSize(5); // 初始连接数 ds.setMaxActive(20); // 最大连接数 ds.setMinIdle(5); // 最小空闲连接 ds.setMaxWait(3000); // 获取连接超时时间,毫秒 dataSource = ds; } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }

分页查询则是把SELECT * FROM book_info改成SELECT * FROM book_info LIMIT ?, ?,配合一个 count 查询算总页数。前端加个分页条,每页 10 条或 20 条。这个改动在答辩时很加分,因为面试官和老师都爱问分页怎么实现的。

-- 分页查询,offset 是起始行,pageSize 是每页条数 SELECT * FROM book_info ORDER BY book_id LIMIT #{offset}, #{pageSize}; -- 总数查询 SELECT COUNT(*) FROM book_info;

最后说个我自己的习惯:每次拿到一套新源码,先不改任何代码,原样跑通一遍,确认环境没问题了再动手改。改的时候用 Git 建个分支,每改一个功能就提交一次,这样改崩了能随时回退。这套图书管理系统结构不复杂,但麻雀虽小五脏俱全,把借阅事务、连接池、分页这几个点吃透,JavaWeb 的基本功就算过关了。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询