基于jspm的高校教材管理系统设计与实现全解析

基于jspm的高校教材管理系统设计与实现全解析 每到毕业季计算机专业的同学都在为一件事发愁毕业设计选题。尤其是“教材管理系统”这类题目看似简单真正动手时会发现既要保证系统功能完整又要把论文写清楚还要在答辩时能讲明白设计思路。网上的源码很多但要么缺少配套说明文档要么项目结构混乱很难直接运行。本文围绕“基于 jspm 的高校教材管理系统”这一经典毕业设计课题从技术选型、数据库设计、核心功能实现到常见报错排查做一次完整拆解帮助你理解这套系统的开发全流程也能直接作为自己设计或二次开发的参考。1. 项目背景与技术选型1.1 教材管理系统解决什么问题高校教材管理是一个典型的业务场景。每学期开学前教务处要统计各院系、各专业的教材需求教材科需要根据库存和征订计划采购教材学生或班级要按指定书目缴费领书期末还要核算各班领用明细、统计剩余库存。若使用 Excel 或手工记账数据分散、更新慢、容易出错也难以追踪每本教材的“入库—领用—库存”全流程。教材管理系统就是把上述流程信息化管理员维护教材信息、管理库存学生或班级负责人在线查看教材、提交领用申请系统自动生成统计报表。和简单的增删改查系统相比这类系统更需要关注“库存状态”“领用记录”“角色权限”这些业务细节也正因为如此它很适合作为计算机毕业设计选题——功能清晰、逻辑完整、工作量适中。1.2 jspm 是什么意思项目标题里的“jspm”并不是某个独立框架而是 JSP Servlet JavaBean 的简称只是拼写被简写成了 jspm。这类项目早期常被称为 JSP Model 1 或 Model 2 结构的教学案例。也就是说它采用 Java Web 最经典的开发方式JSPJava Server Pages负责页面展示动态渲染 HTMLServlet负责接收浏览器请求处理业务逻辑控制页面跳转JavaBean / Model封装数据模型和数据库操作逻辑MySQL保存业务数据Tomcat作为 Web 服务器运行项目。在 Spring Boot 流行的今天为什么要还在用 jspm 做毕业设计主要原因有三个第一它是 Java Web 课程的核心内容能直观体现对 Servlet、JSP、JDBC、面向对象设计的掌握程度第二不依赖过于复杂的框架适合独立开发、独立讲解第三这类项目的源码结构清楚代码量适中写论文时更容易把“设计思路”讲透彻。所以如果你在 GitHub、CSDN 或网盘里看到“基于 jspm 高校教材管理系统”资源先不必被陌生的名字劝退它本质上还是一个标准的 Java Web 项目。1.3 技术栈与运行原理一个典型 jspm 教材管理系统的技术栈如下层次技术/工具作用前端页面JSP、HTML、CSS、JavaScript用户交互与数据展示后端处理Servlet、JavaBean请求分发、业务逻辑、数据库操作数据存储MySQL保存用户、教材、领用、库存等数据服务器Tomcat运行 Web 应用开发工具Eclipse 或 IntelliJ IDEA编写代码、发布项目JDBC 连接JDBC MySQL Connector/JJava 与 MySQL 通信浏览器访问一个 JSP 页面或 Servlet 地址后Tomcat 将请求交给对应 Servlet 处理Servlet 调用 JavaBean 访问 MySQL 数据库再把查询结果放回 request 域或 session 域最终由 JSP 渲染成 HTML 返回给浏览器。这就是 JSP/Servlet 应用最基础的运行原理。2. 环境准备与项目结构2.1 环境版本选择建议一个 jspm 项目对运行环境的要求并不苛刻但版本搭配不合理仍然会出现各种报错。建议使用下面的组合这是目前兼容性较好、资料最多的一套JDK 1.8绝大多数教材和笔记以 JDK 8 为基础Tomcat 8.5 / 9.0MySQL 5.7 或 8.0Eclipse IDE for Enterprise Java and Web Developers或 IntelliJ IDEA UltimateMySQL Connector/J 5.1.49如果使用 MySQL 8.0建议改为 8.0.33注意以上版本并非硬性要求如果你的电脑已经装了更高版本的 JDK 或 Tomcat需要根据实际情况调整。比如 JDK 17 搭配 Tomcat 10 不是不可以只是 Tomcat 10 的包名从javax.servlet改成了jakarta.servlet网上很多基于javax的代码需要改 import 才能运行毕业设计选型时不建议引入这种额外复杂度。2.2 项目目录结构为了便于后续扩展和维护建议在 Eclipse 或 IDEA 中使用标准的 Web 项目结构。以下是一个参考结构TextbookMS/ ├── src/ │ ├── com.textbook.bean/ # 实体类User、Textbook、ReceiveRecord 等 │ ├── com.textbook.dao/ # 数据访问层操作数据库 │ ├── com.textbook.service/ # 业务逻辑层 │ ├── com.textbook.servlet/ # Servlet 控制器 │ └── com.textbook.util/ # 数据库连接、字符串处理等工具类 ├── WebContent/或 src/main/webapp │ ├── admin/ # 管理员页面 │ │ ├── textbookList.jsp │ │ ├── textbookAdd.jsp │ │ └── ... │ ├── student/ # 学生/班级页面 │ ├── login.jsp │ ├── index.jsp │ ├── css/ │ └── js/ └── WebContent/WEB-INF/ └── web.xml # Web 应用部署描述文件实际开发中根据个人习惯可以把dao和service合并也可以直接在 Servlet 中调用 DAO。但为了论文中能写出“分层设计”建议保留三层结构Servlet控制层、Service业务层、DAO数据访问层。3. 数据库设计与核心表结构3.1 数据表关系设计教材管理系统通常包含以下核心表管理员表admin保存系统管理员账号。用户表user保存学生或班级用户信息。教材表textbook保存教材名称、ISBN、作者、出版社、价格、库存等。教材入库表storage_record记录每一批教材入库的数量和时间。领用记录表receive_record记录哪个用户、在什么时间、领用了哪本教材、数量是多少。学院/班级表class_info有些系统还会把学院、班级独立成表方便按班级统计。注意一个关键点教材的“当前库存”不要直接通过历史入库数量减去领用数量临时计算否则数据量变大后查询效率会很差。更合理的做法是在textbook表中维护一个stock字段每次入库增加每次领用减少。具体公式可以保留在论文中做说明但数据库设计必须同时保留流水表和库存字段。3.2 建表 SQL 示例下面给出简化版建表语句方便你快速搭建数据库。字段可以根据实际需要调整。-- 管理员表 CREATE TABLE admin ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 教材表 CREATE TABLE textbook ( id INT NOT NULL AUTO_INCREMENT, isbn VARCHAR(30) DEFAULT NULL, book_name VARCHAR(200) NOT NULL, author VARCHAR(100) DEFAULT NULL, publisher VARCHAR(200) DEFAULT NULL, price DECIMAL(10,2) DEFAULT 0.00, stock INT DEFAULT 0, publish_time DATE DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 教材入库流水表 CREATE TABLE storage_record ( id INT NOT NULL AUTO_INCREMENT, textbook_id INT NOT NULL, storage_count INT NOT NULL, storage_time DATETIME DEFAULT NULL, operator VARCHAR(50) DEFAULT NULL, PRIMARY KEY (id), KEY idx_textbook_id (textbook_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 领用记录表 CREATE TABLE receive_record ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, textbook_id INT NOT NULL, receive_count INT NOT NULL, receive_time DATETIME DEFAULT NULL, remark VARCHAR(500) DEFAULT NULL, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_textbook_id (textbook_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;需要注意的是教材表和领用记录表之间属于一对多关系一条领用记录只能对应一本教材但一本教材可以被多人多次领用。为了控制并发和避免脏数据领用时应该使用事务这一点在后面代码中会体现。4. 核心功能代码实现4.1 数据库连接工具类项目中所有 DAO 层操作都需要获取数据库连接所以第一步是封装一个 JDBC 工具类。这里以 Properties 文件配置连接参数为例便于修改环境。先创建db.properties文件放在src目录下jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/textbookms?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456再创建数据库连接工具类// 文件路径src/com/textbook/util/DBUtil.java package com.textbook.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; public class DBUtil { private static String driver; private static String url; private static String username; private static String password; static { try { // 加载配置文件 InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties); Properties props new Properties(); props.load(in); driver props.getProperty(jdbc.driver); url props.getProperty(jdbc.url); username props.getProperty(jdbc.username); password props.getProperty(jdbc.password); // 注册驱动 Class.forName(driver); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(ResultSet rs, Statement stmt, Connection conn) { try { if (rs ! null) { rs.close(); } if (stmt ! null) { stmt.close(); } if (conn ! null) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }这段代码的核心思路是在静态代码块中读取配置并注册驱动之后所有 DAO 类直接调用getConnection()就能拿到连接。关闭资源时遵循“先开的后关”原则防止连接泄漏。这里有一点必须强调实际项目中不要把数据库密码明文写在代码里最好放在配置文件。如果是本地测试123456这种密码可以使用但论文里如果涉及系统安全设计建议写明“生产环境应使用更强的密码策略并加密存储”。4.2 实体类设计实体类用于封装数据库表的行数据。以教材实体为例// 文件路径src/com/textbook/bean/Textbook.java package com.textbook.bean; import java.math.BigDecimal; public class Textbook { private Integer id; private String isbn; private String bookName; private String author; private String publisher; private BigDecimal price; private Integer stock; private java.sql.Date publishTime; // 无参构造方法 public Textbook() { } public Integer getId() { return id; } public void setId(Integer id) { this.id id; } public String getIsbn() { return isbn; } public void setIsbn(String isbn) { this.isbn isbn; } public String getBookName() { return bookName; } public void setBookName(String bookName) { this.bookName bookName; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author author; } public String getPublisher() { return publisher; } public void setPublisher(String publisher) { this.publisher publisher; } public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price price; } public Integer getStock() { return stock; } public void setStock(Integer stock) { this.stock stock; } public java.sql.Date getPublishTime() { return publishTime; } public void setPublishTime(java.sql.Date publishTime) { this.publishTime publishTime; } }价格字段选择BigDecimal而不是double是为避免浮点数计算误差。这一点在答辩时如果被老师问到是很好的加分点。4.3 DAO 层实现教材查询与新增DAO 层负责和数据库打交道。下面实现两个基础功能分页查询教材列表、新增教材。// 文件路径src/com/textbook/dao/TextbookDao.java package com.textbook.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.ArrayList; import java.util.List; import com.textbook.bean.Textbook; import com.textbook.util.DBUtil; public class TextbookDao { // 分页查询教材列表 public ListTextbook findByPage(int pageNum, int pageSize) throws Exception { ListTextbook list new ArrayList(); String sql SELECT id, isbn, book_name, author, publisher, price, stock, publish_time FROM textbook ORDER BY id DESC LIMIT ?, ?; Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); ps.setInt(1, (pageNum - 1) * pageSize); ps.setInt(2, pageSize); rs ps.executeQuery(); while (rs.next()) { Textbook t new Textbook(); t.setId(rs.getInt(id)); t.setIsbn(rs.getString(isbn)); t.setBookName(rs.getString(book_name)); t.setAuthor(rs.getString(author)); t.setPublisher(rs.getString(publisher)); t.setPrice(rs.getBigDecimal(price)); t.setStock(rs.getInt(stock)); t.setPublishTime(rs.getDate(publish_time)); list.add(t); } return list; } finally { DBUtil.close(rs, ps, conn); } } // 查询教材总记录数用于分页 public int count() throws Exception { String sql SELECT COUNT(*) FROM textbook; Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); rs ps.executeQuery(); if (rs.next()) { return rs.getInt(1); } return 0; } finally { DBUtil.close(rs, ps, conn); } } // 新增教材 public int insert(Textbook t) throws Exception { String sql INSERT INTO textbook(isbn, book_name, author, publisher, price, stock, publish_time) VALUES(?, ?, ?, ?, ?, ?, ?); Connection conn null; PreparedStatement ps null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); ps.setString(1, t.getIsbn()); ps.setString(2, t.getBookName()); ps.setString(3, t.getAuthor()); ps.setString(4, t.getPublisher()); ps.setBigDecimal(5, t.getPrice()); ps.setInt(6, t.getStock()); ps.setDate(7, t.getPublishTime()); return ps.executeUpdate(); } finally { DBUtil.close(null, ps, conn); } } }使用PreparedStatement而不是直接拼接 SQL可以有效防止 SQL 注入也是项目安全设计的必要措施。注意代码中在finally里释放资源这样即使查询出错也能保证连接归还给连接池或关闭。4.4 Servlet 控制层实现分页查询Servlet 的主要职责是从请求中读取参数调用 DAO把结果放到 request 域或 session 域再转发到 JSP 页面。// 文件路径src/com/textbook/servlet/TextbookListServlet.java package com.textbook.servlet; import java.io.IOException; import java.util.List; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.textbook.bean.Textbook; import com.textbook.dao.TextbookDao; WebServlet(/admin/textbookList) public class TextbookListServlet extends HttpServlet { private static final long serialVersionUID 1L; private TextbookDao textbookDao new TextbookDao(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 默认当前页为第 1 页每页显示 5 条 int pageNum 1; int pageSize 5; String pageNumStr request.getParameter(pageNum); if (pageNumStr ! null !.equals(pageNumStr)) { pageNum Integer.parseInt(pageNumStr); } try { ListTextbook list textbookDao.findByPage(pageNum, pageSize); int totalCount textbookDao.count(); int totalPages (int) Math.ceil(totalCount * 1.0 / pageSize); request.setAttribute(list, list); request.setAttribute(totalPages, totalPages); request.setAttribute(pageNum, pageNum); request.getRequestDispatcher(/admin/textbookList.jsp).forward(request, response); } catch (Exception e) { e.printStackTrace(); response.getWriter().write(系统异常请稍后重试); } } }Servlet 用WebServlet注解方式注册代码比在web.xml中配置更简洁。翻页时页面上的分页链接会携带pageNum参数Servlet 重新计算起始行后返回新数据。4.5 用户登录与 Session 校验登录模块是几乎所有 Web 系统都有的功能。这里的关键点不只是“查询到记录就算登录成功”还应该考虑密码加密和 Session 失效。密码加密在毕业设计里经常被忽略。如果直接把明文密码和数据库里的密码比较论文里的“安全设计”会缺乏说服力。一个简单可行的方式是使用 MD5 加盐public static String md5(String source) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] bytes md.digest(source.getBytes(UTF-8)); StringBuilder sb new StringBuilder(); for (byte b : bytes) { String hex Integer.toHexString(b 0xff); if (hex.length() 1) { sb.append(0); } sb.append(hex); } return sb.toString(); } catch (Exception e) { throw new RuntimeException(e); } }注意MD5 在现代安全要求下并不算强加密但作为教学项目已经足够说明你懂“不能明文存密码”的原则。更安全的做法是使用 BCrypt 或 SHA-256 加盐论文中可以根据实际情况选用。下面是登录 Servlet 的简化逻辑// 文件路径src/com/textbook/servlet/LoginServlet.java package com.textbook.servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.textbook.bean.Admin; import com.textbook.dao.AdminDao; WebServlet(/login) public class LoginServlet extends HttpServlet { private static final long serialVersionUID 1L; private AdminDao adminDao new AdminDao(); Override 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 || password null || .equals(username.trim()) || .equals(password.trim())) { request.setAttribute(msg, 用户名和密码不能为空); request.getRequestDispatcher(login.jsp).forward(request, response); return; } try { Admin admin adminDao.findByUsernameAndPassword(username, password); if (admin ! null) { HttpSession session request.getSession(); session.setAttribute(loginAdmin, admin); response.sendRedirect(request.getContextPath() /admin/textbookList); } else { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } } catch (Exception e) { e.printStackTrace(); request.setAttribute(msg, 系统异常); request.getRequestDispatcher(login.jsp).forward(request, response); } } }这里把登录成功的用户对象放进session。之后每个需要权限的页面或 Servlet 都应当先校验 Session 中是否存在该对象没有就强制跳回登录页。这种“Session 过滤器”代码可以在 Filter 中统一实现避免每个 Servlet 重复写判断逻辑。4.6 领用教材与事务控制教材领用是系统中最容易出现数据不一致的功能。假设系统需要满足某个用户领用教材成功后教材表库存减少领用记录表新增一条记录。如果“扣库存”成功但“插入领用记录”失败库存就对不上了。这时必须开启事务。下面是核心逻辑public boolean receiveTextbook(int userId, int textbookId, int count) throws Exception { String updateStockSql UPDATE textbook SET stock stock - ? WHERE id ? AND stock ?; String insertRecordSql INSERT INTO receive_record(user_id, textbook_id, receive_count, receive_time, remark) VALUES(?, ?, ?, NOW(), ?); Connection conn null; PreparedStatement ps1 null; PreparedStatement ps2 null; try { conn DBUtil.getConnection(); // 关闭自动提交开启事务 conn.setAutoCommit(false); ps1 conn.prepareStatement(updateStockSql); ps1.setInt(1, count); ps1.setInt(2, textbookId); ps1.setInt(3, count); int rows ps1.executeUpdate(); if (rows 0) { // 说明库存不足或 id 不存在 conn.rollback(); return false; } ps2 conn.prepareStatement(insertRecordSql); ps2.setInt(1, userId); ps2.setInt(2, textbookId); ps2.setInt(3, count); ps2.setString(4, 班级领用); ps2.executeUpdate(); // 全部成功才提交 conn.commit(); return true; } catch (Exception e) { if (conn ! null) { conn.rollback(); } throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); } DBUtil.close(null, ps2, conn); DBUtil.close(null, ps1, null); } }特别注意第 18 行的 SQL 条件stock ?。这个条件能在数据库层面防止库存扣成负数即使同时有两个用户并发领用也不会出现超卖。这是比“先查询再判断”更可靠的做法。如果论文中需要画流程图可以把领用流程描述为校验登录 → 接收教材与数量参数 → 开启事务 → 扣减库存 → 写入领用记录 → 提交事务 → 返回成功。如果中途发生异常则回滚。5. JSP 页面实现要点5.1 教材列表页面JSP 页面用于展示 Servlet 转发过来的数据。页面中可以通过JSTL标签或 Java 代码片段遍历数据。如果项目没有引入 JSTL 依赖也可以使用最简单的% for %方式。以下是简化版textbookList.jsp片段% page contentTypetext/html;charsetUTF-8 languagejava % % page importjava.util.List % % page importcom.textbook.bean.Textbook % html head title教材列表/title /head body h2教材管理/h2 table border1 cellpadding10 cellspacing0 tr thISBN/th th教材名称/th th作者/th th出版社/th th价格/th th库存/th th操作/th /tr % ListTextbook list (ListTextbook) request.getAttribute(list); if (list ! null) { for (Textbook t : list) { % tr td% t.getIsbn() %/td td% t.getBookName() %/td td% t.getAuthor() %/td td% t.getPublisher() %/td td% t.getPrice() %/td td% t.getStock() %/td td a href% request.getContextPath() %/admin/textbookEdit?id% t.getId() %编辑/a a href% request.getContextPath() %/admin/textbookDelete?id% t.getId() % onclickreturn confirm(确定删除吗)删除/a /td /tr % } } % /table !-- 分页链接 -- % int pageNum (Integer) request.getAttribute(pageNum); int totalPages (Integer) request.getAttribute(totalPages); % p 共 % totalPages % 页 % if (pageNum 1) { % a href% request.getContextPath() %/admin/textbookList?pageNum% pageNum - 1 %上一页/a % } % % if (pageNum totalPages) { % a href% request.getContextPath() %/admin/textbookList?pageNum% pageNum 1 %下一页/a % } % /p /body /html这样写比较直观但页面中嵌入了大量 Java 代码不太优雅。实际操作时如果已经引入了 JSTL推荐用c:forEach遍历页面会清爽很多。这里给出 JSTL 版本的核心片段供参考% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % c:forEach items${list} vart tr td${t.isbn}/td td${t.bookName}/td td${t.author}/td td${t.publisher}/td td${t.price}/td td${t.stock}/td /tr /c:forEach5.2 新增和编辑页面新增和编辑教材页面的表单基本相同。实际操作中可以共用同一个 JSP页面通过 Servlet 是否传入了教材对象来判断是新增还是编辑。% page contentTypetext/html;charsetUTF-8 languagejava % html head title新增/编辑教材/title /head body h2新增/编辑教材/h2 form action% request.getContextPath() %/admin/textbookSave methodpost input typehidden nameid value${textbook.id} / labelISBN/label input typetext nameisbn value${textbook.isbn} / br/br/ label教材名称/label input typetext namebookName value${textbook.bookName} / br/br/ label作者/label input typetext nameauthor value${textbook.author} / br/br/ label出版社/label input typetext namepublisher value${textbook.publisher} / br/br/ label价格/label input typetext nameprice value${textbook.price} / br/br/ label库存/label input typetext namestock value${textbook.stock} / br/br/ label出版日期/label input typedate namepublishTime value${textbook.publishTime} / br/br/ button typesubmit保存/button /form /body /html如果使用纯 JSP 而没有引入 EL 表达式支持那么${textbook.id}无法正常解析。此时需要检查项目是否满足 Servlet 2.4 规范声明或者把 EL 改为% request.getAttribute(textbook) ! null %之类的写法。多数 Tomcat 8.5 默认支持 EL不需要额外配置。6. 常见报错与排查思路毕业设计阶段同学们遇到的大多数问题并不复杂但报错信息往往让人一头雾水。下面整理几张高频排查表。6.1 项目启动与部署常见问题问题现象常见原因解决思路Tomcat 启动时不报错但访问 404Web 应用未部署成功或访问路径不对检查项目是否已经添加到 Tomcat 的 webapps 或通过 IDE 发布确认访问地址是否包含项目上下文名启动报ClassNotFoundException: com.mysql.jdbc.DriverJDBC 驱动包未放入WEB-INF/lib或未引入下载 mysql-connector-java 包放入WEB-INF/lib目录页面中文乱码请求和响应编码不一致确保 JSP 页面使用UTF-8Servlet 中执行request.setCharacterEncoding(UTF-8)数据库 URL 添加useUnicodetruecharacterEncodingutf8无法连接数据库Access denied for user数据库密码不一致检查db.properties中的用户名密码是否和 MySQL 本地一致端口被占用Tomcat 默认 8080 端口被其他程序占用修改 Tomcat 端口或关闭占用端口的进程6.2 代码层面常见异常异常信息常见原因解决思路java.lang.NumberFormatException前端传入了非数字类型参数直接调用Integer.parseInt先做非空判断和正则校验再转型NullPointerException调用了空对象的方法或属性先从request.getAttribute获取对象是否为 null再访问属性ClassCastExceptionSession 或 request 存储数据后类型转换错误检查取出时的类型是否和存入时一致表单提交后价格精度丢失double类型存储价格改用BigDecimal日志无报错但页面空白JSP 页面 EL 表达式不解析或 JSTL 依赖缺失检查是否引入 JSTL 依赖查看 Tomcat 的 localhost 日志6.3 排查问题的一般步骤如果项目运行出现异常建议不要直接搜索报错信息而是按下面顺序排查先看 Tomcat 控制台完整日志找到第一个异常而不是只看最后几行。确认数据库连接 URL、用户名、密码是否正确能否在 Navicat 或命令行中正常连接。检查项目构建路径中是否有 JDK 和 Tomcat 运行时依赖。检查 JSP 页面的表单提交地址是否和 Servlet 注解中的地址一致。在 Servlet 的每一个可能出错的环节打印日志例如System.out.println(pageNum pageNum)。这一步做好很多问题根本不用问别人自己通过日志就能定位。7. 毕业设计文档与答辩建议7.1 LW 文档论文建议结构标题中的“LW 文档”是“论文文档”的意思。教材管理系统的论文一般可以按照以下章节组织绪论课题背景、研究意义、国内外现状、论文结构。相关技术介绍JSP、Servlet、MySQL、Tomcat、JDBC。需求分析功能性需求、非功能性需求、可行性分析。系统设计总体架构、功能模块划分、数据库设计。系统实现管理员模块、教材管理模块、领用模块的关键代码与截图。系统测试测试环境、测试用例、测试结果。总结与展望。写文档时最容易被导师挑问题的地方是“需求分析”和“数据库设计描述不够具体”。建议答辩前重新核对论文中的功能列表是否都能在系统里演示数据库表设计中的字段是否在代码中都用到了测试用例是否真实执行过。7.2 答辩前重点准备的问题毕业答辩中老师经常围绕下面几个点提问“为什么选择 JSP Servlet而不是 Spring Boot” 答作为课程综合实践jspm 方案能更清晰地展示 Java Web 基础原理理解 HTTP 请求处理过程Spring Boot 虽然开发效率更高但高度封装后不利于理解底层流程。“教材库存如何保证数据一致性” 答通过数据库事务扣减库存和新增领用记录在同一事务中完成使用UPDATE ... WHERE stock ?防止超领。“密码如何存储” 答不保存明文使用 MD5 加盐或 SHA-256 处理后再入库。这里不要只答“MD5”因为老师可能接着问“MD5 安全吗”如果答不好反而扣分。“有没有考虑分页查询性能” 答测试数据量小时使用LIMIT分页数据量增大后可以讨论索引优化例如在receive_record表的user_id和textbook_id上建立索引。“系统有哪些可以改进的地方” 答可以提到引入 Maven 管理依赖、使用连接池如 Druid、增加日志框架 Log4j2、前端使用 Vue 或 Bootstrap 优化体验。7.3 二次开发和功能扩展方向如果你的毕设不是简单的“增删改查”而是希望在项目中增加亮点以下几个方向比较实用批量导入教材信息通过 Excel 批量导数据减少手工录入。按班级统计领用明细以班级为单位生成学期教材费用汇总报表。库存预警当教材库存低于阈值在首页给出提醒。基于角色的访问控制区分管理员、班级负责人、普通学生不同角色看到不同菜单。操作日志记录记录每一次入库、领用、编辑操作增强系统可追溯性。这些功能不需要改变核心技术栈在原有 DAO、Servlet 结构上增加对应方法即可却能在论文和答辩中明显提升系统完整度。8. 从 jspm 到更现代的技术栈下一步学什么如果你现在是大三或大四已经完成了 jspm 教材管理系统接下来可以有两条路线深入学习。第一条路线继续走 Java Web 方向。在完成 JSP Servlet 项目后学习 Maven Spring Spring MVC MyBatis最后过渡到 Spring Boot。你会发现 Spring Boot 把很多重复配置自动化了但如果你理解了 Servlet、请求转发、重定向、Session、JDBC 这些底座概念学起来会轻松很多。第二条路线关注前端工程化。现代管理系统不再局限于 JSP 页面很多项目采用前后端分离后端提供 REST 接口前端使用 Vue 或 React。此时 jspm 项目中的 Servlet 就相当于后端接口JSP 页面被 HTML JavaScript 替代。理解了数据如何从数据库经过服务端传到页面再切换到前后端分离架构是比较平滑的。无论选择哪条路线你从 jspm 教材管理系统中练到的核心能力——业务需求建模、数据库设计、分层代码组织、异常排查、文档撰写——都是通用的。不要觉得 JSP 技术“过时”就轻视这个项目恰恰是因为它足够简单才能让你把注意力集中在设计和工程规范上。如果你正在做类似的毕业设计拿到源码后不要急着运行先花半小时把数据库建好、配置文件改好、权限理清然后把每个模块的请求链路串一遍。等你能不看笔记就向别人说清楚“一个教材从登录到入库到领用是如何完成的”这个项目你就真正掌握了。