简介这是一套面向计算机专业学生课程设计、毕业设计及JavaWeb入门实战的图书管理系统完整源码包基于JSPServletTomcatMySQL技术栈实现可直接部署运行适合需要项目实战练习或毕设参考的学习者。压缩包共202个文件约4.57MB其中50个jsp页面承载前端交互与业务展示33个java源文件与33个class文件构成后台逻辑核心另含15个jar依赖、11个js脚本、7个css样式及1个sql数据库脚本资源结构完整、层次清晰。系统覆盖管理员与读者两大角色功能包括图书馆信息与参数设置、书架管理、读者类型与档案管理、图书类型与档案管理以及图书借阅、续借、归还和借阅到期提醒等核心业务并支持图书档案与借阅记录查询、口令修改。目前已有241人学习下载项目经过严格调试配套数据库脚本可直接导入便于快速理解DAO分层设计与JSP页面跳转逻辑是掌握JavaWeb开发流程与数据库操作的实用参考。1. 图书管理系统这套 JavaWeb 老栈为什么至今还是课程设计首选如果你正在搜「基于JavaWebjsptomcatmysql实现的图书管理系统」大概率是三种人之一要交课程设计的学生、要快速搭一个内部借阅台账的开发者、或者想拿一个完整案例把 JavaWeb 全链路跑通的自学者。这套组合——JSP 做视图、Servlet 做控制、MySQL 存数据、Tomcat 当容器——虽然被很多人叫「老古董」但它的价值恰恰在于链路短、依赖少、每一层都看得见摸得着。你不需要理解 Spring 的 Bean 生命周期也不需要配 Maven 多模块只要 JDK、Tomcat、MySQL 三样东西装好把源码丢进去就能跑。对于想搞明白「一个请求从浏览器到数据库再回来」到底发生了什么的人来说这套栈是最诚实的教材。这篇笔记就按「环境怎么搭 → 库表怎么设计 → 核心功能怎么写 → 坑在哪 → 怎么验证」的顺序把一套能落地的图书管理系统讲透。2. 环境搭建JDK、Tomcat、MySQL 三件套的版本对齐2.1 为什么版本对齐比装最新版更重要很多人翻车的起点不是代码而是版本。JSP 和 Servlet 的 API 在 Tomcat 不同大版本之间是有断层的Tomcat 8.5 和 9 用javax.servlet.*包Tomcat 10 之后改成了jakarta.servlet.*。如果你拿一份基于javax写的图书管理系统源码硬塞进 Tomcat 10编译期就会报package javax.servlet does not exist这不是代码错是容器换了命名空间。所以选型上我一般建议锁死这套组合JDK 8 或 JDK 11、Tomcat 8.5 或 9、MySQL 5.7 或 8.0。JDK 8 和 Tomcat 8.5 是兼容性最稳的一对网上绝大多数 JavaWeb 完整案例都是在这个组合下验证过的。MySQL 5.7 和 8.0 的差异主要在驱动类和连接串参数上后面会单独说。装 JDK 时注意JAVA_HOME要指向 JDK 根目录而不是binPath里加上%JAVA_HOME%\bin。装完在命令行敲java -version和javac -version两个版本号必须一致不一致说明你机器上还有别的 JDK 在抢路。Tomcat 是绿色版解压即用但要确认CATALINA_HOME指向解压目录并且startup.bat能正常拉起浏览器访问http://localhost:8080能看到那只猫的默认页才算容器这一层通了。2.2 MySQL 建库建表图书管理系统的核心表结构数据库这块图书管理系统说复杂不复杂但几张核心表的设计直接决定了后面代码好不好写。最少需要四张表用户表、图书表、借阅记录表、图书分类表。下面是我常用的建表脚本字段命名和类型都按实际借阅场景调过。-- 创建数据库字符集用 utf8mb4 避免中文乱码 CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library_db; -- 用户表区分管理员和普通读者 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码实际项目要加密, real_name VARCHAR(50) COMMENT 真实姓名, role TINYINT DEFAULT 0 COMMENT 0读者 1管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 图书分类表 CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名如计算机、文学 ); -- 图书表 CREATE TABLE t_book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) UNIQUE COMMENT ISBN号, title VARCHAR(100) NOT NULL COMMENT 书名, author VARCHAR(50) COMMENT 作者, publisher VARCHAR(100) COMMENT 出版社, category_id INT COMMENT 外键指向分类, total INT DEFAULT 1 COMMENT 总库存, available INT DEFAULT 1 COMMENT 可借数量, cover_img VARCHAR(200) COMMENT 封面图路径, FOREIGN KEY (category_id) REFERENCES t_category(id) ); -- 借阅记录表 CREATE TABLE t_borrow ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, borrow_date DATE NOT NULL COMMENT 借出日期, due_date DATE NOT NULL COMMENT 应还日期, return_date DATE COMMENT 实际归还日期NULL表示未还, status TINYINT DEFAULT 0 COMMENT 0借出中 1已归还 2逾期, FOREIGN KEY (user_id) REFERENCES t_user(id), FOREIGN KEY (book_id) REFERENCES t_book(id) );这段脚本里有两个参数值得说。一是字符集必须用utf8mb4用utf8的话遇到生僻书名或者 emoji 会截断报错这是血泪经验。二是available字段单独存可借数量而不是每次去t_borrow里 count因为借书还书是高频操作实时统计在数据量大时会拖慢列表页。代价是要保证available和借阅记录一致这个一致性靠业务代码里的借书、还书逻辑来维护后面会讲。2.3 在 IDEA 里配置 Tomcat 并跑通第一个 JSP现在主流做法是在 IDEA 里配 Tomcat 而不是单独启动。步骤是Run→Edit Configurations→ 加号选Tomcat Server→Local在Application server里指向你的 Tomcat 解压目录。然后切到Deployment标签点加号选Artifact把项目的 war 或 exploded 包加进去Application context设成/library。这样启动后访问路径就是http://localhost:8080/library/。这里有个高频坑IDEA 里如果没配ArtifactTomcat 起来了但访问 404。Artifact本质是 IDEA 帮你把web目录、编译后的classes、依赖 jar 打包成一个可部署结构。WEB-INF/lib下的 MySQL 驱动 jar 必须被包含进去否则运行时报ClassNotFoundException: com.mysql.cj.jdbc.Driver。检查方法是启动后看out/artifacts目录下有没有对应的lib文件夹和驱动 jar。3. 核心功能实现从登录到借阅的完整链路3.1 数据库连接与 JDBC 工具类JSP 里直接写 JDBC 代码是能跑但每个页面都写一遍DriverManager.getConnection就是灾难。标准做法是抽一个DBUtil工具类把连接参数集中管理。public class DBUtil { // MySQL 8.0 驱动类5.7 用 com.mysql.jdbc.Driver private static final String DRIVER com.mysql.cj.jdbc.Driver; // 8.0 必须加 serverTimezone否则报时区错误 private static final String URL jdbc:mysql://localhost:3306/library_db ?useUnicodetruecharacterEncodingutf8mb4 serverTimezoneAsia/ShanghaiuseSSLfalse; private static final String USER root; private static final String PWD 你的密码; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL驱动加载失败, e); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } public static void close(Connection c, PreparedStatement p, ResultSet r) { // 按 ResultSet - PreparedStatement - Connection 顺序关闭 try { if (r ! null) r.close(); } catch (SQLException ignored) {} try { if (p ! null) p.close(); } catch (SQLException ignored) {} try { if (c ! null) c.close(); } catch (SQLException ignored) {} } }连接串里三个参数是重点。characterEncodingutf8mb4保证中文写入不乱码serverTimezoneAsia/Shanghai是 MySQL 8.0 的强制要求不写会抛The server time zone value is unrecognizeduseSSLfalse关掉 SSL本地开发没必要开开了反而容易遇到mysql ssl连接错误。驱动类名 8.0 是com.mysql.cj.jdbc.Driver5.7 是com.mysql.jdbc.Driver用错版本会报驱动找不到。3.2 登录功能Servlet 接收表单与 Session 管理登录是整个系统的入口逻辑是JSP 表单提交用户名密码 → Servlet 查库比对 → 成功则把用户信息塞进 Session → 跳转主页。下面是一个精简的登录 Servlet。WebServlet(/login) public class LoginServlet extends HttpServlet { 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); String sql SELECT id, username, real_name, role FROM t_user WHERE username? AND password?; try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { // 登录成功写入 Session User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setRealName(rs.getString(real_name)); user.setRole(rs.getInt(role)); req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /book/list); } else { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } } catch (SQLException e) { throw new ServletException(e); } } }req.setCharacterEncoding(UTF-8)这行必须在getParameter之前调用否则 POST 中文参数会乱码这是新手最常踩的坑之一。登录成功后用sendRedirect而不是forward是为了避免刷新页面时重复提交表单。Session 里存的是整个User对象后续页面通过session.getAttribute(loginUser)取当前登录人做权限判断和「我的借阅」过滤。3.3 图书列表与分页JSP 展示数据的正确姿势图书列表页要解决两件事从数据库查数据、在 JSP 里循环渲染。分页是绕不开的一次性SELECT *全查出来在数据量上来后必卡。分页 SQL 用LIMIT实现。// 分页查询page 从 1 开始size 每页条数 public ListBook listBooks(int page, int size) throws SQLException { ListBook list new ArrayList(); String sql SELECT b.*, c.name AS category_name FROM t_book b LEFT JOIN t_category c ON b.category_id c.id ORDER BY b.id DESC LIMIT ?, ?; try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * size); // 偏移量 ps.setInt(2, size); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Book b new Book(); b.setId(rs.getInt(id)); b.setTitle(rs.getString(title)); b.setAuthor(rs.getString(author)); b.setAvailable(rs.getInt(available)); b.setCategoryName(rs.getString(category_name)); list.add(b); } } } return list; }LIMIT的第一个参数是偏移量公式是(page-1)*size第二个是每页条数。JSP 里用 JSTL 的c:forEach遍历比在页面里写 Java 脚本片段干净得多。封面图展示用img src${pageContext.request.contextPath}/upload/${book.coverImg}注意路径要带contextPath否则部署到/library这种非根路径下图片会 404。如果封面图需要按坐标定位裁剪那属于前端 CSS 的object-fit或background-position范畴跟后端无关别在 JSP 里硬算坐标。3.4 借书与还书库存一致性怎么保证借书的核心是「插入借阅记录」和「扣减可借数量」这两步必须一起成功或一起失败否则会出现记录有了但库存没扣或者库存扣了没记录。用 JDBC 事务来保证。public boolean borrowBook(int userId, int bookId) throws SQLException { Connection conn null; try { conn DBUtil.getConn(); conn.setAutoCommit(false); // 开启事务 // 1. 检查库存 String checkSql SELECT available FROM t_book WHERE id? FOR UPDATE; try (PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setInt(1, bookId); try (ResultSet rs ps.executeQuery()) { if (!rs.next() || rs.getInt(available) 0) { conn.rollback(); return false; // 无库存 } } } // 2. 扣库存 String updateSql UPDATE t_book SET available available - 1 WHERE id?; try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setInt(1, bookId); ps.executeUpdate(); } // 3. 插入借阅记录默认借期30天 String insertSql INSERT INTO t_borrow(user_id, book_id, borrow_date, due_date, status) VALUES(?, ?, CURDATE(), DATE_ADD(CURDATE(), INTERVAL 30 DAY), 0); try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setInt(1, userId); ps.setInt(2, bookId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }SELECT ... FOR UPDATE这行是关键它给这行图书记录加了行锁防止两个用户同时借最后一本书时都读到available1然后都扣成 0。没有这个锁并发下库存会变负数。还书逻辑反过来更新借阅记录的return_date和status同时available 1同样要包在事务里。逾期判断可以在查询时用due_date CURDATE() AND return_date IS NULL动态算也可以写个定时任务每天刷status2前者简单后者适合要做逾期提醒的场景。4. 避坑与排查这套栈最容易翻车的五个地方4.1 中文乱码现象是页面显示问号原因是编码链没统一现象JSP 页面提交的中文书名存进数据库变成???或者从库里查出来显示乱码。原因通常有三处没统一JSP 页面头部的pageEncoding、Servlet 里的setCharacterEncoding、数据库连接串的characterEncoding、以及数据库和表的字符集。解决JSP 顶部写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %Servlet 取参数前调req.setCharacterEncoding(UTF-8)连接串带characterEncodingutf8mb4建库建表用utf8mb4。四处对齐后乱码基本消失。4.2 驱动加载失败现象是 ClassNotFoundException原因是 jar 没进 WEB-INF/lib现象启动后访问登录页后台抛java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因MySQL 驱动 jar 只加进了 IDEA 的模块依赖没放进WEB-INF/libTomcat 运行时找不到。解决把mysql-connector-java的 jar 复制到项目web/WEB-INF/lib目录下重新构建 Artifact。注意 8.0 的驱动包名是mysql-connector-java-8.x.x.jar别拿 5.7 的包配 8.0 的库。4.3 时区报错现象是连接被拒原因是 MySQL 8.0 强制要求 serverTimezone现象java.sql.SQLException: The server time zone value ?D1ú±ê×?ê±?? is unrecognized。原因MySQL 8.0 的驱动要求连接串显式指定时区不指定就报这个。解决连接串加serverTimezoneAsia/Shanghai。如果还报检查 MySQL 服务端的time_zone变量必要时在my.ini里设default-time-zone08:00。4.4 404 但 Tomcat 正常启动现象是访问路径打不开原因是 context path 或 Artifact 没配现象Tomcat 控制台没报错浏览器访问http://localhost:8080/是猫页但访问项目路径 404。原因IDEA 的 Deployment 里没加 Artifact或者Application context设成了/而代码里跳转路径写死了/library。解决确认 Deployment 里有 exploded 的 ArtifactApplication context和代码里getContextPath()的用法保持一致。跳转一律用req.getContextPath() /xxx不要写死绝对路径。4.5 库存变负数现象是同一本书被借超原因是没加行锁或没走事务现象并发测试时一本书available变成 -1。原因借书逻辑先查库存再扣减两步之间没有锁两个线程都读到 1 然后都扣。解决查询时用SELECT ... FOR UPDATE加行锁并把查、扣、插包在同一个事务里。如果用的是连接池注意setAutoCommit(false)要在从池里取出连接后立刻设置归还前恢复true否则会影响下一个复用该连接的请求。5. 进阶技巧用一条 SQL 验证数据一致性再谈这套方案值不值得做功能跑通只是及格线真正让这套系统「能交付」的是数据可验证。我习惯在交付前跑一条对账 SQL检查库存字段和借阅记录是否自洽-- 找出 available 与「总数 - 未归还数」不一致的图书 SELECT b.id, b.title, b.total, b.available, b.total - (SELECT COUNT(*) FROM t_borrow br WHERE br.book_id b.id AND br.return_date IS NULL) AS should_be FROM t_book b HAVING b.available should_be;这条查询返回空结果说明库存维护逻辑没漏洞返回了行就说明某次借还操作事务没兜住得回去查那本书的操作日志。这个习惯救过我很多次——上线前跑一遍比上线后被读者投诉「明明显示有货却借不了」要体面得多。再说验证方法。除了对账 SQL我一般会做三组手工测试第一组用两个浏览器同时登录两个读者账号抢借同一本只剩一本库存的书看是否只有一个成功第二组借书后把系统时间或数据库CURDATE()往前调验证逾期状态判断是否生效第三组上传一张中文文件名的封面图看存储路径和页面展示是否乱码。这三组覆盖了并发、时间、编码三个最容易出问题的维度。关于这套方案值不值得投入如果你是要交课程设计它足够完整四张表加五六个 Servlet 就能覆盖增删改查、登录鉴权、事务借阅工作量饱满且每一行都能讲清楚原理。如果你是要做真实的小型图书室管理这套栈也能用但要注意两个边界一是并发量FOR UPDATE行锁在几百人同时借书时会有等待真上规模得换连接池加乐观锁二是密码安全示例里是明文比对实际必须加盐哈希这是底线不是可选项。我自己的习惯是任何带登录的系统密码字段从第一天就存哈希哪怕当时只是练手——因为「先明文跑通以后再改」这个以后往往不会来。希望帮到你。本文还有配套的精品资源点击获取