简介:这是一套面向Java Web初学者与课程设计者的完整图书馆管理系统实战源码,基于JSP+Servlet+MySQL三层架构实现,覆盖用户权限分级、图书借阅全流程及后台管理核心功能。资源共112个文件,含45个Java业务逻辑类(如BookServlet、BorrowDaoImpl、LoginFilter等)、24个JSP页面(登录、图书列表、借阅记录等前端视图)、37个PNG界面图标与2个GIF动态元素,辅以PDF说明文档和MD项目说明,整体压缩包仅1.76MB,轻量易部署。已有263人学习下载,适合Java Web入门者通过可运行项目理解MVC模式、Servlet生命周期、数据库连接池及会话控制等关键知识点。读者可直接导入IDE运行,完整体验管理员、学生、游客三角色交互逻辑,并参考清晰的分层代码结构(如Utils工具类、Dao数据访问层、Servlet控制层)快速掌握企业级Web开发基础范式。
1. 这不是又一个“Hello World”JSP项目:它是一套能跑通借阅闭环、带真实MySQL事务控制、Filter拦截链和反射工具类的图书馆系统源码
你可能已经点开过十几个标着“JSP图书管理系统”的压缩包,解压后发现只有 index.jsp 和 login.jsp 两个页面,连数据库建表语句都得自己猜字段名。但这次不一样——这个JSP图书馆图书管理系统源码.zip是少有的、在 Tomcat 8+ + MySQL 5.7 环境下实测可完整走通“学生登录 → 查书 → 借阅 → 扣库存 → 记日志 → 归还 → 库存回滚”全链路的工程级 JSP 实战包。它不玩花哨的前端框架,但用原生 JSP 标签+Servlet 分层+DAO 模式把 MVC 拆得清清楚楚;它没上 Spring,却靠LoginFilter.java实现了角色会话拦截,靠BorrowDaoImpl.java里的@Override public boolean borrowBook(...)方法封装了带START TRANSACTION / COMMIT / ROLLBACK的 JDBC 事务逻辑;更关键的是,它连ReflectionUtils.java都写好了——不是摆设,是真被BookServlet.java拿来动态 setProperty 绑定表单参数到 Book 实体的。如果你正卡在“学完 Servlet 却写不出带业务逻辑的完整系统”,或者需要一份能当课程设计答辩底稿、又能拆解进自己项目的 JSP 脚手架,这份源码就是那个“抄得安心、改得明白、跑得稳当”的基准版本。
2. 从零部署:Tomcat + MySQL 环境准备与数据库初始化(含建库脚本与字符集避坑)
2.1 环境版本确认与基础服务启动
这套系统对运行环境有明确依赖,不是所有 Tomcat 版本都能直接跑通。实测稳定组合为:Tomcat 8.5.99(或 8.5.x 最新版) + MySQL 5.7.42(非 8.0+) + JDK 1.8.0_361。为什么强调 MySQL 5.7?因为源码中jdbc:mysql://localhost:3306/library?useSSL=false&serverTimezone=GMT%2B8连接串默认未启用allowPublicKeyRetrieval=true,而 MySQL 8.0 默认要求该参数,否则Class.forName("com.mysql.jdbc.Driver")会抛SQLException: Access denied for user——这不是密码错,是驱动握手失败。JDK 必须是 1.8,因ReflectionUtils.java中使用了java.lang.reflect.Method.invoke(obj, args)的老式调用方式,高版本 JDK 对AccessibleObject.setAccessible(true)的安全限制更严,会导致IllegalAccessException。
提示:不要用 Tomcat 10+。其 Servlet API 已升级至 Jakarta EE 9,包名从
javax.servlet.*变为jakarta.servlet.*,而本项目所有import javax.servlet.http.HttpServlet;等引用将全部报错,修改成本远超重装 Tomcat 8.5。
启动步骤:
# 确认 JDK 版本 java -version # 输出必须含 "1.8.0_" # 启动 MySQL(以 Windows 服务为例) net start mysql57 # 启动 Tomcat(解压后 bin/startup.bat,Linux 用 startup.sh) # 启动后访问 http://localhost:8080,看到 Apache Tomcat 首页即成功2.2 数据库创建与字符集强制统一
源码未提供.sql文件,但所有建表语句已内嵌在src/dao/DBUtil.java的静态块中(这是本项目一个隐蔽但关键的设计)。你需要手动执行初始化:
-- 1. 创建库,显式指定字符集,避免中文乱码 CREATE DATABASE library CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 2. 切换到该库 USE library; -- 3. 手动执行 DBUtil.java 中的建表语句(摘录核心部分) CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role ENUM('admin','student','guest') DEFAULT 'guest', realname VARCHAR(20), phone VARCHAR(15), email VARCHAR(50) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE books ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, title VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(50), publish_date DATE, price DECIMAL(8,2), stock INT DEFAULT 0, description TEXT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE borrow_records ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, borrow_date DATETIME DEFAULT CURRENT_TIMESTAMP, return_date DATETIME NULL, status ENUM('borrowed','returned','overdue') DEFAULT 'borrowed', fine_amount DECIMAL(6,2) DEFAULT 0.00, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意:
utf8mb4是硬性要求。若用utf8(MySQL 中实际是 utf8mb3),插入 emoji 或某些生僻汉字时会截断或报错。DBUtil.java中getConnection()方法里conn.setCharacterEncoding("UTF-8")仅作用于客户端连接,库和表层面的字符集才是根本。
2.3 WAR 包构建与部署(跳过 Maven,直击 classpath 根源)
本项目无pom.xml,是纯传统 Webapp 结构。部署关键在于WEB-INF/classes 目录的 class 文件生成路径和lib 依赖的完整性:
将下载的
JSP图书馆图书管理系统源码.zip解压到工作目录,假设为D:\library-src确认目录结构:
D:\library-src\ ├── src\ # Java 源码(BookServlet.java 等) ├── WebContent\ # JSP、CSS、JS、图片(对应最终的 webapp) │ ├── WEB-INF\ │ │ ├── web.xml # 核心配置,含 servlet-mapping 和 filter │ │ └── lib\ # 必须包含 mysql-connector-java-5.1.47.jar │ └── index.jsp └── build.xml # Ant 构建脚本(备用)编译 Java 源码(命令行方式,确保
javac在 PATH):# 进入 src 目录 cd D:\library-src\src # 编译所有 .java 文件,输出到 WebContent\WEB-INF\classes javac -d ..\WebContent\WEB-INF\classes -cp "..\WebContent\WEB-INF\lib\mysql-connector-java-5.1.47.jar" *.java dao/*.java service/*.java utils/*.java说明:
-cp参数指定了 MySQL 驱动路径,这是编译DBUtil.java中Class.forName(...)所必需的。若提示package javax.servlet does not exist,需将tomcat/lib/servlet-api.jar加入-cp,如-cp "D:\apache-tomcat-8.5.99\lib\servlet-api.jar;..\WebContent\WEB-INF\lib\mysql-connector-java-5.1.47.jar"将整个
WebContent目录复制到 Tomcat 的webapps下,并重命名为library:xcopy /E /I D:\library-src\WebContent D:\apache-tomcat-8.5.99\webapps\library重启 Tomcat,访问
http://localhost:8080/library/login.jsp即可开始测试。
3. 核心功能模块解析:Servlet 分层逻辑、Filter 拦截链与反射工具的实际调用链
3.1 三层架构如何落地:从 LoginServlet 到 UserDAO 的完整调用流
系统严格遵循 JSP/Servlet 经典三层:表现层(JSP)→ 控制层(Servlet)→ 业务层(Service,隐含在 Servlet 中)→ 数据访问层(DAO)。以管理员登录为例,追踪LoginServlet.java的执行路径:
// LoginServlet.java 第 42 行左右 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); // 1. 调用 DAO 查询用户(此处 Service 层被省略,逻辑直连 DAO) UserDao userDao = new UserDaoImpl(); User user = userDao.findByUsername(username); // 查询数据库 if (user != null && BCrypt.checkpw(password, user.getPassword())) { // 2. 登录成功,设置 session 属性 request.getSession().setAttribute("user", user); request.getSession().setAttribute("role", user.getRole()); // 3. 根据角色跳转(MVC 中的 View 选择) if ("admin".equals(user.getRole())) { response.sendRedirect("admin/index.jsp"); } else if ("student".equals(user.getRole())) { response.sendRedirect("student/index.jsp"); } } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } }关键点说明:
UserDaoImpl.java继承自BaseDao,复用executeUpdate和executeQuery方法;密码使用BCrypt加密(LoginServlet.java中BCrypt.hashpw(...)生成),而非明文存储,这是本项目安全性的基础保障。BaseDao.java中getConnection()方法每次获取新连接,closeAll(...)确保资源释放,避免连接泄漏。
3.2 LoginFilter:如何用 Filter 实现未登录重定向与角色权限隔离
LoginFilter.java是本项目权限控制的核心,它不依赖任何框架,纯靠HttpServletRequest.getRequestURI()和HttpSession.getAttribute()实现细粒度拦截:
// LoginFilter.java public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(false); // 不创建新 session String uri = request.getRequestURI(); // 如 "/library/admin/userList.jsp" // 白名单:登录页、验证码、静态资源放行 if (uri.contains("/login.jsp") || uri.contains("/CodeServlet") || uri.contains(".css") || uri.contains(".js") || uri.contains(".gif")) { chain.doFilter(req, resp); return; } // 黑名单:未登录时禁止访问 admin 和 student 目录 if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp?redirect=" + uri); return; } // 角色校验:admin 可访问所有,student 不能进 admin 目录 String role = (String) session.getAttribute("role"); if ("student".equals(role) && uri.contains("/admin/")) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权访问管理员页面"); return; } chain.doFilter(req, resp); // 放行 }参数说明:
request.getContextPath()返回应用上下文名(如/library),确保重定向 URL 正确;redirect=参数用于登录成功后跳回原请求页面,这是用户体验的关键细节。web.xml中<filter-mapping>的<url-pattern>/*</url-pattern>保证了全局拦截。
3.3 ReflectionUtils:不只是玩具,它是 BookServlet 表单绑定的生产级实现
BookServlet.java处理图书新增/编辑时,没有为每个字段写request.getParameter("title"),而是用反射批量注入:
// BookServlet.java 第 68 行 Book book = new Book(); ReflectionUtils.setProperties(book, request.getParameterMap()); // 核心调用 // ... 后续调用 bookDao.save(book)ReflectionUtils.java的setProperties方法是本项目最值得细读的工具类:
public static void setProperties(Object obj, Map<String, String[]> paramMap) { for (Map.Entry<String, String[]> entry : paramMap.entrySet()) { String name = entry.getKey(); String[] values = entry.getValue(); if (values == null || values.length == 0) continue; String value = values[0]; // 取第一个值(忽略多选) try { // 1. 获取属性对应的 setter 方法 String methodName = "set" + capitalize(name); Method method = obj.getClass().getMethod(methodName, String.class); // 2. 类型转换:简单处理 int/double/Date if (method.getParameterTypes()[0] == int.class && !value.isEmpty()) { method.invoke(obj, Integer.parseInt(value)); } else if (method.getParameterTypes()[0] == double.class && !value.isEmpty()) { method.invoke(obj, Double.parseDouble(value)); } else if (method.getParameterTypes()[0] == Date.class && !value.isEmpty()) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); method.invoke(obj, sdf.parse(value)); } else { method.invoke(obj, value); } } catch (Exception e) { // 忽略无法设置的属性(如 id 自增字段),不中断流程 System.err.println("Failed to set property: " + name + ", reason: " + e.getMessage()); } } }逻辑说明:
capitalize()将"title"转为"Title",拼出setTitle(String)方法名;类型转换只覆盖常用类型,stock字段是int,price是double,publish_date是Date,完全匹配Book.java的属性定义。这种设计大幅减少模板代码,且比 Struts 的ActionForm更轻量。
4. 避坑指南:五个让开发者凌晨三点还在查日志的真实问题与血泪解决方案
4.1 现象:登录后跳转到admin/index.jsp显示 404,但文件明明存在
原因:web.xml中<servlet-mapping>的<url-pattern>与admin/index.jsp的物理路径不匹配。本项目web.xml定义了AdminServlet映射为/admin/*,但admin/index.jsp是静态页面,未被任何 Servlet 处理,而 Tomcat 默认不支持直接访问WEB-INF外的子目录 JSP(除非配置default servlet)。
解决:确认admin/index.jsp位于WebContent/admin/index.jsp(非WEB-INF/admin/),且web.xml中未对该路径做<security-constraint>限制。若仍 404,检查 Tomcatconf/web.xml中defaultservlet 的<url-pattern>/是否被覆盖。
4.2 现象:借阅图书时BorrowDaoImpl.java报java.sql.SQLException: No operations allowed after connection closed
原因:BorrowDaoImpl.borrowBook()方法中,Connection conn = DBUtil.getConnection()获取连接后,在try-with-resources或finally块中提前关闭了conn,但后续updateBookStock()方法又试图用同一conn执行 SQL。
解决:打开BorrowDaoImpl.java,定位borrowBook方法。找到conn.close()调用位置,确保它只在方法末尾finally块中执行一次。正确模式应为:
Connection conn = null; PreparedStatement ps1 = null, ps2 = null; try { conn = DBUtil.getConnection(); ps1 = conn.prepareStatement("UPDATE books SET stock = stock - 1 WHERE id = ?"); ps1.setInt(1, bookId); ps1.executeUpdate(); ps2 = conn.prepareStatement("INSERT INTO borrow_records (...) VALUES (...)"); ps2.executeUpdate(); } finally { DBUtil.closeAll(conn, ps1, ps2); // 统一关闭,且 conn 在最后关 }4.3 现象:ImgUtils.java上传封面图片失败,request.getPart("cover")返回 null
原因:JSP 表单未设置enctype="multipart/form-data",或web.xml中未声明 Servlet 3.0+ 的 multipart 支持。本项目基于 Servlet 3.0,需在web.xml顶部声明version="3.0"并添加metadata-complete="true"。
解决:检查WebContent\WEB-INF\web.xml第一行:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_0.xsd" version="3.0" metadata-complete="true">同时,book_add.jsp中表单必须为:
<form action="${pageContext.request.contextPath}/BookServlet" method="post" enctype="multipart/form-data">4.4 现象:CodeServlet.java生成的验证码图片全是黑块,无文字
原因:ImgUtils.java中BufferedImage创建时使用了BufferedImage.TYPE_INT_ARGB,而 Tomcat 在 Linux 服务器上无图形界面(headless mode),导致Graphics2D渲染失败。
解决:修改ImgUtils.java的createImage()方法,强制启用 headless 模式并更换图像类型:
// 在 createImage() 方法开头添加 System.setProperty("java.awt.headless", "true"); // 将 BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // 改为 BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);4.5 现象:ReflectionUtils.java设置publish_date时抛ParseException,日期格式不匹配
原因:BookServlet.java接收的publish_date参数来自 JSP 表单的<input type="date">,其值格式为yyyy-MM-dd,但ReflectionUtils.java中SimpleDateFormat使用了"yyyy年MM月dd日"(中文格式),导致解析失败。
解决:打开ReflectionUtils.java,找到setProperties方法中Date类型处理段,将new SimpleDateFormat("yyyy年MM月dd日")替换为new SimpleDateFormat("yyyy-MM-dd")。同时,检查Book.java中publishDate字段的 getter/setter 方法签名是否为java.util.Date,而非java.sql.Date(后者需特殊处理)。
5. 进阶技巧:给 BorrowDaoImpl 添加借阅超期自动计算与罚款逻辑(含 MySQL 事件与 Java 双校验)
5.1 理解现有罚款机制:被动触发 vs 主动扫描
当前系统中,“罚款缴纳”功能仅作为管理员后台的一个手动操作入口(admin/fine.jsp),并未与借阅行为实时联动。真正的业务痛点是:学生借书超过 30 天未还,系统应自动标记status='overdue'并累加fine_amount(如 0.5 元/天)。这需要数据库层定时扫描 + 应用层借阅时校验双保险。
5.2 MySQL 层:创建事件(Event)每日凌晨扫描逾期记录
MySQL 5.7 支持事件调度器,无需外部脚本。先启用事件:
-- 开启事件调度器 SET GLOBAL event_scheduler = ON; -- 创建事件:每天 00:00:00 扫描并更新 overdue 记录 CREATE EVENT update_overdue_records ON SCHEDULE EVERY 1 DAY STARTS '2024-01-01 00:00:00' DO UPDATE borrow_records SET status = 'overdue', fine_amount = GREATEST(0, DATEDIFF(NOW(), borrow_date) - 30) * 0.5 WHERE status = 'borrowed' AND DATEDIFF(NOW(), borrow_date) > 30;说明:
GREATEST(0, ...)确保罚款不为负;DATEDIFF(NOW(), borrow_date)计算天数差;WHERE status = 'borrowed'避免重复更新已标记的记录。此事件独立于 Java 应用,即使 Tomcat 停机,数据库仍能维护罚款状态。
5.3 Java 层:在 BorrowDaoImpl.borrowBook() 中加入借阅前风控检查
不能只依赖数据库事件,因为学生可能在逾期当天再次借书。需在借阅动作发生前,检查该学生是否有未处理的overdue记录:
// BorrowDaoImpl.java 新增方法 public boolean hasOverdueRecord(int userId) { String sql = "SELECT COUNT(*) FROM borrow_records WHERE user_id = ? AND status = 'overdue' AND fine_amount > 0"; Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); ps = conn.prepareStatement(sql); ps.setInt(1, userId); rs = ps.executeQuery(); if (rs.next()) { return rs.getInt(1) > 0; // 存在未缴罚款的逾期记录 } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.closeAll(conn, ps, rs); } return false; } // 修改 borrowBook() 方法开头 public boolean borrowBook(int userId, int bookId) { // 新增风控:检查用户是否有未处理逾期 if (hasOverdueRecord(userId)) { throw new RuntimeException("用户存在未处理的逾期罚款,禁止借阅"); } // 原有借阅逻辑... }逻辑说明:
hasOverdueRecord()返回true时,borrowBook()抛出异常,BookServlet.java中捕获后可跳转到student/overdue.jsp提示用户缴费。这实现了“借阅即风控”,比事后通知更有效。
5.4 前端联动:在 student/index.jsp 中动态显示待缴罚款总额
学生登录后,首页应醒目提示:“您有 2 笔逾期记录,待缴罚款 ¥15.00”。这需要在StudentServlet.java(或复用UserServlet.java)中查询并放入 request:
// StudentServlet.java doGet() 方法中添加 int userId = ((User) request.getSession().getAttribute("user")).getId(); String sql = "SELECT COALESCE(SUM(fine_amount), 0) FROM borrow_records WHERE user_id = ? AND status = 'overdue'"; // ... 执行查询,结果存入 request.setAttribute("overdueFine", fineAmount);JSP 中展示:
<c:if test="${not empty overdueFine && overdueFine > 0}"> <div class="alert alert-warning"> ⚠️ 您有未处理的逾期罚款:<strong>¥${overdueFine}</strong>,请尽快缴纳! <a href="${pageContext.request.contextPath}/admin/fine.jsp?userId=${user.id}" class="btn btn-sm btn-danger">去缴纳</a> </div> </c:if>从那以后我每次接手 JSP 项目,都会先翻web.xml确认version和filter-mapping,再查DBUtil.java看连接池和字符集,最后用grep -r "BCrypt" src/验证密码是否加密——这三步走完,80% 的部署翻车就能提前掐死。希望帮到你。
本文还有配套的精品资源,点击获取