简介:基于JavaWeb的图书管理系统完整源码与配套文档,面向毕业设计、期末大作业与课程设计场景,尤其适合JavaWeb初学者参考和学习。项目为个人手打高分(98分)作品,代码含详细注释,结构清晰,新手也能快速理解并部署运行。资源共352个文件,压缩包约64.79MB,主要包含37个Java源文件、91个class编译文件、91个依赖jar包、14个HTML页面、13个CSS样式、12个JS脚本,以及5个SQL数据库脚本、1份docx文档说明,前端页面、后端逻辑、数据库脚本与部署组件一应俱全。已有544人学习下载。除完整可运行的图书管理功能(书籍信息管理、用户信息管理、Excel导入导出等)外,还附带实践文档与数据库初始化脚本,可直接作为高分毕设、期末大作业提交,也便于二次开发时快速理解业务逻辑与扩展功能。
1. JavaWeb 图书管理系统:为什么「被做烂的题」反而最能拉分差
图书馆里那类「扫码借书、逾期催还」的后台,在企业里叫图书管理系统,在课程设计里则是一道标准 JavaWeb 综合题:用户登录、图书检索、借书还书、超期统计,核心业务就这几件事。正因为它被做烂了,反而成了检验 JavaWeb 基本功最好的试金石——真正拉开分差的从来不是功能多少,而是分层清不清楚、事务有没有管住、文档能不能自圆其说。下面这套内容要解决的就是这个问题:从技术栈选择到建表,从核心代码到排错,最后落回文档和答辩,让你手里那份图书管理系统源码加文档说明既跑得起来,也说得出口。适合正在赶课程设计、准备毕业设计,或者想借一个完整的 javaweb 项目把 Servlet、JSP、MySQL 串成闭环的人。
2. 技术选型与建表 SQL:Servlet+JSP+MySQL 比 Spring Boot 更合适吗
2.1 为什么不直接上 Spring Boot?课程设计场景下的选型逻辑
你打开搜索框,热门词里几乎全是 Spring Boot 图书管理系统,但我要先泼一盆冷水:如果你的目标是「高分项目」,Servlet+JSP+MySQL 这套老组合反而更占便宜。原因很直白——Spring Boot 把请求流转、依赖注入、事务管理都封装成了黑匣子,你写三行注解就能跑,答辩老师问一句「一个请求从浏览器到数据库经历了哪些类、哪些方法」,你很难答出细节。用原生 Servlet 写,每一个环节都摆在明面上:请求进了哪个 Servlet、service 层做了什么校验、DAO 里那条 SQL 是怎么拼的,全部能指着代码讲,这正好是 javaweb 课程设计的核心考核点。
但这不代表 Spring Boot 一无是处。我一般会这样权衡:如果项目要求里明确写了「采用 Spring Boot + MyBatis」,那就顺着要求做;如果只写了「基于 JavaWeb」,我优先选 Servlet+JSP,把选型理由写进文档说明书,这本身就是个加分论点。两者的对比可以做成一个表格放进文档:
| 对比项 | Servlet+JSP+JDBC | Spring Boot+MyBatis |
|---|---|---|
| 请求流转可见度 | 每行代码都能指给老师看 | 注解和代理把流程藏了一半 |
| 搭环境成本 | Tomcat+MySQL,十分钟能跑 | Maven 依赖下载就够折腾半天 |
| 答辩追问风险 | 低,问题都在自己代码里 | 高,容易被问 starter 原理 |
| 真实企业匹配度 | 偏老,但很多遗留系统仍在用 | 主流,跳槽简历更好看 |
表格不是让你照抄,而是建议你把这个对比写进自己的文档说明,向老师传递一个信号:这个题我做过技术选型评估,不是只会复制粘贴。选型这件事没有绝对的对错,关键是你的理由能不能在答辩现场站住脚。
2.2 四张核心表的设计:把「能跑」落到建表 SQL
图书管理系统的表设计有套路,但多数人照着别人的源码抄表结构,抄完也说不清为什么这么建。我这里给出一份自己常用的最小表结构:用户表、图书表、借阅记录表、分类表。四张表就能覆盖登录、查询、借阅、统计全部业务,也方便扩展。
-- 用户表:区分管理员和普通读者 CREATE TABLE `t_user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL COMMENT '登录名', `password` VARCHAR(64) NOT NULL COMMENT 'MD5 密文', `real_name` VARCHAR(50) DEFAULT NULL COMMENT '真实姓名', `role` TINYINT NOT NULL DEFAULT 1 COMMENT '1-读者 0-管理员', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这段建表 SQL 里有两个容易被忽略的细节。一是password字段长度定了 64,因为我们存的是 MD5 的小写在十六进制,正好 32 位,为什么用 64?为了兼容 SHA-256,后面想升级加密算法不用改表。二是role用 TINYINT 而不是 VARCHAR 存字符串,省空间且判断快,页面要显示「管理员/读者」时在代码里做映射,不要直接在数据库里存中文。
-- 图书表:库存与总量分开设计 CREATE TABLE `t_book` ( `id` INT NOT NULL AUTO_INCREMENT, `isbn` VARCHAR(20) NOT NULL COMMENT '国际标准书号', `title` VARCHAR(200) NOT NULL COMMENT '书名', `author` VARCHAR(100) DEFAULT NULL, `publisher` VARCHAR(100) DEFAULT NULL, `stock` INT NOT NULL DEFAULT 1 COMMENT '当前可借库存', `total` INT NOT NULL DEFAULT 1 COMMENT '馆藏总量', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_isbn` (`isbn`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;stock和total分开存,是我踩过坑之后总结出来的教训。曾经见过有人只存一个库存字段,还书时加一、借书时减一,看着没问题,但图书破旧下架时要统计「这个月流失了多少本书」就完全无据可查。分两列之后,下架图书只改 total,stock 用来做借阅可用性的判断,统计口径清晰,答辩时也更有话讲。图书表用isbn做了唯一键,这是图书领域天然的商业主键,比自增 id 更能防止重复录入。
-- 借阅记录表:业务的核心战场 CREATE TABLE `t_borrow` ( `id` INT NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL COMMENT '借阅人', `book_id` INT NOT NULL COMMENT '图书', `borrow_time` DATETIME NOT NULL COMMENT '借出时间', `due_time` DATETIME NOT NULL COMMENT '应还时间', `return_time` DATETIME DEFAULT NULL COMMENT '实际归还时间', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0-借出 1-已还 2-逾期', PRIMARY KEY (`id`), KEY `idx_user` (`user_id`), KEY `idx_book` (`book_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这条建表 SQL 的status字段值得多说几句。常见做法是借阅记录里只存借出时间和归还时间,是否逾期靠查询时用当前时间去比 due_time 现场算,这当然能算出来,但每次列表页面都要做一次计算,数据量大了 SQL 写起来很别扭。我倾向于在归还时或定时任务里把逾期状态落库,查询时直接 where status=2,性能好,也方便文档里写「逾期统计模块」。borrow_time和due_time都用 DATETIME 而不用 TIMESTAMP,因为 TIMESTAMP 有 2038 年的上限,课程设计虽然不会跑那么久,但这个理由写进文档很加分。
2.3 三层架构的工程边界:Controller、Service、DAO 各管哪一摊
很多源码包看起来乱,根因是三层的职责边界没划清楚,全部逻辑堆在 JSP 或 Servlet 里,一个方法几百行,老师看着就头疼。我在工程结构上一般坚持三条规则:Servlet 只做参数接收、类型转换和页面跳转,所有业务判断都进 Service;DAO 只负责 SQL 执行和结果集映射,不写 if-else 业务逻辑;实体类只做字段存取,不挂任何业务方法。这样每一层都能单独写单元测试,虽然不是课程设计的硬要求,但答辩时说到「我这段代码可测试性好」,会明显比同龄人高一个段位。
落到包结构上的话,实体类放entity,数据库访问放dao,业务逻辑放service,前端控制放controller,工具类放util,配置信息放resources。后面第四章会给完整的目录树,这里先记住一个原则:让别人通过目录就能猜到某个功能在哪,这就是规范的价值。图书管理系统这种 CRUD 项目最忌讳的就是把代码写成一个大 Servlet 包打天下,看着功能全跑通了,实际上一点工程价值都没有。
3. 从登录到借阅闭环:三块核心代码与必调的参数
3.1 用户登录:Session 生命周期与 MD5 校验代码
登录是所有 JavaWeb 项目的门面,也是答辩时老师大概率会追问的第一块代码。这里给出一段常用的 LoginServlet 写法,基于 Servlet 3.0 的注解方式,省掉了一部分 web.xml 配置繁琐的映射代码。
@WebServlet("/login") public class LoginServlet extends HttpServlet { private UserService userService = new UserService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); // 裸参数直接拦截,减少数据库无效查询 if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()) { resp.sendRedirect("login.jsp?error=empty"); return; } User user = null; try { // 数据库里存的是 MD5 密文,这里加密后再比对 user = userService.login(username.trim(), DigestUtils.md5Hex(password)); } catch (Exception e) { e.printStackTrace(); resp.sendRedirect("login.jsp?error=db"); return; } if (user == null) { resp.sendRedirect("login.jsp?error=wrong"); return; } // 登录成功:把用户对象挂到 session,后续页面靠它做权限判断 HttpSession session = req.getSession(); session.setAttribute("login_user", user); resp.sendRedirect("index.jsp"); } }代码逻辑不复杂,注意三处参数和边界的选择。第一,req.setCharacterEncoding("UTF-8")必须写在读取参数之前,否则 POST 请求中文用户名会乱码,这一行能省很多事。第二,密码不是明文判断,而是先md5Hex再比对,这个DigestUtils是 commons-codec 的工具类,不建议自己手写一个不规范的 MD5 工具。第三,登录失败用sendRedirect跳回登录页并带error=xxx参数,方便页面提示具体错误原因;登录成功则用重定向避免表单重复提交,这里不能用forward,否则刷新页面会再次触发登录请求。
3.2 图书分页查询:Limit 参数背后的三个配置点
图书列表是读者最常用的功能,不夸张地说,大多数系统翻车都翻在分页上:要么每页条数写死在前端,要么 LIMIT 的两个参数顺序搞错。DAO 层常用写法如下:
public List<Book> findPage(String keyword, int pageNum, int pageSize) throws SQLException { String sql = "SELECT id, isbn, title, author, publisher, stock " + "FROM t_book " + "WHERE title LIKE ? OR author LIKE ? " + "ORDER BY id DESC " + "LIMIT ?, ?"; try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { String like = "%" + keyword + "%"; ps.setString(1, like); ps.setString(2, like); ps.setInt(3, (pageNum - 1) * pageSize); // 偏移量从0开始 ps.setInt(4, pageSize); try (ResultSet rs = ps.executeQuery()) { List<Book> list = new ArrayList<>(); while (rs.next()) { Book b = new Book(); b.setId(rs.getInt("id")); b.setIsbn(rs.getString("isbn")); b.setTitle(rs.getString("title")); b.setAuthor(rs.getString("author")); b.setPublisher(rs.getString("publisher")); b.setStock(rs.getInt("stock")); list.add(b); } return list; } } }这段代码里三个需要调的参数你要心里有数。第一个是(pageNum - 1) * pageSize,这是 LIMIT 的偏移量,前端传pageNum=1时算出来是 0,第二页是 10,很多人直接把 pageNum 传给 SQL,结果第二页从第 11 条开始查,丢掉第一条。第二个是LIKE的%拼接,keyword 为空时不能拼出%%去全表扫,应该在 DAO 外层判断空值直接走不带条件的查询。第三个是try-with-resources:Connection、PreparedStatement、ResultSet 三个资源全部自动关闭,必修课,JavaWeb 连接 MySQL 的项目里漏关连接是内存泄漏的重灾区。
3.3 借书与还书:事务边界和日期计算怎么定
借书流程是图书管理系统的核心业务,也是事务最该出场的地方。很多人写出来的是先扣库存、再插借阅记录两条独立的代码,中间任何一条失败都会造成数据不一致,属于典型的高分项目硬伤。我这里贴出借书 Service 的核心方法:
public boolean borrowBook(int userId, int bookId) throws SQLException { String lockSql = "SELECT stock FROM t_book WHERE id = ? FOR UPDATE"; String updateStock = "UPDATE t_book SET stock = stock - 1 WHERE id = ? AND stock > 0"; String insertBorrow = "INSERT INTO t_borrow(user_id, book_id, borrow_time, due_time, status) " + "VALUES(?, ?, NOW(), ?, 0)"; Connection conn = DbUtil.getConnection(); try { conn.setAutoCommit(false); // 事务边界从这里开始 // 1. 行级锁:锁住这本书,避免并发借书超卖 int stock; try (PreparedStatement ps = conn.prepareStatement(lockSql)) { ps.setInt(1, bookId); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) { conn.rollback(); return false; } stock = rs.getInt("stock"); } } if (stock <= 0) { conn.rollback(); return false; } // 2. 扣库存:这里再次检查 stock > 0,双保险 try (PreparedStatement ps = conn.prepareStatement(updateStock)) { ps.setInt(1, bookId); ps.executeUpdate(); } // 3. 插入借阅记录,截止时间取当前时间加 30 天 Calendar cal = Calendar.getInstance(); cal.add(Calendar.DAY_OF_MONTH, 30); Timestamp dueTime = new Timestamp(cal.getTimeInMillis()); try (PreparedStatement ps = conn.prepareStatement(insertBorrow)) { ps.setInt(1, userId); ps.setInt(2, bookId); ps.setTimestamp(3, dueTime); ps.executeUpdate(); } conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { conn.rollback(); // 任何一步失败,整体回滚 throw e; } finally { conn.setAutoCommit(true); DbUtil.close(conn); } }这块代码的亮点全在事务边界上。setAutoCommit(false)和commit/rollback三个方法构成了最朴素的 JDBC 事务,扣库存和插入借阅记录要么一起成功要么一起失败,不存在「书借出去了但记录没插上」这种脏数据。行级锁FOR UPDATE是并发场景下的关键参数,虽然课程设计演示时没人会并发点击,但代码里写了这一行,答辩被问「多人同时借同一本书怎么办」就能讲出门道。日期计算用Calendar加 30 天,比直接字符串拼日期更安全,还书时判断逾期就是拿当前时间和due_time做比较,这个逻辑放在还书的 Service 里,代码风格保持一致。
4. 源码目录与文档说明书:让答辩老师十分钟看懂整个高分项目
4.1 源码工程目录规范:从「能跑」到「好读」
我见过太多课程设计源码包:一个 src 目录塞了三十个 Servlet,JSP 和图片混在一起,没有 README,下载的人不花两小时根本理不清结构。高分项目和低分项目的第一道分水岭就在目录。以下是我常用的 Maven 工程结构,哪怕你的项目没用 Maven,按这个思想组织普通 Web 工程也完全成立。
book-manager/ ├── pom.xml ├── README.md ├── sql/ │ └── book_manager.sql # 建库建表脚本 + 测试数据 ├── src/ │ └── main/ │ ├── java/com/example/bookmanager/ │ │ ├── controller/ # Servlet:接收请求、控制跳转 │ │ ├── service/ # 业务逻辑:登录校验、借阅事务 │ │ ├── dao/ # 数据库访问:SQL 与结果映射 │ │ ├── entity/ # 实体类:User、Book、Borrow、Category │ │ └── util/ # 工具类:DbUtil、MD5 工具 │ ├── resources/ │ │ └── jdbc.properties # 数据库连接配置 │ └── webapp/ │ ├── WEB-INF/ │ │ └── web.xml │ ├── jsp/ # 页面文件 │ │ ├── login.jsp │ │ ├── book_list.jsp │ │ └── borrow_list.jsp │ ├── css/ │ └── index.jsp注意几个容易被忽略的点。sql/目录放建表脚本是很多源码包缺失的,老师最反感「拿到了源代码却建不出数据库」,有完整的 SQL 脚本,项目落地的第一步就顺畅了。jdbc.properties单独放而不是把数据库连接写在 Servlet 里,因为改库名、改密码只需要动一个文件,这是 JavaWeb 连接 MySQL 项目里最基础的解耦。README.md也不要敷衍,写清楚三件事:用什么版本的环境、怎么初始化数据库、从哪里启动入口。
4.2 文档说明书写法:从需求分析到测试用例的目录骨架
源码+文档说明,标题里后半部分才是拉分的关键。文档说明书不是把代码拷进去凑页数,而是讲清楚「为什么这么做」。我常建议的目录骨架如下,给答辩老师递到手上时,他扫目录就能看懂你的工作量:
- 第一章 项目概述:背景、目标、开发环境(JDK 版本、Tomcat 版本、MySQL 版本)
- 第二章 需求分析:功能需求、角色分析(管理员和读者的权限差异)
- 第三章 系统设计:总体架构图(用文字或简单框图)、功能模块划分
- 第四章 数据库设计:ER 图、四张表结构说明、关键索引设计理由
- 第五章 系统实现:每个模块写出核心代码并配运行截图
- 第六章 系统测试:测试用例表格、测试结论
- 第七章 总结与展望:遇到的问题与解决过程、可扩展方向
这里有个技巧:每一章的「系统实现」不要全代码贴,挑最有代表性的一段放进文档,比如登录的 Session 处理、借书的 JDBC 事务,代码要有注释,旁边配截图。答辩老师看文档的时间有限,他要的是「一眼知道你会什么」,而不是「看完整份源码」。数据库设计那一章把 ER 图和前面第四章的表设计结合起来,明确标注外键关系和索引用途,这部分写透了,借阅查询的 SQL 性能问题就不用费口舌解释。
4.3 在 IDEA 里把 javaweb 项目跑起来:部署配置清单
不管源码写得多漂亮,跑不起来就是零分。IDEA 运行 JavaWeb 项目配置踩坑的人特别多,常见的问题是:项目能编译却无法启动 Tomcat、页面报 404、数据库连接报错。我第一次带学生搭这个环境时,几乎每个人都要卡半小时。这里整理一份我在 IDEA 里跑 javaweb 项目完整案例的最小配置清单,按顺序做即可:
- 第一步:确认 JDK 版本。JDK 8 和 Tomcat 8.5 是稳定组合,不要一上来用 JDK 17 配老 Tomcat,模块化问题会让你怀疑人生。
- 第二步:IDEA 里配置 Application Server。进入 Settings → Build, Execution, Deployment → Application Servers,添加本地的 Tomcat 安装目录,注意选的是解压目录而不是 bin 目录。
- 第三步:给 Web 模块加 Artifact。Project Structure → Artifacts,选择 Web Application: Exploded,把依赖的 jar 包加入 WEB-INF/lib。这一步漏掉,运行时会报
ClassNotFoundException,对应的是 MySQL 驱动等依赖缺失。 - 第四步:配置 Run/Debug Configuration。点加号选 Tomcat Server → Local,Deployment 标签页添加刚才的 Artifact,Application context 填
/book-manager,这个名字要和你访问 URL 保持一致。 - 第五步:检查
jdbc.properties。URL 里的数据库名、用户名、密码、时区参数全部确认,MySQL 8 的连接串必须加serverTimezone=Asia/Shanghai。
这套清单里最容易出问题的是第三步和第五步。IDEA 新建的 Web 工程默认不会帮你建 Artifact,很多人代码没问题,运行按钮却是灰的,就是这个环节没做。我一般的验证方式是:先把 Tomcat 单独启动一次,访问 8080 端口看到猫页面,再回 IDEA 启动,这样能把 Tomcat 自身问题排除在外,不混淆故障源。
5. 排错避坑:JavaWeb 连 MySQL 最常见的 5 个翻车现场
5.1 数据库连接失败:时区、SSL 和驱动版本三连坑
现象:启动 Tomcat 后访问页面,控制台报Cannot create PoolableConnectionFactory或者Communications link failure。原因:MySQL 8 以后,驱动版本、时区参数、SSL 安全连接三个点不配合就会连接被拒。JDBC URL 里没写serverTimezone必然报错,SSL 没关在某些云数据库上也会握手失败。解决:用下面这条连接串,适配 MySQL 8 的常见姿势:
jdbc.url=jdbc:mysql://localhost:3306/book_manager?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.username=root jdbc.password=你的密码注意驱动类名从com.mysql.jdbc.Driver换成了com.mysql.cj.jdbc.Driver,这是 MySQL 8 之后的强制要求。如果你用的是 5.x 的驱动连 8.x 的库,驱动包本身不认新协议,照样失败。遇到连接问题先做三件事:pom 或 lib 里确认驱动 jar 版本、用 Navicat 单独测试连接串、把上面的 URL 格式抄过去。90% 的连接问题都在这三点里。
5.2 中文乱码不显示:请求、响应、数据库三层编码
现象:页面上显示「???」或者中文变成空,登录输入中文用户名直接乱码。原因:JSP 页面编码、Servlet 请求参数编码、数据库表编码三层不一致。最常见的是 JSP 页面是 UTF-8,但 Servlet 里没设request.setCharacterEncoding("UTF-8"),POST 参数按照 ISO-8859-1 解码,中文自然碎掉;另一个是建表时用了latin1,改什么都不好使。解决:三层全部统一到 UTF-8。
<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>这是 Tomcat 自带的编码过滤器,放在所有 Servlet 前面,全站请求都过一遍 UTF-8。建表时统一用CHARSET=utf8mb4,JSP 页面头部加<%@ page contentType="text/html;charset=UTF-8" %>。三层都做了,乱码问题基本绝迹。如果还有个别字段乱码,检查是不是数据已经用错误编码写进库了,这种只能清掉重导,没有后悔药。
5.3 借书超卖:库存扣减和借阅记录不在同一个事务
现象:两个人几乎同时点借同一本书,最后库存变成 -1,或者借阅记录多了一条但库存没减。原因:扣库存和插借阅记录是两条独立 SQL,没有包在commit/rollback里。这是图书管理系统源码里最隐蔽的硬伤,正常演示根本发现不了,但代码评审一眼就能挑出来。解决:用第三章给出的模板,把两个操作放到同一个 Connection 的事务里,再加一行SELECT ... FOR UPDATE锁住行记录。这里提醒一句:FOR UPDATE必须在事务内且连接不关闭的前提下生效,很多人把锁写对了,却因为每次查询都新建连接导致锁自动失效,等于白写。
5.4 打开页面就 404/405:找 servlet-mapping 的五个位置
现象:登录页能打开,一提交表单就 404,或者点按钮提示 405 Method Not Allowed。原因:Servlet 的访问路径和表单 action 路径不匹配,或者 doPost/doGet 只实现了一半。排查时按顺序检查五处:表单的action有没有带工程上下文路径、@WebServlet注解路径和 action 是否一致、web.xml里是否还残留了旧的 servlet-mapping 覆盖新路径、Servlet 类是不是重写了service方法却忘了写 doGet、404 时看 IDEA Console 里有没有报ClassNotFound导致 Servlet 根本没加载。405 几乎都是 doGet/doPost 缺失的问题,这类错误报错信息很直白,但新手经常盯着页面代码找半天,忘了看方法名。
5.5 IDEA 导入项目跑不起来:Artifact 和 Tomcat 配置
现象:代码是同学给的,源码也完整,导入 IDEA 后运行按钮灰色,或运行起来是 404。原因:导入的工程没有配置 Web Artifact,也没有添加 Tomcat 运行环境。IDEA 不像 Eclipse 那样自动识别 Web 工程,开放导入后要手动关联。解决:按第四章 4.3 的清单走一遍,重点是 Project Structure 里把webapp目录标记为 Web 资源目录,再新建 Artifact。另一个高频坑是导入后发现 JDK 版本和项目的编译级别不一致,报invalid source release,把 Project Structure 里 Project、Modules、Settings 三处的 JDK 全部统一到同一版本。这类问题不是代码问题,是环境问题,锻炼的是排查环境的能力,遇多了就熟练了。
6. 进阶与收尾:加完这 3 个加分点再交付
6.1 登录拦截器与借阅超期提醒:两个低成本的加分点
书还完了、代码跑通了,这时候不要急着交。有两个小功能性价比极高,其中一个就是登录拦截器。没有它,用户直接在浏览器地址栏敲book_list.jsp就能绕过登录页访问系统,这是课程设计最常见的功能漏洞,也是答辩高频翻车点。加一个 Filter 就能解决:
@WebFilter("/jsp/*") 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(); if (session.getAttribute("login_user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }这个是标准写法,不需要再改参数。另一个加分点是借阅超期提醒,不需要做短信邮件那种重功能,只要在登录后的首页写一段查询:SELECT COUNT(*) FROM t_borrow WHERE user_id=? AND status=2,pending 的数据展示成一行红字「您有 N 本图书已逾期」。这两个功能代码量不大,文档里却能各自占一小节,功能价值不大但展示价值大,能说明你考虑了「用户的真实使用场景」。
6.2 交付前自测:一张清单覆盖「能跑、敢答辩、不怕问」
交付前最后两小时,别急着打包源码,先把这张自测清单走一遍:
| 功能项 | 操作步骤 | 预期结果 |
|---|---|---|
| 登录成功 | 读者账号+正确密码 | 跳转首页,右上角显示姓名 |
| 登录失败 | 错误密码 | 返回登录页并提示「用户名或密码错误」 |
| 未登录访问 | 地址栏直接访问 jsp 页面 | 被拦截跳回登录页 |
| 图书分页 | 第二页 | 显示第 11-20 条,页码高亮且参数正确 |
| 借书 | 可借库存大于 0 的书 | 库存减 1,借阅列表出现新纪录 |
| 借书超卖模拟 | 库存为 1 的书连续借两次 | 第二次提示库存不足,无脏数据 |
| 还书 | 借出状态的书 | 状态变已还,库存加 1 |
| 逾期展示 | 造一条 due_time 小于当前时间的记录 | 首页出现逾期提示 |
这张表里最有价值的是「借书超卖模拟」和「逾期展示」这两行,前者验证事务,后者验证日期计算,正是答辩时最可能被追问的两个技术点。我以前做课程设计栽过跟头:功能全通,老师一问「同一个用户反复点借书会发生什么」,当场手心冒汗。后来所有项目交付前都会刻意造边界数据压一遍,自己先把自己问倒,答辩就从容了。希望这份笔记能帮你把图书管理系统做得比「能跑」再往前走一步,既出得了手,也答得上问——祝顺利。
本文还有配套的精品资源,点击获取