
简介本资源是一套基于JavaWeb技术栈开发的图书管理系统完整项目面向计算机专业毕业设计学生及Java初学者提供可直接部署运行的实战案例。项目采用JSPServletMySQLTomcat经典组合涵盖系统管理、读者管理、图书管理、借阅归还、查询统计等核心模块功能完整且经过严格调试适合作为课程设计或毕设基础框架。压缩包共202个文件含50个JSP页面实现前后端交互、33个Java类含DAO、Service、Form等分层逻辑、33个编译后Class文件、23个GIF图标资源、15个Jar依赖库及SQL数据库脚本等整体4.57MB结构清晰、层次分明便于理解MVC架构与Web应用开发流程。目前已有241人学习下载资源附带完整源码、建库脚本与配置说明开箱即用显著降低环境搭建与调试门槛助力快速掌握JavaWeb开发全流程。1. 这不是“过时技术堆砌”而是一套能跑通、能交付、能答辩的 JavaWeb 图书管理系统落地闭环你在网上搜“JavaWeb 图书管理系统”十有八九会撞见一堆带【完整源码数据库】的压缩包——解压后是WebContent/目录下密密麻麻的.jsp文件WEB-INF/web.xml里还写着servlet标签lib/下塞着mysql-connector-java-5.1.47.jar。有人嗤之以鼻“JSP 都淘汰了还教这个”但现实是高校课程设计、高职毕设、中小型企业内部工具、政务外网老旧系统维保……这些场景里Tomcat JSP Servlet MySQL 的四件套仍是交付成本最低、调试路径最短、答辩老师最熟悉的技术栈。它不炫技但能让你在 3 天内把登录页、图书增删改查、借阅记录导出全跑起来它不抽象每个 request 走哪条 servlet、session 存什么、SQL 怎么拼全在眼皮底下它不依赖 Spring Boot 自动装配的黑匣子出错时你能精准定位到BookServlet.java第 87 行rs.next()报空指针——而不是在Transactional和Async的嵌套里找后悔药。本文不讲“为什么不用 Spring Boot”只讲怎么用最朴素的 JavaWeb 组件把图书管理系统从 idea 新建项目开始一砖一瓦搭成可运行、可演示、可扩展的完整体。适合正在赶毕设 deadline 的学生、需要快速交付内部管理工具的运维/测试转开发人员以及想亲手摸清 Web 请求生命周期的初学者。2. 从零搭建环境JDK、Tomcat、MySQL、IDEA 四步连环配置2.1 JDK 8 是当前最稳的“地基”别碰 JDK 17 的坑JavaWeb 项目对 JDK 版本极其敏感。JSP 编译器Jasper在 JDK 9 中被移除Tomcat 9 默认支持 JDK 8–11但大量老版servlet-api.jar和jsp-api.jar与 JDK 14 不兼容。我实测过JDK 17 下 Tomcat 启动成功但访问login.jsp直接 500 错误日志报java.lang.NoClassDefFoundError: javax/servlet/jsp/JspFactory——这不是你的代码问题是类加载器找不到旧版 JSP API。所以第一步必须锁定 JDK 8u291Oracle 官方最后稳定版或 OpenJDK 8u362Adoptium 提供。安装后验证java -version # 输出应为java version 1.8.0_291 javac -version # 输出应为javac 1.8.0_291提示Windows 用户务必检查系统环境变量JAVA_HOME指向 JDK 根目录非 JRE且PATH中%JAVA_HOME%\bin在C:\Windows\System32之前否则javac命令可能调用到系统自带的旧版。2.2 Tomcat 8.5.94选它不是因为新而是因为“兼容性补丁最全”Tomcat 9 要求 Servlet 4.0而多数教材级 JSP 示例仍基于 Servlet 3.1Tomcat 10 强制使用 Jakarta EE 命名空间jakarta.servlet.*所有javax.servlet.*包全失效——这意味着你下载的“完整源码”里 90% 的 import 语句要重写。Tomcat 8.5.x 是最后一个同时支持javax.*和 Servlet 3.1 的 LTS 版本且社区补丁最多。我们选 8.5.942023 年 10 月发布修复了 Windows 下中文路径乱码和 Linux systemd 自启失败等关键问题。下载解压后关键三步配置设置CATALINA_HOME环境变量指向解压根目录如D:\apache-tomcat-8.5.94修改conf/server.xml放开 HTTP 端口并启用 UTF-8!-- 找到 Connector 标签修改 port 和 URIEncoding -- Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 / !-- 此行必须加否则中文参数乱码 --验证启动双击bin/startup.batWindows或./startup.shLinux看到控制台输出Server startup in [xxx] ms且浏览器能打开http://localhost:8080显示 Tomcat 首页即成功。2.3 MySQL 5.7.42别用 8.0字符集和驱动坑太多MySQL 8.0 默认caching_sha2_password认证插件而mysql-connector-java-5.1.xJSP 项目标配根本不认识它连接时直接报Access denied for user。MySQL 5.7.42 是最后一个默认mysql_native_password且完全兼容老驱动的稳定版。安装时务必勾选 “Use Legacy Authentication Method”并手动执行-- 登录 MySQL 后执行强制用户用旧认证 ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY your_password; FLUSH PRIVILEGES;注意Navicat 或 DBeaver 连接时驱动选择MySQL (com.mysql.jdbc.Driver)而非MySQL (com.mysql.cj.jdbc.Driver)——后者是 MySQL 8.0 专用与本项目不兼容。2.4 IDEA 配置 JSP 项目不是新建 Spring Initializr而是“传统 Web Application”很多人卡在第一步IDEA 里点 “New Project” 就选错模板。必须选 “Enterprise Edition” → “Web Application” → 取消勾选 “Create web.xml”新版 IDEA 默认不生成我们要手动建。后续步骤Project SDK 选 JDK 8Web Application Version 选 3.1对应 Servlet 3.1Finish 后右键项目 → “Add Framework Support” → 勾选 “Web” 和 “Java EE Persistence”后者用于后续 JDBC 配置最关键在Project Structure→Artifacts中确保Output Layout里WEB-INF/lib/下已包含mysql-connector-java-5.1.47.jar从 Maven Repository 下载或手动复制此时项目结构应为book-system/ ├── src/ # Java 类放这里Servlet、DAO ├── web/ # JSP、CSS、JS 放这里IDEA 2022 默认叫 web不是 WebContent │ ├── index.jsp │ └── WEB-INF/ │ ├── web.xml # 必须手写Servlet 映射全靠它 │ └── lib/ # mysql 驱动放这里 └── pom.xml # 可选但建议加方便管理依赖3. 数据库设计与连接池用 Druid 替代裸 DriverManager抗并发更稳3.1 图书管理系统核心表结构5 张表够用字段命名拒绝“t_book”式玄学很多“完整源码”数据库表名带前缀如t_user,t_book字段用user_name而非username看似规范实则增加 SQL 拼写错误率。我们按实际业务域建表字段名直白、类型精准、索引明确表名字段说明关键约束userid(PK, auto_inc),username(UNIQUE),password(VARCHAR(64), 存 BCrypt 加密后字符串),role(ENUM(admin,librarian,reader))主键索引、username 唯一索引bookisbn(PK),title,author,publisher,publish_year(YEAR),stock(INT DEFAULT 0)ISBN 主键天然唯一publish_year 用 YEAR 类型省空间borrow_recordid(PK),user_id(FK),isbn(FK),borrow_date(DATE),return_date(DATE NULL)复合索引(user_id, borrow_date)加速个人借阅查询categoryid(PK),name(UNIQUE)分类表避免在 book 表存冗余分类名book_categoryisbn(FK),category_id(FK)多对多关联表支持一本书属多个分类提示stock字段设DEFAULT 0而非NULL避免借书逻辑中if (book.stock 0)判空翻车return_date允许 NULL表示未归还。3.2 用 Druid 连接池替代 DriverManager10 行代码解决高并发下的连接泄漏裸写DriverManager.getConnection()在单用户测试时没问题但一旦模拟 50 人同时借书就会出现Too many connections错误——因为每次请求都新建连接用完不关MySQL 默认最大连接数 151。Druid 是阿里开源、Spring Boot 默认集成的连接池但纯 JSP 项目也能用只需 3 步在WEB-INF/lib/放入druid-1.2.21.jarMaven Central 可下载在src/下新建com/book/util/DBUtil.javapackage com.book.util; import com.alibaba.druid.pool.DruidDataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtil { private static DruidDataSource dataSource; static { dataSource new DruidDataSource(); dataSource.setUrl(jdbc:mysql://localhost:3306/bookdb?useUnicodetruecharacterEncodingUTF-8); dataSource.setUsername(root); dataSource.setPassword(your_password); dataSource.setInitialSize(5); // 初始连接数 dataSource.setMaxActive(20); // 最大活跃连接数 dataSource.setMinIdle(2); // 最小空闲连接数 dataSource.setValidationQuery(SELECT 1); // 连接有效性检测 SQL dataSource.setTestWhileIdle(true); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); // 从此处获取连接自动复用/回收 } }在 Servlet 中调用Connection conn null; PreparedStatement ps null; try { conn DBUtil.getConnection(); // 不再用 DriverManager ps conn.prepareStatement(UPDATE book SET stock stock - 1 WHERE isbn ?); ps.setString(1, isbn); ps.executeUpdate(); } finally { if (ps ! null) ps.close(); if (conn ! null) conn.close(); // 归还连接到池非真正关闭 }逻辑说明dataSource.getConnection()从池中取连接conn.close()实际是归还连接不是销毁。setValidationQuery(SELECT 1)确保取出的连接有效避免网络抖动导致的Communications link failure。3.3 数据库初始化脚本一键建库建表拒绝手动 Navicat 点点点把建库、建表、插入初始数据写成init.sql放在项目根目录用命令行一键执行-- init.sql CREATE DATABASE IF NOT EXISTS bookdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE bookdb; -- 创建 user 表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, role ENUM(admin,librarian,reader) DEFAULT reader ); -- 插入管理员账号密码明文仅用于演示实际需 BCrypt 加密 INSERT INTO user (username, password, role) VALUES (admin, 21232f297a57a5a743894a0e4a801fc3, admin); -- md5(admin) -- 其他表创建语句...执行命令Linux/macOSmysql -u root -p init.sqlWindows 下用mysql.exe -u root -p init.sql。此脚本必须 UTF-8 编码保存否则中文注释会导致执行失败。4. 核心功能实现Servlet JSP JDBC 三层拆分拒绝“万能 JSP”4.1 登录模块用 Filter 拦截未登录请求比 session 判空更可靠很多“完整源码”在每个 JSP 顶部写% if(session.getAttribute(user)null) response.sendRedirect(login.jsp); %这有严重缺陷JSP 编译成 Servlet 后response.sendRedirect()发生在页面渲染中途易报IllegalStateException: Cannot forward after response has been committed。正确做法是用 Filter 统一拦截新建com.book.filter.LoginFilter.javapackage com.book.filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginFilter implements Filter { Override 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); // 白名单登录页、静态资源、登录处理 Servlet 不拦截 String uri request.getRequestURI(); if (uri.contains(/login.jsp) || uri.contains(/LoginServlet) || uri.contains(.css) || uri.contains(.js)) { chain.doFilter(req, resp); return; } // 拦截无用户 session 且非白名单重定向到登录页 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.book.filter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/*/url-pattern !-- 拦截所有请求 -- /filter-mapping参数说明request.getContextPath()获取应用上下文路径如/book-system避免硬编码路径session.getAttribute(user)存的是登录成功的 User 对象非字符串。4.2 图书 CRUD用 DAO 模式解耦让 JSP 只负责展示拒绝在 JSP 里写% Class.forName(com.mysql.jdbc.Driver); ... %。标准三层JSPView→ ServletController→ DAOModelBookDAO.java封装所有数据库操作BookServlet.java接收请求、调用 DAO、转发结果book-list.jsp只用 JSTL 标签显示数据BookDAO.java关键方法public ListBook findAll() throws SQLException { String sql SELECT b.*, GROUP_CONCAT(c.name) as categories FROM book b LEFT JOIN book_category bc ON b.isbn bc.isbn LEFT JOIN category c ON bc.category_id c.id GROUP BY b.isbn; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { ListBook books new ArrayList(); while (rs.next()) { Book book new Book(); book.setIsbn(rs.getString(isbn)); book.setTitle(rs.getString(title)); book.setAuthor(rs.getString(author)); book.setStock(rs.getInt(stock)); book.setCategories(rs.getString(categories)); // 多分类合并显示 books.add(book); } return books; } }BookServlet.java处理列表请求protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { ListBook books new BookDAO().findAll(); request.setAttribute(books, books); // 绑定到 request 作用域 request.getRequestDispatcher(/book-list.jsp).forward(request, response); } catch (SQLException e) { request.setAttribute(error, 查询图书失败 e.getMessage()); request.getRequestDispatcher(/error.jsp).forward(request, response); } }book-list.jsp展示% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table trthISBN/thth书名/thth作者/thth库存/thth分类/th/tr c:forEach items${books} varbook tr td${book.isbn}/td td${book.title}/td td${book.author}/td td${book.stock}/td td${book.categories}/td /tr /c:forEach /table逻辑说明c:forEach是 JSTL 标签比% for(...) { out.print(...); } %更安全自动 HTML 转义防 XSSrequest.setAttribute()将数据传给 JSP作用域仅本次请求。4.3 借阅功能事务控制 库存校验两步原子化借书不是简单 insert 一条记录必须保证库存 0borrow_record插入成功book.stock减 1三者要么全成功要么全失败。用 JDBC 的事务控制而非依赖 MySQL 存储过程public boolean borrowBook(String isbn, int userId) throws SQLException { Connection conn null; PreparedStatement ps1 null, ps2 null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 步骤1检查库存 ps1 conn.prepareStatement(SELECT stock FROM book WHERE isbn ? FOR UPDATE); ps1.setString(1, isbn); ResultSet rs ps1.executeQuery(); if (!rs.next() || rs.getInt(stock) 0) { throw new SQLException(图书库存不足); } // 步骤2插入借阅记录 ps2 conn.prepareStatement(INSERT INTO borrow_record (user_id, isbn, borrow_date) VALUES (?, ?, ?)); ps2.setInt(1, userId); ps2.setString(2, isbn); ps2.setDate(3, new java.sql.Date(System.currentTimeMillis())); ps2.executeUpdate(); // 步骤3扣减库存 ps1 conn.prepareStatement(UPDATE book SET stock stock - 1 WHERE isbn ?); ps1.setString(1, isbn); ps1.executeUpdate(); conn.commit(); // 手动提交 return true; } catch (SQLException e) { if (conn ! null) conn.rollback(); // 出错回滚 throw e; } finally { if (ps1 ! null) ps1.close(); if (ps2 ! null) ps2.close(); if (conn ! null) conn.close(); } }关键点FOR UPDATE锁住该行防止并发借书时超卖conn.setAutoCommit(false)开启事务conn.rollback()确保异常时数据一致。5. 避坑指南那些让新手崩溃 3 小时却没人告诉你的细节5.1 现象IDEA 启动 Tomcat 报 “源服务器未能找到目标资源”404原因web.xml中servlet-mapping的url-pattern与浏览器访问路径不匹配例如 Servlet 映射/login但你访问http://localhost:8080/login.jspJSP 路径而非http://localhost:8080/loginServlet 路径Artifact配置错误IDEA 的Project Structure→Artifacts中Output Layout未将web/目录设为Web Pages导致 JSP 文件未打包进 warTomcat 部署路径错误IDEA 的Run Configurations→Deployment选项卡中Application context写成了/book-system但你访问的是/解决统一路径Servlet 映射/login前端表单action/loginJSP 页面如login.jsp直接通过http://localhost:8080/login.jsp访问不经过 Servlet检查 Artifact右键项目 →Open Module Settings→Artifacts→ 确保web/目录在Output Layout的Web Pages节点下清理部署Run→Edit Configurations→Deployment→ 删除旧 artifact重新添加Application context留空即根路径/5.2 现象中文乱码——JSP 页面显示 “???”数据库存入 “æŽå”原因Tomcat 的server.xml未配置URIEncodingUTF-8GET 请求参数乱码MySQL 连接 URL 缺少characterEncodingUTF-8导致 JDBC 读写乱码JSP 文件本身不是 UTF-8 编码Windows 记事本默认 ANSI解决server.xml中 Connector 标签加URIEncodingUTF-8已强调DBUtil.java的 JDBC URL 改为jdbc:mysql://localhost:3306/bookdb?useUnicodetruecharacterEncodingUTF-8IDEA 中File→Settings→Editor→File Encodings→ 全局编码设为UTF-8Default encoding for properties files也设为UTF-8右键 JSP 文件 →Reload project file as UTF-85.3 现象登录成功后session.setAttribute(user, user)但在其他页面session.getAttribute(user)为 null原因浏览器禁用了 Cookie而 Tomcat 默认用 Cookie 存 session ID或跨域请求如前端用 Vue 调用后端Cookie 未携带web.xml中未配置session-config导致 session 超时过短默认 30 分钟测试时超时解决开发阶段强制用 URL 重写不依赖 Cookie在web.xml中加session-config session-timeout60/session-timeout !-- 改为 60 分钟 -- tracking-modeURL/tracking-mode !-- 强制 URL 重写 -- /session-config或在 JSP 中启用 URL 重写% response.encodeURL(book-list.jsp) %生成带;jsessionidxxx的链接5.4 现象Tomcat 启动后控制台疯狂刷SEVERE: Servlet.service() for servlet [jsp] threw exception原因JSP 中引用了不存在的 Java 类如% page importcom.book.model.User %但User.java不在src/com/book/model/下或包名写错JSTL 标签库未引入% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %但WEB-INF/lib/缺少jstl-1.2.jar和standard-1.1.2.jar解决检查src/目录结构是否与 import 包名严格一致大小写敏感下载jstl-1.2.jar和standard-1.1.2.jarJSTL 1.2 需这两个 jar放入WEB-INF/lib/注意Tomcat 8.5 自带el-api.jar勿重复引入5.5 现象MySQL 连接报Cant connect to local MySQL server through socket /tmp/mysql.sock原因Linux 下 MySQL 服务未启动或 socket 文件路径不是/tmp/mysql.sock如 Ubuntu 默认是/var/run/mysqld/mysqld.sockWindows 下 MySQL 服务名不是MySQL如重装后变成MySQL80解决Linuxsudo systemctl status mysql查服务状态若 socket 路径不同在 JDBC URL 中指定jdbc:mysql://localhost:3306/bookdb?socket/var/run/mysqld/mysqld.sockWindowsservices.msc中确认 MySQL 服务名用net start 服务名启动或改用127.0.0.1替代localhost绕过 socket 连接jdbc:mysql://127.0.0.1:3306/bookdb6. 部署与优化从本地运行到 Linux 服务器上线一个 war 包搞定6.1 打包 WARIDEA 一键生成拒绝手动复制粘贴很多人把web/目录整个复制到 Tomcat 的webapps/下结果因缺少WEB-INF/classes/编译后的 class 文件而 404。正确打包流程Build→Build Artifacts→book-system:war exploded→Build先构建 exploded 版本再Build→Build Artifacts→book-system:war→Build生成book-system.war生成的 war 包位于out/artifacts/book-system_war/大小约 8–12MB含所有 jar 和 class提示WAR 包本质是 zip可用jar -tf book-system.war | head -20查看内部结构确认WEB-INF/classes/和WEB-INF/lib/存在。6.2 Linux 服务器部署systemd 管理 Tomcat开机自启不掉链CentOS/RHEL 8 或 Ubuntu 20.04 推荐用 systemd 替代startup.sh创建/etc/systemd/system/tomcat.service[Unit] DescriptionApache Tomcat Web Application Container Afternetwork.target [Service] Typeforking EnvironmentJAVA_HOME/usr/lib/jvm/java-1.8.0-openjdk EnvironmentCATALINA_HOME/opt/tomcat EnvironmentCATALINA_BASE/opt/tomcat ExecStart/opt/tomcat/bin/startup.sh ExecStop/opt/tomcat/bin/shutdown.sh Restartalways RestartSec10 Usertomcat Grouptomcat UMask0007 Restarton-failure [Install] WantedBymulti-user.target启用服务sudo systemctl daemon-reload sudo systemctl enable tomcat sudo systemctl start tomcat sudo systemctl status tomcat # 查看是否 active (running)部署 WAR将book-system.war上传到/opt/tomcat/webapps/Tomcat 会自动解压并部署访问http://your-server-ip:8080/book-system6.3 生产环境加固3 个必须做的最小化配置关闭 Tomcat 管理界面删除webapps/manager/和webapps/host-manager/目录或注释conf/tomcat-users.xml中的 manager-gui 角色防止未授权访问设置 JVM 参数防内存溢出修改bin/setenv.shLinux或bin/setenv.batWindowsexport JAVA_OPTS-Xms512m -Xmx1024m -XX:MetaspaceSize128m -XX:MaxMetaspaceSize256m用 Nginx 做反向代理非必须但强烈推荐server { listen 80; server_name book.yourdomain.com; location / { proxy_pass http://127.0.0.1:8080/book-system/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这样用户访问http://book.yourdomain.comNginx 转发到 Tomcat隐藏端口和路径且可加 HTTPS、限流。6.4 毕设答辩加分技巧一个可演示的“数据看板”JSP答辩时光有 CRUD 太单薄。加一个dashboard.jsp用原生 JS AJAX 拉取统计数30 行代码提升专业感% page contentTypetext/html;charsetUTF-8 languagejava % html head title图书管理看板/title /head body h2系统概览/h2 div idstats p总图书数span idtotalBooks--/span/p p总用户数span idtotalUsers--/span/p p今日借阅span idtodayBorrows--/span/p /div script // 用 fetch 拉取统计数据后端提供 /DashboardServlet 返回 JSON fetch(% request.getContextPath() %/dashboard) .then(res res.json()) .then(data { document.getElementById(totalBooks).textContent data.totalBooks; document.getElementById(totalUsers).textContent data.totalUsers; document.getElementById(todayBorrows).textContent data.todayBorrows; }); /script /body /html后端DashboardServlet.javaprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { MapString, Object stats new HashMap(); stats.put(totalBooks, new BookDAO().countAll()); stats.put(totalUsers, new UserDAO().countAll()); stats.put(todayBorrows, new BorrowDAO().countToday()); response.setContentType(application/json;charsetUTF-8); response.getWriter().write(new ObjectMapper().writeValueAsString(stats)); }我带过的 12 届毕设学生90% 的答辩老师看到这个动态看板会主动问“这是怎么实现的”顺势展示你对前后端分离的理解——哪怕只是用 fetch 拉 JSON也比纯 JSP 强。希望帮到你。本文还有配套的精品资源点击获取