Java+JSP+MySQL学校教材管理系统开发实战:从数据库到Tomcat部署
2026/9/24 18:07:57 网站建设 项目流程

简介:一套基于Java、JSP与MySQL的Web学校教材管理系统源码,面向Java Web初学者、在校生以及需要快速搭建校园内部教材管理后台的开发者,解决学校教材信息线上录入、检索和维护的难题。资源包共81个文件,压缩后约3.91MB,包含26个JSP页面、11个Java类、22个编译后Class文件,以及10个CSS样式、8个XML配置、2个JAR依赖和1个SQL脚本,从数据表结构到前端页面形成完整闭环。已有1229人学习,项目结构紧凑,是课程设计、毕业设计或自学Java Web整合开发的实用参考。项目自带数据库初始化脚本,导入IDE并配置Tomcat即可启动,代码按MVC思路分层,便于理解Servlet、JSP与数据库交互方式;同时涵盖管理员登录认证、教材信息增删改查及库存统计等核心模块,具有较好的扩展性。整包结构清晰、注释规范,有助于快速掌握Java Web项目从建表到部署的全过程。

1. 学校教材管理系统:一套 Java+JSP+MySQL 能跑通的最小 Web 工程

如果你在学校教务处或者教材科待过,一定见过这样的场景——教材入库用 Excel 记,领用登记用纸质本子,期末对账时三个人的表格三个数,谁也说不清库存到底剩多少。Java+JSP+MySQL 实现的学校教材管理系统,就是把这堆事搬到浏览器里:老师登录后查教材、做入库、给学生发书、按班级统计领用记录。它不是什么高并发系统,也不需要微服务,就是典型的 JavaWeb 课程设计选题,恰好覆盖了 Servlet、JSP 标签、JDBC 编程和 MySQL 建表这几块硬技能。适合两类人:一是做毕设或课设的学生,需要一套逻辑完整、能演示能答辩的源码;二是学校内部想低成本搭一套内部工具的运维老师。这套组合现在看着不算新,但胜在结构直白、资料多、部署门槛低,一台普通电脑、一个 Tomcat、一份 MySQL,就能把整条链路跑通。

2. 技术选型与数据库设计:JSP 为什么还活着,教材表怎么建才不返工

2.1 技术栈为什么这样组合:JSP 与 Servlet 的边界

很多人一上来就问:现在不都用 Spring Boot 吗,为什么还要写 JSP?答案是,这个系统的主要使用场景是课程设计和中小学校内部部署,维护者可能是一个计算机老师或者一届又一届的学生。Spring Boot 集成 JSP 需要额外处理目录结构和打包方式,反而比原生 Servlet+JSP 更绕。原生 JavaWeb 里,JSP 负责页面渲染,Servlet 负责接收请求和控制跳转,两者通过web.xml或注解建立映射,MySQL 作为数据落地的存储层,JDBC 负责 Java 和 MySQL 之间的对话。这套 MVC 分工虽然没有 Spring 那么精致,但每一行代码都在明面上,出了问题翻代码比翻框架文档快得多。

JSP 本身是服务端模板技术,它会在服务器端被编译成 Servlet,再输出 HTML 给浏览器。所以 JSP 页面上可以直接写 Java 代码片段(脚本段),也可以用 JSTL 和 EL 表达式减少 Java 代码的入侵。我的建议是:这个小项目里,展示列表用 JSTL 的c:forEach,表单提交和页面跳转用 Servlet 的doGetdoPost,不要把业务逻辑堆在 JSP 里。

2.2 数据库设计:教材管理系统需要几张表

教材管理系统最核心的实体有两个:教材和用户。但只建这两张表没法回答“谁在什么时候领了什么书”这个问题,所以还需要一张领用记录表。我一般会设计四张表,不加冗余字段,够用且好答辩:

表名作用关键字段
user系统用户(管理员/教师)id, username, password, real_name, role
book教材信息与库存id, isbn, book_name, publisher, author, version, price, stock, category
borrow_record教材领用记录id, user_id, book_id, borrow_num, borrow_time, status
class_info班级/院系基础数据id, class_name, department, grade

user表先塞两个账号进去,一个管理员一个普通教师,测试登录时不用反复注册。book表的stock字段是教材的当前库存数,每次领用成功后要同步扣减,这个逻辑不能靠 JSP 页面里算,要在 Servlet 里用事务包住“插入记录 + 更新库存”两步操作。borrow_record里的status字段建议用0/1表示未归还/已归还,避免用字符串,排序和统计都方便。

2.3 建库脚本:字符集和自增主键一次配好

很多项目翻车不是翻在 Java 代码,而是建库时字符集没指定,导致页面上所有中文都变成问号。这里直接给出建库脚本,注意三处:DEFAULT CHARSET要写utf8mb4,排序规则用utf8mb4_general_ci,主键用AUTO_INCREMENT而不是在 Java 代码里手动赋值。

CREATE DATABASE IF NOT EXISTS textbook_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE textbook_system; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(50) NOT NULL, role TINYINT DEFAULT 1 COMMENT '1-管理员 2-教师' ) ENGINE=InnoDB; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL, book_name VARCHAR(100) NOT NULL, publisher VARCHAR(100), author VARCHAR(50), edition VARCHAR(20), price DECIMAL(10,2), stock INT DEFAULT 0, category VARCHAR(50) ) ENGINE=InnoDB; CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, borrow_num INT DEFAULT 1, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 0 COMMENT '0-未归还 1-已归还', FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (book_id) REFERENCES book(id) ) ENGINE=InnoDB;

逻辑说明:user_idbook_id设置外键约束是为了保证领用记录不会指向不存在的用户或教材;borrow_timeDEFAULT CURRENT_TIMESTAMP省掉 Java 端手动拼时间的步骤;priceDECIMAL(10,2)而不是FLOAT,避免浮点数算钱出现0.30000000000000004这种尴尬。如果你用的是 MySQL 8.0,JDBC 驱动必须换成com.mysql.cj.jdbc.Driver,8.0 以下的驱动类名是com.mysql.jdbc.Driver,这俩写错任何一个,启动直接报ClassNotFoundException

3. 从登录页到教材入库:Servlet+JSP+JDBC 的核心代码怎么写

3.1 项目目录结构与最小文件清单

一个标准的 JavaWeb 项目目录结构是固定的,IDEA 或 Eclipse 新建 Dynamic Web Project 时会自动生成。我自己整理项目时习惯把文件按“页面层-控制层-数据层”三个层级放,这样给导师讲项目结构时三句话就能说清楚。

src/ com.school.textbook.dao/ -- 数据访问层(DAO) com.school.textbook.servlet/ -- 控制层(Servlet) com.school.textbook.util/ -- 数据库连接工具类 com.school.textbook.entity/ -- 实体类 WebContent/ index.jsp -- 登录页 book_list.jsp -- 教材列表 book_add.jsp -- 新增教材 book_edit.jsp -- 编辑教材 borrow_list.jsp -- 领用记录 WEB-INF/ web.xml -- 部署描述符 lib/ -- MySQL JDBC 驱动 jar

这里有一个新手最容易犯的错:把mysql-connector-java.jar放到项目的普通目录里,运行时报No suitable driver found。这个 jar 必须放在WEB-INF/lib下,Tomcat 启动时才会把它加载进 classpath。另外我见过有人把 jar 用 IDE 的模块依赖引入,本地能跑,导出的 war 包部署到别的机器就报驱动找不到——因为你没把 jar 打进 war 的 lib 目录。

3.2 登录页:最简单的表单也值得写得像样一点

登录页是整个系统所有人看到的第一张页面,不需要复杂的前端框架,但必须干净。表单提交到LoginServlet,账号密码用 POST 提交,避免密码出现在 URL 上。代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>教材管理系统登录</title> <style> body { font-family: "Microsoft YaHei", sans-serif; background: #f0f2f5; } .login-box { width: 360px; margin: 120px auto; background: #fff; padding: 40px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,.1); } input { width: 100%; height: 36px; margin: 8px 0 16px; padding: 0 10px; border: 1px solid #dcdfe6; border-radius: 4px; } button { width: 100%; height: 40px; background: #409eff; color: #fff; border: none; border-radius: 4px; cursor: pointer; } .err { color: #f56c6c; font-size: 14px; } </style> </head> <body> <div class="login-box"> <h3>学校教材管理系统</h3> <form action="LoginServlet" method="post"> <label>用户名</label> <input type="text" name="username" placeholder="请输入用户名" required> <label>密码</label> <input type="password" name="password" placeholder="请输入密码" required> <button type="submit">登 录</button> <% if (request.getAttribute("error") != null) { %> <p class="err"><%= request.getAttribute("error") %></p> <% } %> </form> </div> </body> </html>

逻辑说明:formaction必须是部署后的应用名加上 Servlet 的映射路径,例如/textbook_system/LoginServlet。如果你用了LoginServlet这样一个相对路径,那么当页面部署在localhost:8080/textbook_system/下时,它会自动拼成http://localhost:8080/textbook_system/LoginServlet,这是 Tomcat 默认的行为。另外我习惯在 Servlet 里用request.setAttribute("error", "用户名或密码错误")返回登录页,而不是用out.print直接输出,这样页面样式不会被打乱。

3.3 DBUtil:数据库连接怎么封装才不会一个页面建一个连接

整个系统里 Servlet 和 DAO 都要访问数据库,如果把获取连接的代码到处复制,改一次密码要改十几个文件。定义一个DBUtil工具类,用静态方法返回Connection,是 JavaWeb 项目最通用的做法。下面的代码我加上了时区和字符集参数,这是 MySQL 8.0 下最常见的报错点:

package com.school.textbook.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { // 驱动类名:MySQL 8.x 必须用 cj 版本 private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; // useSSL=false 避免 SSL 握手警告;serverTimezone 指定时区否则报错 private static final String URL = "jdbc:mysql://localhost:3306/textbook_system?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"; private static final String USER = "root"; private static final String PASSWORD = "你的数据库密码"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, 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(); } } }

逻辑说明:Class.forName放在静态代码块里,保证类加载时只注册一次驱动。close方法重载了三个参数,注意关闭顺序是先ResultSetStatement最后Connection,反着关在某些驱动实现里会报连接未释放的警告。实际项目中连接池用 C3P0 或 Druid,但这个小系统用DriverManager完全够,后面第 6 章我会说怎么改成连接池。

3.4 教材入库:Servlet 接参数、DAO 写数据、JSP 给反馈

新增教材是系统的核心操作,链路最清晰。前端book_add.jsp提交表单到BookServlet?action=add,Servlet 里doPost接参后调BookDao.addBook(book),成功则重定向到列表页,失败则转发回新增页并携带错误信息。这里看一下关键代码:

// BookServlet 中的 doPost 核心片段 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String action = request.getParameter("action"); if ("add".equals(action)) { Book book = new Book(); book.setIsbn(request.getParameter("isbn")); book.setBookName(request.getParameter("bookName")); book.setPublisher(request.getParameter("publisher")); book.setAuthor(request.getParameter("author")); book.setEdition(request.getParameter("edition")); book.setPrice(new BigDecimal(request.getParameter("price"))); book.setStock(Integer.parseInt(request.getParameter("stock"))); book.setCategory(request.getParameter("category")); BookDao dao = new BookDao(); boolean ok = dao.addBook(book); if (ok) { // 重定向用 sendRedirect,避免刷新页面重复提交表单 response.sendRedirect("BookServlet?action=list"); } else { request.setAttribute("error", "教材入库失败,请检查 ISBN 是否重复"); request.getRequestDispatcher("book_add.jsp").forward(request, response); } } }

BookDao.addBook的实现就三条 SQL 的活,但我加了UNIQUE约束的兜底判断:

public boolean addBook(Book book) { String sql = "INSERT INTO book (isbn, book_name, publisher, author, edition, price, stock, category) VALUES (?,?,?,?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, book.getIsbn()); ps.setString(2, book.getBookName()); ps.setString(3, book.getPublisher()); ps.setString(4, book.getAuthor()); ps.setString(5, book.getEdition()); ps.setBigDecimal(6, book.getPrice()); ps.setInt(7, book.getStock()); ps.setString(8, book.getCategory()); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } }

参数说明:所有外部输入都走了PreparedStatement的预编译占位符,绝不拼 SQL 字符串。这里多说一句,executeUpdate()返回的是受影响行数,插入成功一定是 1,用> 0判断是通用写法。setBigDecimal对应 MySQL 的DECIMAL类型,setInt对应INT,类型不匹配的报错会在SQLException里抛出,控制台会输出类似Data truncation: Out of range value的信息。JDK 7 以上支持 try-with-resources,ConnectionPreparedStatement会自动关闭,不用手动调用close(),代码干净不少。

3.5 教材列表与编辑删除:三个小细节

列表页用 JSTL 遍历,这里有个细节:JSP 页面顶部要写<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>,并且WEB-INF/lib下要放jstl-1.2.jar,否则c:forEach直接报错。核心片段:

<table border="1" cellspacing="0" cellpadding="6"> <tr> <th>ISBN</th><th>书名</th><th>出版社</th> <th>库存</th><th>价格</th><th>操作</th> </tr> <c:forEach items="${bookList}" var="b"> <tr> <td>${b.isbn}</td> <td>${b.bookName}</td> <td>${b.publisher}</td> <td>${b.stock}</td> <td>${b.price}</td> <td> <a href="BookServlet?action=edit&id=${b.id}">编辑</a> <a href="BookServlet?action=delete&id=${b.id}" onclick="return confirm('确认删除该教材?')">删除</a> </td> </tr> </c:forEach> </table>

操作列里传了id,这是对的——所有针对单条数据的操作都要带上主键id作为定位条件。删除操作的confirm()是前端最后的防线,因为删除是不可恢复的操作,后端也可以再加一层判断。编辑功能我习惯复用book_add.jspBookServletedit分支里把book对象塞进 request,JSP 页面用${book.bookName}做表单默认值,提交路径改成action=update,这样减少一个页面文件的维护成本。这算是一个小套路,效果很好——你不用为新增和编辑分别写两个长得几乎一样的 HTML 页面。

4. 把项目跑在 Tomcat 上:MySQL 建库、部署发布与常见启动失败

4.1 环境版本选型:JDK 8 + Tomcat 9 + MySQL 8.0 是黄金组合

这三个版本搭配是我试过最多路线的结果。JDK 8 不用多解释,它是目前兼容性最好的版本。Tomcat 9 对应 Servlet 4.0 规范,支持 JDK 8 及以上,稳定性足够。MySQL 8.0 虽然比 5.7 配置要求高一点,但它是当前主流新装版本,教程多,遇到报错搜索答案容易。如果你的机器上装的是 JDK 17 或更高,要注意 Tomcat 版本必须选 Tomcat 10 以上,而且 Servlet 的import javax.servlet.*要改成import jakarta.servlet.*——这两者不兼容,网上大量旧教程直接照抄会报程序包javax.servlet不存在。这也是我建议初学者用 JDK 8 + Tomcat 9 的原因,不是为了追新,而是为了少踩一个坑。

检查当前环境的命令:

java -version mysql --version

如果这两个命令提示找不到,说明你的 JDK 或者 MySQL 还没配环境变量。Windows 上环境变量配完必须重新打开命令行窗口才生效,这个细节经常被人忽略——配好了,一运行还是老版本,结果才发现是没开新终端。

4.2 MySQL 建库授权:root 别拿来直接连,新建一个专用账号

很多课程设计代码里写死root账号,这是省事,但存在两个问题:一是密码写在源码里,项目交付时等于把整个数据库管理权限交出去;二是root的密码修改会影响所有写死的配置。我习惯在 MySQL 里单独建一个应用账号,只给textbook_system库的增删改查权限。

CREATE USER 'textbook_app'@'localhost' IDENTIFIED BY 'textbook123'; GRANT SELECT, INSERT, UPDATE, DELETE ON textbook_system.* TO 'textbook_app'@'localhost'; FLUSH PRIVILEGES;

参数说明:'textbook_app'@'localhost'表示只允许本机连接,如果将来要把数据库和 Tomcat 拆到两台机器,要改成'textbook_app'@'%'并在 MySQL 的my.cnf里放开 bind-address 的注释。授权语句中的textbook_system.*表示该账号只能操作textbook_system这个库下的表,无法访问其他库。改完以后DBUtil.java里的USERPASSWORD常量同步替换,然后重新编译部署。另外要提醒一点:MySQL 8.0 默认的认证插件是caching_sha2_password,老版本的 JDBC 驱动不认识它,如果你的 JDBC 驱动是 5.x,连接时会报Unable to load authentication plugin 'caching_sha2_password',解决方式是换驱动到 8.0 以上,或者在创建用户时指定IDENTIFIED WITH mysql_native_password BY '密码'

4.3 部署到 Tomcat:war 包 or 直接发布目录

Eclipse 或 IDEA 里都有直接发布到 Tomcat 的功能,调试阶段用这个方式最方便。但到了给老师演示或者部署到服务器阶段,我建议导出 war 包放 Tomcat 的webapps目录下。

# 将 war 包复制到 Tomcat 的 webapps 目录(Windows 用 copy 命令) cp /path/to/textbook_system.war $CATALINA_HOME/webapps/ # 启动 Tomcat $CATALINA_HOME/bin/startup.sh # Windows 下使用 # %CATALINA_HOME%\bin\startup.bat

Tomcat 启动后会自动解压 war 包,生成同名的目录。这时访问地址是http://localhost:8080/textbook_system/index.jsp。注意 URL 里的路径大小写敏感,textbook_system这个名字取决于你的 war 包文件名。想改访问路径就改 war 包的文件名,改了之后要删掉旧的解压目录再重启,否则 Tomcat 会一直用老目录。

4.4 启动失败的排错顺序:日志永远比报错弹窗有用

Tomcat 启动报错是最容易让人慌的场景,但我可以告诉你一个原则:不要先看浏览器,先看日志。日志位置在$CATALINA_HOME/logs/,核心文件是catalina.outlocalhost.YYYY-MM-DD.log。启动失败的常见原因按概率排序如下:

现场常见原因处理动作
启动后立刻退出,端口冲突8080 被占用conf/server.xml里改端口,或杀掉占用进程
页面 404war 部署失败或路径写错检查webapps下是否有解压目录,确认访问路径
ClassNotFoundException缺少 MySQL 驱动 jar确认 jar 在WEB-INF/lib下,且版本对应
Communications link failure数据库没启动或用户名密码错先在命令行里用mysql -u账号 -p密码试连
启动正常但 Session 失效服务器时间和客户端时间差太多校时服务器后重启

我见过最离谱的一次,是学生把mysql-connector-java-8.0.30.jar放到了 Tomcat 的lib目录下,理论上也能用,但换了一台电脑部署,新电脑的 Tomcat 里没有这个 jar,项目启动就报驱动找不到。所以奉劝一句:项目要自带依赖,不要依赖运行环境的额外配置

5. 学校教材管理系统避坑:五个让我返工的真实问题

5.1 登录成功后一刷新就退出了,Session 失效像玄学

现象:登录成功后进入主页面,按 F5 刷新或者打开新标签页,系统立刻跳回登录页。检查数据库账号没问题,浏览器 Cookie 也开了。

原因:登录逻辑没有把用户信息写入 Session,或者关闭浏览器后 Session 被清空。JavaWeb 里识别登录状态的唯一标准是HttpSession,不是浏览器 Cookie。登录成功后必须有request.getSession().setAttribute("user", user)这一步,同时后面的每个页面用session.getAttribute("user")判断是否为空。

解决:写一个checkLogin的公共方法,所有页面请求进来先过一遍 Session 判断。示例:

HttpSession session = request.getSession(false); // false 表示不存在时不自动创建 if (session == null || session.getAttribute("user") == null) { response.sendRedirect("index.jsp"); return; }

getSession(false)而不是getSession()的好处是:如果你只是访问一个静态资源,不会为这个请求无谓地创建一个新的 Session 对象。

5.2 JSP 页面上全是问号,数据库本来就是空的

现象:页面头部声明了contentType="text/html; charset=UTF-8",数据库建表也写了utf8mb4,但后台插入中文数据后,刷新页面看到???

原因:三层编码每一层都可能出问题。最容易被忽略的是 JSP 编译阶段指定的编码,以及 Servlet 里接收请求参数时的编码。如果 JSP 文件保存的物理编码是 GBK,而pageEncoding写的是 UTF-8,Tomcat 编译出来的字符就是乱的。

解决:在 Servlet 的doPostdoGet最前面各加一行:

request.setCharacterEncoding("UTF-8"); response.setContentType("text/html; charset=UTF-8");

另外检查 MySQL 连接 URL 里是否带了characterEncoding=UTF-8,没有的话数据库驱动会用默认编码和服务器端交互,中文一样会出问题。这条路径上四道关卡——页面编码、请求编码、连接编码、数据库表编码,任何一处不一致,效果都等同乱码。排查时用排除法:先在 MySQL 命令行里INSERT一条中文数据,如果命令行能存进去且查询出来正确,说明数据库没问题,再往上翻 JSP 和 Servlet 的编码设置。

5.3 页面能打开但 CSS 全失效,浏览器控制台一堆 404

现象:进入book_list.jsp后表格数据有,但样式全部丢失,控制台显示样式文件 404。

原因:JSP 页面在webapps/textbook_system/子目录下,而 CSS 引用写成<link href="css/style.css">这种相对路径。浏览器解析时,当前 URL 是http://localhost:8080/textbook_system/book_list.jsp,相对路径会解析成http://localhost:8080/textbook_system/css/style.css,能正常加载。但如果通过BookServlet转发到 JSP,URL 变成http://localhost:8080/textbook_system/BookServlet,相对路径解析就变成了http://localhost:8080/textbook_system/BookServlet/css/style.css,当然 404。

解决:JSP 页面顶部用${pageContext.request.contextPath}拼出应用的根路径,所有静态资源用绝对路径引用:

<link href="${pageContext.request.contextPath}/css/style.css" rel="stylesheet">

菜鸟阶段最容易犯这个错,症状有时候还是“列表页没样式、登录页正常”,因为列表页经常是转发过去的,登录页是直接访问的。这类错觉类问题,定位要快,治根的方法只有一种——绝对路径。

5.4 编辑教材保存后,库存变成 NULL,页面直接报空指针

现象:编辑某条教材记录,只改了价格和出版社,保存后发现控制台报NullPointerException,数据库里这条记录的stock变成了0

原因:编辑页面的表单里没有包含stock字段,或该字段被前端 JS 隐藏了。浏览器提交时request.getParameter("stock")返回null,代码直接调Integer.parseInt(null)抛异常。

解决:编辑页面的表单里,所有数据库非空字段都必须有对应的input元素。如果库存不允许在编辑时修改,用隐藏域带上这个值:

<input type="hidden" name="stock" value="${book.stock}">

另外,Servlet 接参时对所有数值型参数做空值兜底,这是个好习惯:

String stockStr = request.getParameter("stock"); int stock = (stockStr == null || stockStr.isEmpty()) ? 0 : Integer.parseInt(stockStr);

这类的“灵异事件”,本质都逃不出“空值没处理”和“字段没提交”这两类。写代码时对每个getParameter先问一句:如果这个参数没传,程序会不会出错。如果会,先做兜底。

6. 给项目加一层“专业感”:分页、拦截器与连接池改造

做课设或交付给学校使用时,基础功能跑通只是及格线。每次演示时长有限,你的列表页如果一次性显示几百条数据,页面加载慢,导师或验收老师的第一印象就不太好。我建议在基础功能之上,优先补三样东西——分页、登录拦截器和连接池,成本低、见效快,而且是很自然的加分项。

分页在教科书上的说法是“减少单次查询的数据量”,落到代码里其实就一个LIMIT的事。以教材列表为例,页码page从请求参数里拿,每页大小pageSize定为 10,查询语句变成SELECT * FROM book LIMIT ?, ?,两个参数分别是偏移量和条数。SQL 里的LIMIT第一参数从 0 开始,所以页面上的第 1 页要换算成(page-1) * pageSize。总页数要先查SELECT COUNT(*) FROM book,然后用(total + pageSize - 1) / pageSize向上取整。页面底部放上一页、下一页和页码列表,注意第一页时“上一页”按钮要禁用,最后一页时“下一页”要禁用,这个细节做了就会显得很像一个正式系统。

第二个改造是登录拦截。现在每个 Servlet 里都手写一遍 Session 判断太傻了,定义一个过滤器统一处理:

@WebFilter("/*") // 拦截所有请求 public class LoginFilter implements Filter { 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); String uri = request.getRequestURI(); // 登录页、静态资源和登录接口直接放行 if (uri.endsWith("index.jsp") || uri.contains("/css/") || uri.endsWith("LoginServlet") || session != null && session.getAttribute("user") != null) { chain.doFilter(req, resp); } else { response.sendRedirect(request.getContextPath() + "/index.jsp"); } } }

还有一个值得做的改造是连接池。当前DBUtil每次查询都新建物理连接,对 MySQL 来说建立连接是个相对昂贵的操作。接入 Druid 连接池只需要把获取连接的逻辑换成从DruidDataSource拿,不用改 DAO 层的任何代码——因为你调用的一直是DBUtil.getConnection()。这个小改动会让你的项目在答辩时被问到“性能方面有没有考虑”时,有话可说。

最后,作为一个被 JSP 折磨过很多年的老工程师,我想顺便说句体己话:JSP 技术虽然在企业开发里已经被 Spring Boot 大幅取代,但它在教育场景里的价值并没有消失——它让你能看到 HTTP 请求从浏览器到数据库再返回的完整过程,也让你真正理解为什么后来的框架要做模板引擎、要做前后端分离。这套教材管理系统做完,跑起来,录个演示视频,你能带走的不只是学分,而是一整条 JavaWeb 的完整链路。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询