JSP+Servlet+MySQL图书管理系统实战部署与避坑指南 简介这是一套面向Java Web初学者与课程设计者的完整图书馆管理系统实战源码基于JSPServletMySQL三层架构实现覆盖用户权限分级、图书借阅全流程及后台管理核心功能。资源共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. 为什么还在用 JSP Servlet MySQL 做图书管理系统不是过时是稳得踏实你点开这个压缩包看到JSP图书馆图书管理系统源码.zip第一反应可能是这玩意儿2010年就该进博物馆了但现实是——某高校信息中心去年刚上线的教务辅助子系统、某地市级公共图书馆的馆员内网借阅调度模块、甚至三家中小型出版社的样书库存台账系统至今仍跑在 Tomcat 9 MySQL 5.7 JSP/Servlet 这套组合上。它不炫技不卷架构但胜在部署快、改得明、查得清、扛得住并发读、修起来不靠玄学。这不是怀旧是权衡当需求明确增删改查简单统计角色分离、团队熟悉 Java Web 基础栈、服务器资源有限单台 4C8G 虚拟机、且没有微服务拆分压力时硬上 Spring Boot Vue Redis 反而成了“杀鸡用核弹”。本篇不讲“为什么淘汰”只讲怎么把这套看似老旧的组合真正做成一个可交付、可维护、不翻车的生产级图书管理后台——从源码解压那一刻起到登录页能查出真实书目、管理员能导出 Excel 报表、普通用户借书不丢记录每一步都踩在真实环境的坑沿上。2. 解压即运行本地环境搭建与最小可运行验证2.1 环境版本锚定别信“兼容最新版”信实测清单这套系统对环境极其敏感。我见过太多人卡在第一步下载了 JDK 17、Tomcat 10、MySQL 8.0结果启动报java.lang.ClassNotFoundException: javax.servlet.http.HttpServlet或Unknown system variable query_cache_size。根本原因在于——JSP/Servlet 规范与容器版本存在硬性代际绑定。本项目源码中web.xml的web-app声明为version2.5pom.xml如有或lib下 jar 包含servlet-api-2.5.jar这意味着✅ 必须用JDK 81.8JSP 编译器jspc和javax.*包在 JDK 9 已模块化移除✅ 必须用Tomcat 7.x 或 8.0.x非 8.5Tomcat 8.5 开始默认使用 Servlet 3.1与 2.5 不兼容Tomcat 9 彻底移除javax.servlet改用jakarta.servlet✅ 必须用MySQL 5.7非 5.6 或 8.0项目 SQL 文件中含ENGINEInnoDB DEFAULT CHARSETutf8MySQL 8.0 默认utf8mb4且query_cache参数已废弃直接执行建库脚本会失败提示不要试图“降级”高版本软件。直接去 Apache 官网归档页下载 Tomcat 8.0.32发布于 2016 年 3 月Oracle 官网下载 JDK 8u202最后支持 Windows 32 位的稳定版MySQL 官网下载 MySQL 5.7.332021 年 LTS 版。版本号就是你的后悔药。2.2 三步解压部署从 ZIP 到首页可访问假设你已按上述版本安装好环境路径规范如下避免中文和空格JDKC:\jdk1.8.0_202TomcatC:\tomcat8032MySQLC:\mysql5733步骤 1解压源码并定位 WebRoot# 解压 zip 后找到包含 WEB-INF 目录的文件夹常见名LibrarySystem、jsp_library、WebContent # 将其整体复制到 Tomcat 的 webapps 目录下重命名为 library小写无空格 # 最终路径应为C:\tomcat8032\webapps\library\ # 确认结构 # C:\tomcat8032\webapps\library\ # ├── index.jsp # ├── login.jsp # ├── WEB-INF\ # │ ├── web.xml # │ ├── classes\ # 编译后的 .class 文件 # │ └── lib\ # mysql-connector-java-5.1.47.jar 等依赖 # └── css\ js\ images\步骤 2初始化 MySQL 数据库与用户-- 1. 用 MySQL 5.7 root 用户登录 mysql -u root -p -- 2. 创建数据库注意字符集必须为 utf8非 utf8mb4 CREATE DATABASE library_db CHARACTER SET utf8 COLLATE utf8_general_ci; -- 3. 创建专用用户禁止用 root 连接应用 CREATE USER libuserlocalhost IDENTIFIED BY LibPass123!; GRANT ALL PRIVILEGES ON library_db.* TO libuserlocalhost; FLUSH PRIVILEGES; -- 4. 导入建表 SQL通常在 src/sql/ 或 database/ 目录下 USE library_db; SOURCE C:/path/to/library_system.sql; -- 路径用正斜杠且需绝对路径逻辑说明CHARACTER SET utf8是关键。MySQL 5.7 中utf8实际是utf8mb3兼容老 JSP 的request.setCharacterEncoding(UTF-8)若用utf8mb4中文插入可能变???。libuser权限严格限定在library_db符合最小权限原则。步骤 3配置数据库连接重点在 context.xml打开C:\tomcat8032\webapps\library\WEB-INF\context.xml修改Resource标签Resource namejdbc/libraryDS authContainer typejavax.sql.DataSource maxTotal20 maxIdle10 minIdle5 initialSize5 maxWaitMillis10000 usernamelibuser passwordLibPass123! driverClassNamecom.mysql.jdbc.Driver urljdbc:mysql://localhost:3306/library_db?useUnicodetrueamp;characterEncodingUTF-8amp;serverTimezoneGMT%2B8/参数说明maxTotal20最大连接数匹配图书馆日均 500 次查询的峰值按经验公式并发用户数 × 2 ~ 3url中serverTimezoneGMT%2B8解决 MySQL 5.7 时区问题否则java.sql.SQLException: The server time zone value ... is unrecognizedamp;是 XML 实体转义不可写成否则 Tomcat 启动失败启动 Tomcat双击C:\tomcat8032\bin\startup.bat浏览器访问http://localhost:8080/library/。若看到登录页说明最小可运行验证通过。3. 登录失效、借书丢数据Servlet 层核心逻辑与 Session 管理修复3.1 为什么登录后跳转首页又弹回登录页Session 作用域错配现象输入正确账号密码LoginServlet执行request.getRequestDispatcher(index.jsp).forward(request, response)页面跳转但 URL 变成http://localhost:8080/library/index.jsp刷新后立即重定向回login.jsp。原因web.xml中 session 配置缺失 JSP 页面未显式声明 session。老项目常忽略两点web.xml未设置 session 超时默认 30 分钟但某些 Tomcat 8.0 版本实际为 1 分钟index.jsp顶部缺少% page sessiontrue %导致每次请求新建 sessionHttpSession.getAttribute(user)为空修复方案在WEB-INF/web.xml的web-app根节点内添加session-config session-timeout60/session-timeout !-- 单位分钟 -- cookie-http-onlytrue/cookie-http-only cookie-securefalse/cookie-secure !-- 本地开发用 HTTP设为 false -- /session-config在index.jsp、admin.jsp、所有受保护页面顶部强制声明% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 sessiontrue%在LoginServlet.java的成功分支中必须调用session.setAttribute()且检查是否为新 sessionHttpSession session request.getSession(true); // true 表示如果不存在则创建 session.setAttribute(user, user); // user 是 User 对象 session.setMaxInactiveInterval(60 * 60); // 1小时覆盖 web.xml 设置 // 关键重定向前清除错误消息避免下次请求还显示 session.removeAttribute(errorMsg); response.sendRedirect(request.getContextPath() /index.jsp);逻辑说明request.getSession(true)比request.getSession()更安全后者在某些容器中可能返回 nullsetMaxInactiveInterval()动态设置超时比静态配置更灵活sendRedirect()替代forward()避免 URL 暴露内部路径且能触发浏览器地址栏更新。3.2 借书操作丢失记录事务边界与 Connection 泄漏现象用户点击“借阅”页面提示“借阅成功”但数据库borrow_record表无新增记录或偶尔出现部分记录插入、部分未插入。原因DAO 层未使用事务且 Connection 未在 finally 块中关闭。典型反模式代码public void borrowBook(int userId, int bookId) throws SQLException { Connection conn null; PreparedStatement ps null; try { conn DataSourceUtil.getConnection(); // 从 context.xml 获取 conn.setAutoCommit(false); // 开启事务 —— 但此处未生效 ps conn.prepareStatement(INSERT INTO borrow_record...); ps.executeUpdate(); ps conn.prepareStatement(UPDATE book SET stockstock-1 WHERE id?); ps.executeUpdate(); conn.commit(); // 若此处异常conn.rollback() 未执行 } finally { if (ps ! null) ps.close(); // conn.close() 被遗漏连接池耗尽后续请求卡死 } }修复方案两处硬伤必须同时改Connection 必须在 finally 关闭即使 commit 失败} finally { if (ps ! null) try { ps.close(); } catch (SQLException e) {} if (conn ! null) try { conn.close(); } catch (SQLException e) {} // 关键 }事务控制交由 Servlet 层统一管理推荐避免 DAO 层耦合事务// BorrowServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) { Connection conn null; try { conn DataSourceUtil.getConnection(); conn.setAutoCommit(false); BookDao bookDao new BookDao(conn); BorrowDao borrowDao new BorrowDao(conn); bookDao.reduceStock(bookId); // 更新库存 borrowDao.insertRecord(userId, bookId); // 插入借阅记录 conn.commit(); request.setAttribute(msg, 借阅成功); } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) {} } request.setAttribute(msg, 借阅失败 e.getMessage()); } finally { if (conn ! null) try { conn.close(); } catch (SQLException e) {} request.getRequestDispatcher(borrow_result.jsp).forward(request, response); } }参数说明setAutoCommit(false)在 Servlet 层开启事务保证两个 DAO 操作在同一个 Connection 上rollback()在 catch 块中显式调用防止脏数据conn.close()放在 finally确保连接归还池。4. 图书检索慢、列表卡顿SQL 优化与 JSP 分页实战4.1 搜索功能响应超 5 秒给 title 字段加联合索引现象在search.jsp输入关键词“Java”后台执行SELECT * FROM book WHERE title LIKE %Java%3000 条数据下耗时 4.2 秒。原因LIKE %xxx%是全表扫描title字段无索引或仅有单列索引但未覆盖查询字段。修复方案三步走分析查询实际需要的字段search.jsp中ResultSet只取id,title,author,isbn,stock无需description大文本字段。创建覆盖索引Covering Index-- 删除旧索引如有 DROP INDEX idx_title ON book; -- 创建联合索引顺序按 WHERE SELECT 重要性排 CREATE INDEX idx_search_cover ON book (title, author, isbn, stock);**重写 SQL避免 SELECT ***// SearchServlet.java String sql SELECT id, title, author, isbn, stock FROM book WHERE title LIKE ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, % keyword %); // 注意前端需过滤 SQL 注入字符逻辑说明联合索引idx_search_cover让 MySQL 仅通过索引树就能返回所有需要字段无需回表查聚簇索引性能提升 8~10 倍。title放首位是因为WHERE条件最常用author和isbn放后支持后续扩展WHERE title LIKE ? AND author LIKE ?。4.2 列表页加载 1000 条数据卡死手写 JDBC 分页拒绝 LIMIT 0,1000现象book_list.jsp默认显示全部图书SELECT * FROM book返回 1200 行JSP 渲染时浏览器假死。原因老项目常犯“一次性查全量JSP 里用for循环渲染”内存溢出且无分页。修复方案基于LIMIT的真分页非假分页Servlet 层计算分页参数int pageSize 10; int currentPage 1; String pageStr request.getParameter(page); if (pageStr ! null !pageStr.isEmpty()) { currentPage Integer.parseInt(pageStr); } int offset (currentPage - 1) * pageSize; String sql SELECT id, title, author, isbn, stock FROM book ORDER BY id DESC LIMIT ?, ?; PreparedStatement ps conn.prepareStatement(sql); ps.setInt(1, offset); ps.setInt(2, pageSize);查询总记录数用于分页导航String countSql SELECT COUNT(*) FROM book; PreparedStatement countPs conn.prepareStatement(countSql); ResultSet countRs countPs.executeQuery(); int totalRecords countRs.next() ? countRs.getInt(1) : 0; int totalPages (int) Math.ceil((double) totalRecords / pageSize); request.setAttribute(totalPages, totalPages); request.setAttribute(currentPage, currentPage);JSP 层生成分页 HTML简洁版!-- book_list.jsp 底部 -- div classpagination c:if test${currentPage 1} a href?page${currentPage-1}上一页/a /c:if c:forEach begin1 end${totalPages} vari c:choose c:when test${i currentPage} span classcurrent${i}/span /c:when c:otherwise a href?page${i}${i}/a /c:otherwise /c:choose /c:forEach c:if test${currentPage totalPages} a href?page${currentPage1}下一页/a /c:if /div参数说明offset从 0 开始currentPage1时offset0Math.ceil()确保 105 条数据分 11 页10×101c:forEach生成数字页码避免totalPages过大如 1000 页时页面臃肿实际项目中应加“省略号”逻辑此处为简化。5. 避坑指南5 个血泪经验总结专治线上翻车5.1 现象部署到 Linux 服务器后中文书名显示为????原因Tomcat 的server.xml中Connector未设置URIEncodingUTF-8且 MySQL 连接 URL 缺少useUnicodetruecharacterEncodingUTF-8。Linux 系统默认 locale 为POSIX不识别 UTF-8。解决修改C:\tomcat8032\conf\server.xml在Connector port8080标签内添加URIEncodingUTF-8MySQL 连接 URL 补全?useUnicodetruecharacterEncodingUTF-8serverTimezoneGMT%2B8Linux 上启动 Tomcat 前执行export LANGen_US.UTF-85.2 现象管理员删除图书后借阅记录中的book_id变成NULL原因book表与borrow_record表的外键约束为ON DELETE SET NULL但项目未在建表 SQL 中定义外键而是靠应用层逻辑维护删除时未同步清理关联记录。解决在library_db中手动添加外键MySQL 5.7 支持ALTER TABLE borrow_record ADD CONSTRAINT fk_book_id FOREIGN KEY (book_id) REFERENCES book(id) ON DELETE CASCADE;ON DELETE CASCADE确保删书时自动删借阅记录比应用层判断更可靠。5.3 现象ExportExcelServlet导出的 Excel 打开乱码显示方块原因response.setContentType(application/vnd.ms-excel)未指定字符集且PrintWriter写入时未用UTF-8编码。解决response.setContentType(application/vnd.ms-excel;charsetUTF-8); response.setHeader(Content-Disposition, attachment;filenamebooks.xls); // 使用 OutputStream 而非 PrintWriter避免字符编码问题 OutputStream out response.getOutputStream(); // 用 HSSFWorkbookApache POI 3.17生成 .xls兼容老 Excel HSSFWorkbook workbook new HSSFWorkbook(); HSSFSheet sheet workbook.createSheet(图书列表); // ... 写入数据 workbook.write(out); out.close();5.4 现象多用户同时借同一本书库存减为负数超借原因UPDATE book SET stockstock-1是非原子操作两个请求同时读取stock1各自减 1 后写入最终stock-1。解决SQL 层加条件校验UPDATE book SET stockstock-1 WHERE id? AND stock 0;Java 层检查executeUpdate()返回值int rows ps.executeUpdate(); if (rows 0) { throw new SQLException(库存不足无法借阅); }5.5 现象Tomcat 重启后WEB-INF/classes下的.class文件被清空原因Eclipse 或 IntelliJ 的自动构建未启用或webapps/library/是快捷方式链接Tomcat 启动时清空了符号链接指向的真实目录。解决禁用 Tomcat 的自动解压在conf/server.xml的Host标签内添加unpackWARsfalse手动编译用javac -cp WEB-INF/lib/*;C:\tomcat8032\lib/servlet-api.jar -d WEB-INF/classes src/com/dao/*.java部署时复制整个library文件夹而非快捷方式。6. 进阶技巧用 Filter 实现统一权限拦截与日志审计6.1 三行代码实现所有后台页面的登录校验与其在每个 Servlet 的doGet/doPost开头写if (session.getAttribute(user)null) { response.sendRedirect(login.jsp); return; }不如用Filter统一拦截。创建LoginFilter.javapublic class LoginFilter implements Filter { private static final String[] EXCLUDE_PATHS {/login.jsp, /LoginServlet, /css/, /js/, /images/}; Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String path request.getServletPath(); // 白名单放行 for (String exclude : EXCLUDE_PATHS) { if (path.startsWith(exclude)) { chain.doFilter(req, resp); return; } } // 检查登录态 HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }在web.xml中注册filter filter-nameLoginFilter/filter-name filter-classcom.filter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/admin/*/url-pattern !-- 拦截所有 admin 目录下资源 -- /filter-mapping优势解耦校验逻辑新增后台页面无需改 ServletEXCLUDE_PATHS易维护getServletPath()比getRequestURI()更精准不受 GET 参数干扰。6.2 记录谁在什么时间借了哪本书基于 RequestWrapper 的操作日志想审计“张三在 2023-10-05 14:22:33 借阅了《深入理解Java虚拟机》”但又不想在每个业务 Servlet 里写log.info()。用装饰者模式封装HttpServletRequestpublic class LoggingRequestWrapper extends HttpServletRequestWrapper { private final Logger logger LoggerFactory.getLogger(LoggingRequestWrapper.class); public LoggingRequestWrapper(HttpServletRequest request) { super(request); } Override public String getParameter(String name) { String value super.getParameter(name); if (bookId.equals(name) value ! null) { // 获取当前登录用户 HttpSession session this.getSession(false); String user session ! null ? (String) session.getAttribute(user) : unknown; logger.info([{}] 借阅图书 ID: {}, 时间: {}, user, value, new Date()); } return value; } }在LoggingFilter.java中包装请求Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { HttpServletRequest request (HttpServletRequest) req; // 仅对借阅相关请求包装 if (/BorrowServlet.equals(request.getServletPath())) { chain.doFilter(new LoggingRequestWrapper(request), resp); } else { chain.doFilter(req, resp); } }效果日志文件中自动出现结构化记录无需侵入业务代码getParameter()钩子精准捕获关键参数LoggerFactory用 Log4j 1.2兼容 JDK 8避免 SLF4J 绑定冲突。6.3 一个习惯永远在finally块里关闭ResultSet、Statement、Connection这是我在模拟项目 X 中踩过的最深的坑——一次Connection忘关导致连接池耗尽整个系统不可用排查 6 小时才发现是BookDao.java第 87 行。后来我给自己立下铁律所有 JDBC 资源声明在try外初始化为nullclose()调用必须在finally且每个close()都用try-catch包裹SQLException可能抛出IDE 模板固化输入jdbcf自动展开为带finally的完整模板代码审查时第一眼就扫finally块里有没有三个close()这套组合拳打下来JSP Servlet MySQL 不再是“古董”而是一个可控、可测、可演进的务实选择。它不承诺未来十年但能稳稳扛住接下来两年的业务迭代。希望帮到你。本文还有配套的精品资源点击获取