简介这是一套基于Java Web技术开发的图书馆管理系统源码使用Servlet、JSP和JavaBean实现配合MySQL数据库存储数据适合高校图书馆场景和Java Web课程设计使用。压缩包共209个文件包含51个JSP页面、32个Java源码及32个class字节码另附9个jar依赖库、SQL脚本及多张MySQL数据表.frm/.myd/.myi整体大小1.91MB目录结构清晰便于直接导入IDE部署运行。目前已有358人学习下载。系统覆盖MVC分层设计、Servlet与JSP交互、用户登录鉴权、图书与借阅管理等核心流程并附带完整的数据库建表文件对理解Web应用请求转发、异常处理和项目部署均有实际参考价值。读者可基于源码快速搭建一套可用的图书管理系统用于课程设计、毕业设计或Java Web进阶练习。1. 图书馆管理系统一个还在用 JSP Servlet 的 JavaWeb 项目为什么值得跑前阵子接手一个「老人」项目——同事离职留给我一套 javaWeb 图书馆管理系统源码MySQL 版本Servlet JSP JDBC 全手动拼 SQL。第一眼觉得落后跑起来才发现这套东西把 JavaWeb 的核心链路讲得明明白白请求怎么进 Tomcat、Servlet 怎么转发、JDBC 怎么管事务、Session 怎么控权限每一步都是可断点跟进去看的。对刚学完 JavaWeb 但没写过完整项目的人或者毕设想找「能跑、能改、能讲清楚」的系统的同学这套源码比 Spring Boot 黑盒式封装更适合当底子。本篇文章围绕这套源码拆它的模块设计、跑通步骤和二次开发路径新手照着操作能上线演示熟手也能从中看到经典三层架构的实际落法。2. 架构与数据模型三层结构、5 张核心表和一整套权限划分2.1 技术选型为什么 JSP Servlet MySQL 是这套系统的底子这个项目没有引入 Spring、MyBatis 这类框架而是回归到了 JavaWeb 最原生的那套组合Servlet 负责接收 HTTP 请求和控制页面跳转JSP 负责渲染动态页面JDBC 直接操作 MySQL 数据库。从教学和排查角度讲这种选型把「请求进来之后发生了什么」完全暴露在代码里不像 Spring MVC 那样还要经过 DispatcherServlet 的一堆映射规则。亮点在于它的分层特别干净control 层只做参数接收和视图转发dao 层只做 SQL 执行model 层只承载数据没有出现 Servlet 里写大段 SQL 的坏味道。我一般会建议这样理解这套技术栈JSP 是视图Servlet 是控制器DAO 是数据访问层三者通过请求转发和 JavaBean 传值串联。相比现在流行的前后端分离这套方案是服务端渲染的老路子浏览器拿到的就是完整的 HTML 页面好处是调试链路短F12 里能看到表单提交到了哪个 Servlet坏处是页面和 Java 代码耦合度高改样式要动 JSP。作为学习项目这个「短链路」反而是优势——你可以在一个请求的完整生命周期里搞清楚 MVC 到底怎么转的。2.2 目录结构与包职责拿到源码解压后先不要急着丢进 Tomcat建议花十分钟把包结构过一遍。这项目的目录不是随便堆的每个包名都对应一类职责。展开看大概是这样的src/ com.library.control/ 控制层存放各类 Servlet LoginControl.java 处理登录请求 BookControl.java 处理图书增删改查请求 BorrowControl.java 处理借书还书请求 ReaderControl.java 处理读者管理请求 com.library.dao/ 数据访问层封装所有 SQL 操作 BookDao.java BorrowDao.java ReaderDao.java com.library.model/ 实体类对应数据库表结构 Book.java Reader.java Borrow.java com.library.util/ 工具类 DBUtil.java 获取数据库连接 WebContent/ admin/ 管理员页面 book_list.jsp borrow_list.jsp reader/ 读者页面 index.jsp borrow.jsp login.jsp 登录页 WEB-INF/web.xml Servlet 映射配置 database/ library.sql 建库建表脚本控制层的命名很直白LoginControl 就是处理登录的BorrowControl 就是处理借还书的没有任何绕弯子的抽象。DAO 层每个类对应一张表的操作方法名 findByBookName、getBorrowListByReaderId 这种看到名字就知道干什么。这种「一个包一类职责」的组织方式恰恰是很多框架项目里最容易丢的东西——Spring Boot 项目一个 Service 里塞五张表的逻辑是常事而这个老项目反而保持了纪律。2.3 数据库设计权限分离靠一张表搞定数据库脚本在 database 目录下导入 MySQL 后会出现五张核心表表结构如下表名职责关键字段reader用户表区分管理员和普通读者reader_id, username, password, rolebook图书表book_id, book_name, author, publisher, price, stockborrow借阅记录表borrow_id, reader_id, book_id, borrow_time, return_timecategory图书分类表category_id, category_namepenalty罚款记录表penalty_id, reader_id, amount, reason权限是怎么做的答案在 reader 表里有一个 role 字段用字符串区分「admin」和「reader」两种身份。登录成功后把这个值写进 Session每次请求管理员页面时先判断 Session 里的 role 是不是 admin不是就直接重定向到登录页。这种写法在小型系统里非常实用省掉了独立的权限表也降低了理解成本。建表时注意 reader 表里 username 加了唯一索引这是防止重复注册的第一道防线。CREATE TABLE reader ( reader_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(50) NOT NULL, role VARCHAR(20) DEFAULT reader );role 字段默认值是 reader意味着注册进来的人默认只有借书权限管理员账号需要手动往数据库里插或者后期在管理端开放角色提升功能。这套设计在演示环境完全够用但如果要上生产密码还是得做哈希存储后面改造部分会单独说。3. 核心模块拆解从登录到借还书的完整代码链路3.1 DBUtil所有数据库操作的起点整个项目的数据库操作都绕不开 util 包里的 DBUtil 类它就是那个最基础的「连接工厂」。现在看这段代码会觉得原始但它把 JDBC 的核心步骤压缩到了一个类里加载驱动、建立连接、抛异常。这项目里没有用连接池每次操作数据库都是 DriverManager 现拿现用操作完再关闭。常见做法不外乎就是这么一回事。public class DBUtil { private static String url jdbc:mysql://localhost:3306/library?useSSLfalsecharacterEncodingutf8; private static String user root; private static String password 123456; static { try { Class.forName(com.mysql.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs ! null) rs.close(); if (ps ! null) ps.close(); if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }上述代码有两点要留意。第一个是 Class.forName 里的驱动名如果是 MySQL 5.x 用 com.mysql.jdbc.Driver 没问题换到 MySQL 8.0 就得改成 com.mysql.cj.jdbc.Driver否则会直接报找不到驱动类——这个坑我在第 4 章避坑部分还会再提。第二个是关闭顺序一定先关 ResultSet再关 PreparedStatement最后关 Connection顺序反了容易出现资源没释放干净的情况。URL 参数里 characterEncodingutf8 是强制客户端和服务端用 UTF-8 通信少了它中文写入数据库后查询出来就是问号。3.2 登录验证从请求参数到会话保持登录模块是整个系统的门面它串起了 JSP 表单、Servlet 参数解析、DAO 查询和 Session 写入四个环节。核心逻辑写在 LoginControl 里流程是从 request 里拿用户名和密码调 ReaderDao 的 findByUsernameAndPassword 方法查数据库查到就登录成功把用户信息塞进 Session然后按角色跳转到不同首页。protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); ReaderDao dao new ReaderDao(); Reader reader dao.findByUsernameAndPassword(username, password); if (reader ! null) { HttpSession session request.getSession(); session.setAttribute(currentUser, reader); if (admin.equals(reader.getRole())) { response.sendRedirect(admin/book_list.jsp); } else { response.sendRedirect(reader/index.jsp); } } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } }逻辑说明先用 getParameter 拿到表单提交的两个字符串这里没有做非空校验是这块代码一个明显短板——用户什么都不填直接点登录也会走一次数据库查询。findByUsernameAndPassword 是把用户输入的密码原样拼进 SQL 去比对属于明文比对方式。登录成功的标志是 reader 对象非空然后把整个 reader 对象塞进 Session后续页面想显示当前用户名直接用 session.getAttribute(currentUser) 再往下拿属性就行。失败场景用的是 forward 转发而不是重定向这样 request 里的 errorMsg 属性才能在 login.jsp 上显示出来如果你在这里写成 sendRedirect页面上永远看不到错误提示。3.3 借书流程库存判断与事务回滚如果说登录模块是入门那借书模块就是这项目里最有含金量的一段代码。它涉及两张表的写操作往 borrow 表插一条借阅记录同时要把 book 表的 stock 库存减一。这两个动作必须绑在一起要么都成功要么都失败。这个项目处理得挺规矩——手动开启事务catch 到异常就回滚。Connection conn null; PreparedStatement insertBorrow null; PreparedStatement updateStock null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); insertBorrow conn.prepareStatement( INSERT INTO borrow (reader_id, book_id, borrow_time) VALUES (?, ?, NOW())); insertBorrow.setInt(1, readerId); insertBorrow.setInt(2, bookId); insertBorrow.executeUpdate(); updateStock conn.prepareStatement( UPDATE book SET stock stock - 1 WHERE book_id ? AND stock 0); updateStock.setInt(1, bookId); int rows updateStock.executeUpdate(); if (rows 0) { throw new RuntimeException(库存不足借书失败); } conn.commit(); } catch (Exception e) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } request.setAttribute(errorMsg, e.getMessage()); } finally { DBUtil.close(conn, insertBorrow, null); DBUtil.close(null, updateStock, null); }关键点在两个地方。第一个是 setAutoCommit(false)这会让连接进入手动提交模式后续两条 SQL 都执行成功后才统一 commit只要中间任何一步抛异常就执行 rollback把前面已经执行的 SQL 撤销掉。第二个是 UPDATE 语句末尾的 AND stock 0这是库存判断的兜底逻辑——即使 Java 代码里先查过库存大于零并发情况下也可能在这个连接读库存之后、更新之前被别的请求扣掉库存SQL 层面的条件判断是最后一道防线。executeUpdate 返回受影响行数小于 1 就说明没有匹配的库存记录可扣直接抛出异常触发回滚借阅记录也就不会残留。参数说明readerId 和 bookId 是从页面传过来的通常一个来自 Session 里的当前用户一个来自请求参数。borrow_time 直接用数据库函数 NOW()避免 Java 和数据库时区不一致导致时间偏移。这套事务写法放在现在依然规范遇到修改需求时注意别把 setAutoCommit(false) 删掉就行。4. 部署与避坑Tomcat MySQL 跑通的每一步和翻车记录4.1 环境准备与项目导入运行这个项目需要的基础环境是JDK 1.8、Tomcat 8.5 或 9.x、MySQL 5.7 或 8.0外加一个能跑 Web 项目的 IDE常见做法是用 Eclipse 的 Java EE 版本直接导入。如果是 IDEA导入时选择「Import Project」然后选 Eclipse 项目结构注意 JDK 级别选 1.8别默认选到 17否则老项目里一些写法会直接编译报错。导入后有两处必须手工处理。第一处是把 mysql-connector-java 的 jar 包加到项目的构建路径里同时确保它被同步到 WEB-INF/lib 目录下第二处是确认 Tomcat 运行时已经配置到 IDE 里否则项目右键找不到 Run on Server 选项。这里的常见错误是把 jar 包只加到了 Build Path 但没放进 WEB-INF/lib结果 IDE 里编译不报错启动 Tomcat 后一访问数据库相关页面就报 ClassNotFoundException。4.2 数据库初始化与配置修改打开 MySQL 命令行或者 Navicat执行 database 目录下的 library.sql。这脚本会建库建表并把一些测试数据一起 insert 进去管理员账号和几本测试图书都在里面。执行完成后确认 library 库下能看到 5 张表然后打开 DBUtil.java把 url、user、password 三个字段改成你自己 MySQL 的地址和账号。mysql -u root -p database/library.sql改完配置文件有个简单的自检方法写一个带 main 方法的测试类直接 new DBUtil 然后调 getConnection能拿到 Connection 对象就说明数据库层面通了。这一步值得单独做因为很多 Tomcat 启动报错其实是数据库连接参数写错而不是 Web 配置的问题先把变量隔离掉后面排查范围就小多了。4.3 部署到 Tomcat 并验证功能在 IDE 里右键项目选择 Run on ServerTomcat 启动后浏览器访问 http://localhost:8080/项目名/login.jsp。这里项目名取决于你导入时设置的 Context Path一般是工程名也可以手动改成 library。第一次启动如果页面 404优先检查 Tomcat 的 webapps 下有没有生成对应的部署目录以及访问路径里项目名是否和实际部署名一致。验证功能建议按这条路径走先用管理员账号登录确认能跳转到 admin 首页然后走一遍添加图书、查看借阅记录的完整流程再退出登录用普通读者账号登录去试借一本书看看库存是不是减一了。这套流程能覆盖大部分核心代码的分支走通了基本就说明这个项目跑起来了。4.4 避坑记录五个容易翻车的现场现象一Tomcat 启动后访问页面报 ClassNotFoundException: com.mysql.jdbc.Driver原因九成是 mysql 驱动 jar 包没有真正部署到 WEB-INF/lib 目录。IDEA 里加 Build Path 只是编译期有效运行期类加载是找 WEB-INF/lib 下的包。解决方法是把驱动 jar 直接复制进 WebContent/WEB-INF/lib 里然后重新部署。现象二往数据库里插中文查出来全是问号原因分两层数据库表字符集不是 utf8或者 JDBC 连接串没加 characterEncodingutf8。解决方法是建库时指定 DEFAULT CHARSETutf8同时把 DBUtil 里 url 参数补上字符集声明两层都对了才能彻底解决。现象三借书操作偶尔出现「借阅记录在库存没减」或者反过来原因是开发时把两条 SQL 放在两个独立方法里执行了或者虽然在一个方法里但没开事务。解决方法是把插入借阅记录和扣减库存放进同一个事务块参照第 3 章的写法setAutoCommit(false) 加 catch 里 rollback。现象四MySQL 8.0 连接时报 Public Key Retrieval is not allowed比现象三更隐蔽本质是 MySQL 8 默认认证插件是 caching_sha2_password老版本驱动不认识。解决方法是换 mysql-connector-java 8.x 版本同时连接串加 allowPublicKeyRetrievaltrueuseSSLfalse。现象五系统用着用着变卡数据库连接数飙高原因是 DBUtil.close 在部分异常路径上没被执行连接泄漏了。解决方法是检查每个 finally 块是否把 Connection 关闭或者在 catch 块里也调用 close。老项目里最容易漏的是查询方法里 ResultSet 不为空时提前 return导致后面的关闭代码没跑到。5. 二次开发思路给老项目加一个热门图书排行接口跑通只是第一步真正能体现这套源码价值的是改造它。我最建议做的一个改造是「热门图书排行」——按借阅次数倒序展示 Top 10这个功能不复杂但正好覆盖了 DAO 层加方法、控制层加 Servlet、前端加展示三个环节适合作为从读到写的过渡练习。第一步在 BookDao 里加一个方法查询借阅次数最多的前 10 本书public ListBook findHotBooks() { ListBook books new ArrayList(); String sql SELECT b.*, COUNT(br.borrow_id) AS borrow_count FROM book b LEFT JOIN borrow br ON b.book_id br.book_id GROUP BY b.book_id ORDER BY borrow_count DESC LIMIT 10; 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)); books.add(book); } } catch (SQLException e) { e.printStackTrace(); } return books; }这里用 LEFT JOIN 而不是 INNER JOIN是为了把一本都没被借过的书也统计进来借阅次数按 COUNT(borrow.brrow_id) 计没借过的会显示 0。这个写法也解决了排序里 NULL 值的问题——如果借阅表里没有对应记录COUNT 返回 0 而不是 NULL排序不会出错。第二步新建一个 HotBookServlet把查询结果塞进 request转发到一个新建的 hot_book.jsp 页面去展示。改造完这几步你会发现原来的分层结构完全撑得住新功能扩展数据操作进 DAO请求处理进控制层展示进 JSP不需要动任何已有代码文件。从那以后我每次拿到老项目都会强制自己先走一遍「跑通 → 加一个小功能 → 看改动落在哪些层」的完整流程而不是只顾着跑通就收工。这个习惯帮我在改任何历史代码时都心里有底——毕竟刚接手的项目能跑起来只是开始能在一小时内加上一个新功能才是真正摸透了它的脾气。希望帮到你。本文还有配套的精品资源点击获取