
简介一份完整的JavaWeb图书管理系统源码包适用于课程设计、期末大作业参考也适合Java初学者对照学习Web开发全流程。项目以Java为后端语言结合JSP、JavaScript及Bootstrap、Layui等前端框架实现图书查询、借阅、归还等核心功能代码注释清晰可直接运行并预留了二次开发空间。压缩包共278个文件大小11.67MB核心包含45个Java源码、19个JSP页面、26个JavaScript与9个CSS样式文件另有15个JAR依赖库、46个数据库备份文件及配套文档覆盖建库脚本、页面展示到业务逻辑的完整链路。已有73人学习下载文档说明中详细梳理了数据库结构、模块划分与接口定义有助于理解分层开发思路既可作为高分手写项目的参考模板也能在此基础上扩展预约、统计等新功能。1. JavaWeb 图书管理系统源码包先弄明白这套东西值不值得你花时间我调过好几次JavaWeb 图书管理系统源码数据库文档说明这种打包好的课程设计项目最常见的结果是代码能看懂一半数据库脚本导入就报错文档里写的表结构和 SQL 对不上最后卡在登录页面前面。反直觉的是这类项目真正难的不是 Java 代码而是把源码、数据库、文档三样东西对齐。这篇文章想讲清楚的就是拿到这样一个交付包怎么用最少的时间把它跑起来再判断它的代码质量能不能支撑你后续加功能。适合刚学完 JSPServlet 正愁课程设计没思路的同学也适合接手别人遗留代码但第一次碰这类小系统的开发者。你会得到一套从导入到验证的完整动作以及几个我踩过的坑。2. JavaWeb 图书管理系统的技术选型与四张核心表设计2.1 为什么 JSP Servlet JDBC MySQL 仍是这类项目最常见的组合标题里写的是JavaWeb这个词在不同人嘴里含义不一样。有人拿它指 JSPServlet 的纯 JavaWeb 阶段有人拿它泛指所有 Java Web 项目。我做这类图书管理系统时一般先看配套文档里有没有 Spring、SpringMVC、MyBatis 的痕迹。没有框架痕迹就用回最经典的 JSP Servlet JDBC MySQL 四件套原因很实际它符合课程设计阶段的教学大纲答辩时你能把一次 HTTP 请求从浏览器到 Servlet、再通过 JDBC 到 MySQL 的全过程讲明白这是加分项同时这个组合不依赖复杂的依赖管理哪怕人工把 jar 包拖进 WEB-INF/lib 也能跑。从另一个角度说选这个组合也是在降低环境门槛。图书管理系统的核心就是增删改查Servlet 里写 JDBC 虽然啰嗦但每一行代码的执行路径非常清晰新人跟着断点就能理解三层结构。我不建议一上来就套 Spring Boot除非文档明确写了。我接过一个所谓JavaWeb图书管理系统打开发现是 Spring Boot 2.7 的老版本项目依赖冲突折腾了两小时这对练手来说完全没有意义。2.2 图书管理系统的四张核心表admin、book、reader、borrow数据库设计是整个项目的地基。不管源码里 Service 层怎么包装最终落库的通常就是这几张表管理员表admin、图书表book、读者表reader、借阅表borrow。有的项目会把管理员和读者合并成 user 表加角色字段也有的把归还记录单独拆成 return_book 表但核心结构逃不出这四件套。我通常先建库再建表字符集统一用 utf8mb4避免后面中文乱码。下面这份 SQL 是最常用的最小版本能覆盖登录、图书列表、借书、还书四条主流程CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4; USE library; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 密码明文或MD5, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT管理员表; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) UNIQUE COMMENT ISBN编号, name VARCHAR(100) NOT NULL COMMENT 书名, author VARCHAR(50) COMMENT 作者, publisher VARCHAR(50) COMMENT 出版社, total INT DEFAULT 1 COMMENT 总库存, remain INT DEFAULT 1 COMMENT 当前可借数量 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(30) NOT NULL COMMENT 读者姓名, card_no VARCHAR(20) UNIQUE COMMENT 借书证号, phone VARCHAR(20) COMMENT 联系电话 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT读者表; CREATE TABLE borrow ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 借出时间, due_time DATETIME COMMENT 应还时间, return_time DATETIME DEFAULT NULL COMMENT 实际归还时间, status TINYINT DEFAULT 0 COMMENT 0未归还 1已归还, KEY idx_book_id (book_id), KEY idx_reader_id (reader_id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book (id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅表; INSERT INTO admin (username, password) VALUES (admin, e10adc3949ba59abbe56e057f20f883e); INSERT INTO book (isbn, name, author, publisher, total, remain) VALUES (9787111213826, Java编程思想, Bruce Eckel, 机械工业出版社, 5, 5), (9787115428028, JavaWeb开发实战经典, 李兴华, 清华大学出版社, 3, 2); INSERT INTO reader (name, card_no, phone) VALUES (张三, R001, 13800001111);这段脚本里有一个细节值得注意我故意没有把 password 存成明文而是放了e10adc3949ba59abbe56e057f20f883e也就是 123456 的 MD5 值。很多课程设计源码里直接明文存密码演示没毛病但只要文档里写了密码字段建议加密你就知道对方是有意识在做规范。2.3 借阅关系表如何承载借书—还书—超期三个状态图书管理系统里最容易设计错的是 borrow 表。新手常犯的错是加一个is_return字段还书时改成 true但丢掉了借出时间和应还时间导致超期查询只能靠猜。我这个版本里用borrow_time、due_time、return_time三个 DATETIME 字段加一个 status 小整数就能覆盖常见业务场景。借书时写入book_id、reader_id、borrow_time把due_time设为当前时间加 30 天status 保持 0。还书时把return_time设为当前时间status 改为 1。超期判断不用额外字段SQL 里WHERE return_time due_time就是超期记录。这套设计还有一个好处想统计某本书的借阅次数直接 count 这张表不需要在 book 表里维护一个被借过多少次的冗余计数。状态字段我建议用 TINYINT 而不是 VARCHAR存 0/1 比存 未归还/已归还 更省空间也避免字符串比较时因大小写或空格踩坑。不过要注意如果你接手项目的源码里 borrow 表用的是字符串状态比如borrowed和returned那说明作者考虑了可读性也不算错只是你在写 SQL 时要按源码里的实际值来不要照搬我这套数字约定。3. 从源码包到跑通IDEA 导入、三层结构落地与核心代码3.1 拿到源码包先核对这几件事别急着点运行很多人拿到压缩包直接解压、双击 IDEA、Open 然后就 Run结果报一整屏错误。我一般会先花五分钟核对三样东西工程目录是不是标准 Web 结构src web/WEB-INF/web.xmllib 目录里有没有 MySQL 驱动 jar数据库脚本和文档里的表结构描述是否一致。这一步看似玄学实际上能过滤掉一多半翻车现场。标准 JavaWeb 工程在 IDEA 里导入时先选 Import Project 还是 Open 有讲究。如果你打开的是一个 pom.xml就用 Maven 导入如果只有 .iml 或 .idea就直接 Open。对于最常见的非 Maven 旧工程我习惯新建一个空项目把 src 和 web 目录拷进去再配置 Tomcat这类工程跑通的关键不是源码而是 Artifact 配置。注意看 IDEA 的 Project StructureCtrlShiftAltS里有没有把 web/WEB-INF/lib 下的 jar 包加到 Artifact 的 WEB-INF/lib 里漏了这一步启动 Tomcat 后一访问 Servlet 就报 ClassNotFoundException。3.2 数据库连接工具类JDBC 参数的四个必配项工程里通常能找到一个叫DBUtil.java或JdbcUtil.java的类它是整个系统访问数据库的入口。这个类一般不直接 new而是用一个静态方法返回Connection。我见过太多人把连接串里的参数删得只剩地址结果 SSL 握手失败或中文乱码。下面这个是我在 MySQL 8 常见驱动版本下用着最稳的最小写法package util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class JdbcUtil { private static final String URL jdbc:mysql://localhost:3306/library ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, java.sql.Statement stmt, java.sql.ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }我这里把驱动类写成了com.mysql.cj.jdbc.Driver这是 MySQL 8 版本驱动的要求。如果你拿到的 jar 是 mysql-connector-java 5.1.x就必须改回com.mysql.jdbc.Driver写反了加载阶段就报错。四个必配参数集中在 URL 里useUnicodetrue和characterEncodingutf8管中文读写useSSLfalse避免本机连数据库时做无意义的 SSL 握手serverTimezoneAsia/Shanghai解决 8.x 驱动要求指定时区的问题。USER 和 PASSWORD 要和你本地 MySQL 的实际账号一致最容易被忽略的是 MySQL 8 默认用的 caching_sha2_password 认证老驱动连不上新驱动才行。3.3 登录、图书列表、借还书的 Service/Dao 关键代码典型的三层结构里Servlet 只做参数接收和页面跳转业务逻辑在 Service数据库操作在 Dao。很多课程设计省掉 Service直接在 Servlet 里写 Dao 调用也不是不能跑但答辩时你对三层结构的描述会打折扣。我按规范一点的写法给你拆开。登录是最常见的入口下面是 LoginServlet 的 doPostWebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); if (username null || username.trim().isEmpty() || password null || password.trim().isEmpty()) { response.sendRedirect(login.jsp?error1); return; } AdminDao dao new AdminDao(); boolean ok dao.login(username, password); if (ok) { request.getSession().setAttribute(loginUser, username); response.sendRedirect(bookList); } else { response.sendRedirect(login.jsp?error2); } } }注意这里我在取参数前先调用了request.setCharacterEncoding(UTF-8)这行代码对 POST 请求的中文参数至关重要漏掉它表单里输入张三到 Servlet 里就可能是乱码。但这个方法对 GET 请求的 QueryString 无效GET 方式的乱码需要去 Tomcat 的 server.xml 里给 Connector 加URIEncodingUTF-8或者用new String(username.getBytes(ISO-8859-1), UTF-8)这种补救手法。借书操作是图书管理系统里最值得写好的方法因为它涉及 book 表和 borrow 表两处变更必须放到一个数据库事务里public boolean borrowBook(int bookId, int readerId) { String updateSql UPDATE book SET remain remain - 1 WHERE id ? AND remain 0; String insertSql INSERT INTO borrow (book_id, reader_id, borrow_time, due_time, status) VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), 0); Connection conn null; PreparedStatement ps1 null; PreparedStatement ps2 null; try { conn JdbcUtil.getConnection(); conn.setAutoCommit(false); ps1 conn.prepareStatement(updateSql); ps1.setInt(1, bookId); int rows ps1.executeUpdate(); if (rows ! 1) { conn.rollback(); return false; // 库存不足或不存在 } ps2 conn.prepareStatement(insertSql); ps2.setInt(1, bookId); ps2.setInt(2, readerId); ps2.executeUpdate(); conn.commit(); return true; } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } return false; } finally { JdbcUtil.close(conn, ps1, null); JdbcUtil.close(null, ps2, null); } }这段代码里最有价值的不是 SQL 本身而是setAutoCommit(false)和rollback()。第一句 UPDATE 用remain 0作为条件保证不会把库存扣成负数UPDATE 影响行数是 1 才继续否则回滚。如果不做事务可能出现库存扣了但借阅记录没插入的数据不一致这类 bug 在演示时很难触发一旦并发量上来就会暴露。另外我把还书操作设计成对称的UPDATE book SET remain remain 1加更新 borrow 表 status同样包在事务里。3.4 web.xml 与 JSP 页面乱码和路径在哪个环节埋雷非注解的旧项目依赖 web.xml新版 Servlet 3.0 可以用注解省掉一部分配置。如果源码包里的 Servlet 既没有WebServletweb.xml 里也没有对应的servlet-mapping那你的访问路径一定 404。常见的匹配方式是/login、/bookList、/borrowBook这种简洁路径也有老项目写成*.do如/login.do。这里埋的雷是JSP 表单里的action写的是什么web.xml 里注册的 url-pattern 必须严格对应差一个斜杠都完蛋。字符编码过滤器的配置也值得检查很多项目的 web.xml 里有这样一个 Filterfilter filter-nameencodingFilter/filter-name filter-classorg.apache.catalina.filters.SetCharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingJSP 页面头部则要保证 pageEncoding 和 contentType 都是 UTF-8% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%。我排查乱码的顺序固定是数据库表字符集 → JDBC URL 参数 → JSP pageEncoding → 请求编码。这个顺序基本不会漏。至于页面路径问题JSP 里引用 CSS、JS 或另一个页面时我建议全部写成绝对路径比如${pageContext.request.contextPath}/css/style.css不要用相对路径否则目录层级一变就找不到资源。4. 数据库导入与文档说明把 SQL 装进 MySQL 并核对数据字典4.1 MySQL 8 与 MySQL 5.7 的导入差异数据库脚本是源码 数据库 文档这个交付包里最容易出现兼容性问题的一环。最常见的现象是你在 MySQL 5.7 上写好的脚本拿到 MySQL 8 上跑报SQL syntax或者Incorrect table definition。原因多半出在 timestamp 默认值、排序规则和用户认证三个地方。MySQL 8 默认字符集已经是 utf8mb4排序规则是 utf8mb4_0900_ai_ci而 5.7 默认是 utf8mb4_general_ci。脚本里如果写了DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci在 8.0 上也能正常执行怕的是反过来脚本里没写 COLLATE但某些字段的默认排序规则不一致导致 JOIN 时报告 collation mismatch。遇到这种情况我的处理习惯是打开 SQL 脚本检查每个表的 ENGINE 和 CHARSET 是否齐全再把表名前后的反引号保留即可不要自己乱加。用户认证差异是这个项目的另一道坎。MySQL 8 的 root 默认用caching_sha2_password老驱动 5.1.x 根本连不上如果你的源码里用的还是老驱动要么升级 mysql-connector-java 到 8.0.x要么在 MySQL 里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456;把认证方式改回去。课程设计阶段我建议改驱动参数不要改服务器认证因为那是临时的换一台电脑又会踩一遍。4.2 三种导入数据库脚本的方式导入脚本考验的不是你会不会用图形界面而是你能不能看懂报错。下面这几种方式按我推荐程度从高到低排第一种MySQL 命令行直接导入。先确认 MySQL 的 bin 目录在 PATH 里然后执行mysql -u root -p123456 -e CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4; mysql -u root -p123456 library db_library.sql命令行导入的好处是报错信息完整哪一行语法错误会直接打在屏幕上。注意-p后面不要留空格否则 MySQL 会把空格当成密码的一部分本地密码如果含特殊字符建议写成-p不带密码回车后再交互输入。第二种MySQL Workbench / Navicat / IDEA Database 面板等图形工具。左侧右键选择 Run SQL Script...选文件即可。这种方式的坑是工具可能默认把DELIMITER处理得很奇怪如果你的脚本里有存储过程就得手动检查。IDEA 自带的 Database 面板导入时还要看连接 URL 里有没有设置allowMultiQueriestrue否则多条语句一次执行不过去。第三种运行时由程序执行建表语句。有些 JavaWeb 课程设计确实在项目启动时自动建表底层是CREATE TABLE IF NOT EXISTS。这种设计对使用者最友好但面试官通常不认可因为它掩盖了数据库设计应该由 DBA 主导这一基本事实。我建议你拿到源码后还是手动导一遍脚本这样你能真正掌握库结构而不是靠程序猜。4.3 文档说明里最该看的三部分一个完整的 JavaWeb 图书管理系统交付包文档说明不外乎三类内容运行环境说明、数据库设计说明、操作手册。这三部分的用法完全不同很多人只看操作手册忽略了前两者结果连默认账号都找不到。运行环境说明里最要紧的是 JDK 版本、Tomcat 版本、MySQL 版本三者的匹配关系。JDK 8 Tomcat 8.5 MySQL 5.7 是最常见的黄金组合比它新的组合比如 JDK 17 Tomcat 10会带来跨版本问题Tomcat 10 把javax.servlet换成了jakarta.servlet老源码完全跑不起来。数据库设计说明里通常包含数据字典和 E-R 图你要做的是把数据字典和 SQL 脚本里的字段类型逐一核对重点看 varchar 长度、datetime 是否允许 NULL、status 字段的注释。操作手册部分一般写着管理员账号 admin密码 admin但你刚导完的数据库里admin 的密码 MD5 值可能是 123456 对应的也可能是 111111 对应的直接登录会发现密码正确却进不去这时就要手动 UPDATE admin 表把密码改成文档里的值。文档和源码不一致是家常便饭。我处理过一套系统文档里写 borrow 表有book_name冗余字段SQL 脚本里却没有导致 Dao 层SELECT book_name直接报列不存在。这种问题没人能替你绕开只能靠脚本和实体类拿着放大镜对。你可以用下面这条 SQL 快速检查表结构是否存在DESC borrow;再和实体类里的字段名对照所有不一致先记下来再决定改哪边。5. 图书管理系统跑不起来的常见坑现象、原因、解决5.1 中文乱码从登录开始出现现象浏览器里正常显示中文但登录后从数据库查出来的书名、作者全是问号或者表单填入的中文提交后变成乱码。原因数据库连接 URL 里没带characterEncodingutf8或者请求进来时没有统一设置 UTF-8 编码。很多源码只在 Dao 层写了查库没在 Servlet 层处理请求参数编码导致入库的是中文查出来是乱码和提交时就是乱码两种症状混在一起。解决先按三层补全再验证——JSP 页面头部加pageEncodingUTF-8Servlet doPost 开头加request.setCharacterEncoding(UTF-8)JDBC URL 增加useUnicodetruecharacterEncodingutf8最后确认表、库、连接三处字符集统一。查一个库的当前默认字符集可用SHOW VARIABLES LIKE character_set%;确认character_set_server是 utf8mb4 即可。注意浏览器的查看编码也要设置 UTF-8有些浏览器默认 GBK 会把输出显示错乱但那是显示层不是数据层别搞混。5.2 ClassNotFoundException: com.mysql.jdbc.Driver现象Tomcat 启动正常一点按钮访问 Servlet后台报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因MySQL 驱动 jar 没被正确打包进 WEB-INF/lib。IDEA 的坑在于jar 明明在 Project Structure 的 Libraries 里但在运行时 Artifact 里没有对应的输出Tomcat 根本找不到它。这个问题的本质是编译期有依赖运行期没有依赖。解决打开 Project StructureCtrlShiftAltS选 Artifacts在输出目录的 WEB-INF/lib 下确认有 mysql-connector-java 的 jar 文件没有就点加号从 Library 里添加然后重新 build。用 Maven 的同学在 pom.xml 里确认mysql-connector-java的 scope 不是 provided并执行 mvn clean package把 war 里 lib 目录列出来。这个坑我见过至少三次都是 Artifact 配置漏了。另一个伴随症状是驱动类名写错老驱动用com.mysql.jdbc.Driver新驱动必须用com.mysql.cj.jdbc.DriverClass.forName 写错同样报 ClassNotFoundException所以要在日志里分清楚是类不存在还是类名拼错。5.3 数据库连接时报 SSL 或时区错误现象数据库导入没问题但程序启动或第一次执行查询时控制台报The server time zone value ... is unrecognized或SSL connection error。原因MySQL 8 的驱动要求连接串里明确指定 serverTimezone否则用 JVM 默认时区可能和服务器时区不一致同时 MySQL 8 服务端默认开启 SSL老连接串没加useSSLfalse时驱动尝试做 SSL 握手在某些本地环境就超时或失败。这两个错误通常一起出现但报错信息里会分开提示。解决回到 JdbcUtil 的 URL按这个顺序补齐参数jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai。如果你在 MySQL 命令行工具里连接正常但 Java 连不上十有八九就是 URL 参数问题。还有一个边界情况时区参数写Asia/Shanghai需要驱动版本较新老驱动不认识这个区域名可以改用serverTimezoneGMT%2B8或数据库直接不传时区由 JDBC 读取服务端配置但这依赖 MySQL 自己设置的系统时区是正确的。5.4 borrow 表字段对不上插入数据报错现象程序启动好登录成功进入借书页面一提交就报Unknown column xxx in field list或者Column count doesnt match value count。原因源码里的实体类、JSP 表单、数据库表三者字段名不一致。典型情况是源码里用borrowDateSQL 脚本里叫borrow_timeMyBatis 或手写映射时没有做别名处理还有的是 Dao 层的 INSERT 语句写了 5 个字段而表实际有 6 个非 NULL 字段缺一个就没法插入。解决用DESC borrow;把表结构打出来再打开 Dao 层相关的 INSERT/UPDATE SQL 对照。一般我会写一个查询测试提前验证SELECT * FROM borrow LIMIT 1;如果这条能过但程序报错说明映射层有字段名拼写问题。对于 JSP 表单和实体类不一致的情况重点检查id是不是自增字段如果 INSERT 语句里手动写了 id 且传了 0 或空MySQL 也会报错。处理办法是调试时把 Dao 层 SQL 打印到控制台先用 System.out 输出再跑一遍借书看得一清二楚。5.5 改了 JSP 不生效浏览器里永远是旧页面现象修改了 bookList.jsp 的书名列刷新浏览器还是旧内容清缓存也没用。原因IDEA 或 Eclipse 部署到 Tomcat 时修改的 JSP 没有被重新编译部署target/classes 或编译输出目录里还是旧文件。旧版 IDEA 的习惯是 Update resources只有 Update classes and resources 才会把 JSP 推送到 Tomcat 的 webapps 目录这个按钮在 Run 面板的绿色锤子附近。还有一种可能是你改了 webapp 下的源文件但 Tomcat 部署的是另一个副本目录。解决改用 Debug 模式改完 JSP 后点 Update resources如果不行就彻底 clean Tomcat 的 work 目录再重启。我总是建立一个检查习惯改动代码后看一眼 Tomcat 日志里有没有出现Reloading Context或Deploying字样没有就说明文件根本没推送过去。另外浏览器 304 缓存也会造成旧页面JSP 页面放在 WEB-INF 下再通过 Servlet 转发访问可以避开一部分直接访问时的缓存问题但这是写法问题不是部署问题的重点。6. 跑通之后先做并发验证借书操作有没有把关再加一点改造成长建议跑通只是起点真正能看出源码水平的是借书并发验证。做法很简单把一本库存只有 1 的图书开两个浏览器标签页几乎同时点借阅。如果系统里事务写得规范第二个请求查remain 0时发现已经是 0会提示库存不足如果没写事务或UPDATE语句没有条件判断你会发现图书库存变成负数或者借阅表出现两条重复记录。更严谨的验证可以在 Dao 的 borrowBook 方法里临时加一行Thread.sleep(3000)模拟慢查询制造人为并发窗口再两个请求同时往里打。看到负库存的瞬间比任何答辩提问都更能说明问题。验证通过后如果还想让这个项目在简历上更有说服力我会建议你做一个低成本的改造把当前的登录校验从写死在 Servlet 里抽成一个简单的 Filter统一做未登录拦截。这个改动不碰业务代码却能让项目的结构从能达到课程设计标准提升到接近生产项目的层次。另一个很常见的成长路线是逐步用 SpringMVC 替代 Servlet 的请求路由把WebServlet(/bookList)换成RequestMapping(/bookList)方法签名和返回逻辑基本能平移这样你等于用自己熟悉的功能完成从 JavaWeb 旧时代到主流框架的过渡比背框架文档有效得多。我当年做图书管理系统时就是靠这两步改造把一段普通的 JSPServlet 代码讲出了分层设计和并发防护的意识那门课答辩后还被老师单独留下聊了十分钟。做一个项目最怕的是跑通就结束你多验证一步并发多拆一层结构收获会完全不同。希望帮到你。本文还有配套的精品资源点击获取