资讯动态

基于JSP+Servlet+MySQL的教材管理系统开发实战

发布时间:2026/10/8 23:23:50 来源:尧图企业网站定制
简介一套基于Java与JSP及MySQL技术构建的Web学校教材管理系统完整项目源码主要面向计算机专业学生、毕业设计者以及初学Java Web开发的读者用于解决学校教材信息管理场景中的登录认证和教材数据维护问题。系统涵盖管理员身份验证、教材入库、信息修改、条件检索与删除等模块由JSP页面负责视图展示Java类处理业务逻辑MySQL完成数据存储清晰呈现了传统MVC分层模式在真实项目中的落地方式。压缩包共81个文件包括26个JSP页面、11个Java源文件、22个class编译文件、10个CSS样式、8个XML配置和1个SQL脚本整体大小3.91MB可配合Tomcat直接部署运行。目前已有1229人学习下载借助该资源可以查看完整源码、导入数据库并实际操作快速理解表单提交、数据库连接、SQL语句等Java Web开发关键环节项目目录规范、层次清楚很适合作为课程设计或框架学习前的参考项目。1. 教材管理系统解决什么问题从纸质台账到在线流转的完整闭环每学期开学教材科抱着一叠 Excel 和手写申请单跟各院系对账教师报上来的教材需求靠人肉汇总学生领书签字后留一堆纸质回执。这套 JavaJSPMysql 实现的 Web 学校教材管理系统就是把教材入库、订购申领、发放回收、库存统计全部搬到浏览器里处理。管理员维护书目与库存教师提交需求单学生按授权领书每一笔流转都带操作人和时间记录落在 MySQL 里可回溯。技术栈是典型的 Servlet JSP JDBC不依赖 Spring 全家桶一个 Tomcat 加一个 MySQL 就能跑起来。这套系统既适合做课程设计和毕业设计的可运行方案也适合中小学校快速落地内部管理系统。如果你是刚接触 Java Web 的初学者想知道这个经典结构怎么一步步跑通或者需要一个能展示完整业务闭环的小项目顺着下面这条链路读完你就能在本地把它复现出来。2. 技术选型与项目结构为什么 JSPServlet 依然适合这类系统先立个共识这不是一个需要高并发、微服务、分布式缓存的项目它的真实负载是几百个教师学生同时在线查书目、提交申领单峰值出现在每学期开学的头两周。用 Spring Boot 当然能做但引入构建工具、自动配置、内嵌服务器之后初学者往往被框架怎么装配带走反而忽略了教务业务本身。而 JavaJSPMysql 这套组合Tomcat 本来就天然识别 JSPServlet 负责路由JDBC 直接读写 MySQL整个请求链路短出了错在日志里一眼能找到 Servlet 和 SQL 的位置。我在实际带课程设计和做小单位内部系统时经常给团队定这条选型线没有外部访问压力、不需要前后端分离、团队里 Java 基础没那么重就老老实实用这套别为了简历上写个Spring Cloud把项目拖进依赖泥潭。2.1 JSPServlet 分层为什么这套组合在教材管理里够用JSP 的本质是能写 Java 代码的 HTML 模板但它不该承载业务逻辑。常见的做法是把代码分成三层JSP 页面只管展示和表单交互Servlet 接收请求、调用服务、决定跳转JDBC/DAO 层负责拼 SQL 和执行数据库操作。请求从浏览器发到 TomcatTomcat 根据 web.xml 或 WebServlet 注解找到对应的 ServletServlet 从 request 里取参数调 DAO 查询 MySQL把结果放进 request 或 session最后 forward 到 JSP 渲染成 HTML。这个流转顺序清晰排错时只要盯住哪个 Servlet 被调了、它查到的数据是什么、JSP 里 EL 表达式有没有写错三件事。相比 Spring MVC它少了 IOC 容器和注解扫描的抽象层但也意味着你不需要理解一堆自动配置原理。教材管理系统的用户表、教材表、申请单、库存流水表都是单一对象、少量 Join用最原始的 PreparedStatement 就能轻松驾驭。如果你拿这个项目去面试反而能借机把 Servlet 生命周期、JSP 九大隐式对象、JDBC 事务边界这些基础问题讲得比只会 Spring Boot 的人更清楚这正好是 Java 面试题里常被追问的底层知识点。当然边界也要说清楚JSP 不适合做大量异步交互。我的经验是页面里的下拉联动、数量校验、按班级批量选择教材用一点 jQuery 和 Ajax 调用独立的 Servlet 就好不要把所有交互逻辑都塞进 JSP 的脚本片段里。教材管理员最常用的是选教材 → 填数量 → 提交这样的同步表单这类流程用 JSP 表单 action 指向 Servlet 天然顺手完全没必要上 Vue。真要在页面里做复杂的联动比如选择了专业之后自动带出课程列表可以在 JSP 里放一个隐藏域用 jQuery 监听下拉框 change 事件再 Ajax 请求一个返回 JSON 的 Servlet拿到数据后拼option。这个套路对新手最友好比直接写 JSP 表达式嵌套数据库查询干净得多。2.2 项目目录、Tomcat 环境与依赖导入先搭出能跑的骨架我一般先按下面的目录结构建项目不管是 Eclipse 的动态 Web 项目还是 IDEA 的 Web Application 项目本质都是这么一层textbook-manager/ ├── src/ │ ├── com/school/textbook/ │ │ ├── dao/ # JDBC 数据访问BookDao、UserDao、ApplyDao │ │ ├── entity/ # User、Book、Apply、StockLog │ │ ├── servlet/ # LoginServlet、BookServlet、ApplyServlet │ │ └── util/ # DBUtil 连接工具、字符编码过滤器 │ └── (配置文件 db.properties) ├── WebContent/ # 或 src/main/webapp │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ # mysql-connector-java.jar 放这里 │ ├── css/ js/ │ ├── login.jsp │ └── book/list.jsp └── build/ # 编译输出关键点是 WEB-INF/lib 下的 JDBC 驱动。用 Maven 的话在 pom.xml 里加 mysql-connector-java 依赖坐标大致是mysql:mysql-connector-java:8.0.33如果项目是在没有外网的机房或内网环境里做更省事的做法是把驱动 jar 直接复制到 WebContent/WEB-INF/lib 下Tomcat 会自动把它加进 classpath。不要图省事只放在本机 IDE 的运行环境里等部署到另一台机器时会发现 Tomcat 报 ClassNotFoundException。运行环境上JDK 8 Tomcat 8.5/9.0 MySQL 5.7 或 8.0 是稳的。无论你是用 rpm 安装 mysql 还是解压安装包装完先执行mysql -u root -p能登录再用 status 命令确认服务字符集再交给项目。MySQL 8.0 要特别注意驱动名和连接参数这个我放到 3.3 节专门讲。Tomcat 端口冲突很常见具体排法在第 5 章避坑里再提这里只需要知道 catalina.bat run 能看实时日志就够了。Servlet 的写法现在主流是注解驱动直接在类上写WebServlet(/book/list)省掉 web.xml 里一长串servlet和servlet-mapping。老项目里用 web.xml 映射也照样能跑两种方式本质是一样的。我建议新写代码用注解因为少文件少错遇到要配置初始化参数的场景再回 web.xml。JSP 页面里尽量用 JSTL 标签库的c:forEach、c:if和 EL 表达式不要写% %脚本片段否则页面塞满 Java 代码后期连改个表头都胆战心惊。JSTL 需要两个 jarjstl-api 和 jstl-impl同样放进 WEB-INF/lib页面顶部用% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %引入。这个细节决定了你的 JSP 项目维护性也是我评审代码时最在意的点。关于 servlet-api 的版本归属也值得敲一下黑板Tomcat 9 及以下对应 javax.servlet 包名Tomcat 10 开始改成 jakarta.servlet。如果你在 IDEA 里新建项目时默认用了 Tomcat 10旧代码里的javax.servlet.http.HttpServlet就会编译报错。教材管理系统这种项目我一般锁定 Tomcat 9 javax 坐标网上现成代码几乎都是 javax 写法踩坑最少。日志方面项目里用 JDK 自带的 java.util.logging 或者 log4j 1.x 简单配置就行不要为了规范引入 log4j2 全家桶XML 配置和异步 Appender 对这个小项目完全是负担。最后补一句部署视角项目编译后打成 war 包丢进 Tomcat 的 webapps 目录就能运行IDEA 里也可以配置 Artifact 直接部署。war 包的名字就是访问路径的前缀比如 textbook-manager.war 对应http://localhost:8080/textbook-manager/。很多新手在本地能打开页面一到导出 war 部署就 404多半是没记这个前缀。第六章我会给一份上线前的验证清单专门对这类问题做回归。3. 建库建表与 JDBC 连接四张表跑通教材管理主线数据库是整个系统的地基也是最容易返工的一层。我见过不少教材管理系统项目边写代码边加字段最后表结构乱到 SQL 都不敢改。正确的顺序是先把用户、教材、申请、库存流水四张表设计好再动手写 Java 代码。下面给出一个能直接运行的基础版本字段按够用、不花哨的原则设计。3.1 建库建表user、book、apply、stock_log 四张核心表CREATE DATABASE IF NOT EXISTS textbook_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE textbook_db; -- 用户表管理员、教师、学生共用一张表用 role 区分 CREATE TABLE user ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(50) NOT NULL, role TINYINT NOT NULL DEFAULT 0, -- 0 管理员 1 教师 2 学生 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_role (role) ) ENGINEInnoDB; -- 教材表库存 stock 设置默认值为 0缺数据时不至于读到 null CREATE TABLE book ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(100) NOT NULL, isbn VARCHAR(20) NOT NULL UNIQUE, author VARCHAR(50), publisher VARCHAR(80), price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, category VARCHAR(50), threshold INT NOT NULL DEFAULT 20, -- 库存预警阈值 update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_book_cat (category), INDEX idx_book_stock (stock) ) ENGINEInnoDB; -- 申领单教师提交、管理员审核发放 CREATE TABLE apply ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, book_id INT UNSIGNED NOT NULL, user_id INT UNSIGNED NOT NULL, quantity INT NOT NULL DEFAULT 1, status TINYINT NOT NULL DEFAULT 0, -- 0 待审核 1 已发放 2 已退回 apply_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME, audit_by INT UNSIGNED, CONSTRAINT fk_apply_book FOREIGN KEY (book_id) REFERENCES book(id), CONSTRAINT fk_apply_user FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB; -- 库存流水入库、出库、退库都在这里记账只增不改 CREATE TABLE stock_log ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, book_id INT UNSIGNED NOT NULL, change_type TINYINT NOT NULL, -- 1 入库 2 发放 3 退库 change_qty INT NOT NULL, before_stock INT NOT NULL, after_stock INT NOT NULL, operator_id INT UNSIGNED, remark VARCHAR(200), create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_log_book_time (book_id, create_time) ) ENGINEInnoDB;四张表的职责很明确。user 表把管理员、教师、学生统一成一张表用 role 区分避免了建三张表然后再做三套登录的冗余用户名加了唯一索引登录时能直接命中索引。book 表里 stock 默认值为 0threshold 是库存预警阈值后面生成补货清单时直接WHERE stock threshold就能筛出来。apply 表是业务核心外键挂到 book 和 userstatus 字段驱动待审核→已发放的状态流。stock_log 只追加不修改保证了任何人都不能事后改账这是教材管理系统审计价值的根基。字段类型上的取舍价格用 DECIMAL(10,2) 而不是 FLOAT因为浮点算钱会出现 0.10.2 不精确的问题这在 MySQL 里属于经典考点数量和库存用 INT 并加上 UNSIGNED挡住负库存写入时间和外键都加了索引后续统计某教材一个学期发放了多少本某个班级领了哪些书时这些索引直接决定查询能不能走覆盖索引。角色用 TINYINT 而不是字符串常见面试题里会问为什么——TINYINT 只占 1 字节、比 VARCHAR 省空间且比较快。注意到一个细节外键约束在真实小系统里未必是标配。我这个方案保留了外键但生产环境常见的做法是去掉 FK由业务层保证完整性避免并发写入时外键校验成为瓶颈。教材管理这种量级用 FK 没问题你只要知道它的存在是为了防脏数据就行。如果想按专业或者年级维度再拆班级表可以扩展一个 class 表并放到 user.id 上做关联这里主流程不展开。3.2 建表索引与字符集选择编码、精度和排序的坑数据库层面最容易被忽略的是字符集。表统一用 utf8mb4 才能正确存中文、生僻字和 emoji只建 utf8 会遇到 4 字节字符插入报错。连接字符串里也必须明确 characterEncodingutf8否则 MySQL 5.7 默认 latin1、8.0 默认 utf8mb4 的差异会让你在 Java 端看到????。排序时 MySQL 支持的 utf8mb4_general_ci 已经够用想要更精确的拼音排序可以换成 utf8mb4_unicode_ci代价是性能略低。这里顺带提一句 MySQL 排序ORDER BY apply_time DESC走不了索引时会出现 filesort小数据量无感数据过万后可考虑在 create_time 上加索引SQL 里不要对索引列套函数比如DATE(apply_time)CURDATE()会让索引失效。建表后的第一个动作是插入一个默认管理员。密码不能明文存用 MD5 虽然不安全但在教学项目里仍很常见我建议至少用 SHA-256 或 BCrypt教学演示里图省事可以用 MD5但要在文档里标注仅演示。下面这条 SQL 初始化一个账号密码字段我写入 SHA-256 后的占位值实际开发中注册功能里要用MessageDigest或 jBCrypt 生成摘要再入库。-- 默认管理员账号 admin / 初始密码请自行替换成哈希值 INSERT INTO user (username, password, real_name, role) VALUES (admin, 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8, 系统管理员, 0);上面密码占位是 SHA-256 后的默认值登录时对输入做同样的哈希再比对。这么做虽然比 MD5 强但仍属于可爆破的哈希算法一线系统应该上 BCrypt不过教材管理系统课程设计里用 SHA-256 已经比多数示例项目规范了。你把它替换成自己生成的摘要再投入使用不要把占位哈希直接带到生产环境。索引不是越多越好这一点初学者特别容易走极端。我的经验是只在 WHERE 条件高频列、外键列、排序列上加索引比如 book.category、apply.user_id、stock_log.create_time。写操作频繁的列加索引反而拖慢 insert 和 update教材表每天更新库存stock 列的索引更多是服务管理端按库存排序筛选的查询量级上去以后才值得权衡。建立索引的通用准则是区分度高的列优先比如 isbn 适合status 这种只有 0/1/2 三值的列加索引意义很小优化器可能直接扫表。3.3 JDBC 连接串与驱动选择MySQL 5.7 与 8.0 的最小配置JDBC 连接是整个 Java 项目里第一个玄学环节换一个 MySQL 小版本你之前跑通的代码可能就起不来了。MySQL 5.7 年代常用驱动是 com.mysql.jdbc.Driver到了 MySQL 8.0 官方把类名改成了 com.mysql.cj.jdbc.Driver连接 URL 也必须带上时区和 SSL 参数。如果你下载的是 mysql 8.0 安装包却继续写老驱动类名Tomcat 启动时直接报 ClassNotFoundException这是最常见的第一道坎。// DBUtil.java —— 数据库连接工具类 package com.school.textbook.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { static { try { // MySQL 8.0 用 com.mysql.cj.jdbc.Driver5.7 及以前可用 com.mysql.jdbc.Driver Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { String url jdbc:mysql://localhost:3306/textbook_db ?useSSLfalseserverTimezoneAsia/Shanghai characterEncodingutf8allowPublicKeyRetrievaltrue; return DriverManager.getConnection(url, root, 你的密码); } }这段代码能跑通的前提有三点驱动类名跟 MySQL 版本对得上url 里 useSSLfalse 直接绕开本地开发环境常见的 SSL 握手报错allowPublicKeyRetrievaltrue 解决 MySQL 8.0 首次连接时Public Key Retrieval is not allowed的报错serverTimezoneAsia/Shanghai 解决日期时间差八小时的问题。密码不建议硬编码更稳妥的是读取 db.properties 文件用ClassLoader.getResourceAsStream(db.properties)加载这样部署时改配置不用重新编译。如果项目用到了连接池常见选择是 Alibaba Druid 或 Apache DBCP。把 druid 放进 WEB-INF/lib初始化一个 DruidDataSource核心参数是 initialSize、maxActive、minIdle 和 validationQuery。对教材管理系统来说默认 5 个初始连接、20 个最大活跃连接足够validationQuery 可以用SELECT 1。用连接池最大的好处是避免每次 Servlet 请求都新建物理连接否则高峰时数据库连接数会瞬间打满报 Too many connections。连接池使用中还有一个容易踩的坑从池里拿到的连接用完必须 close这里 close 不是真的断开而是把连接归还给池只拿不还池很快被耗尽数据库后端也会积累一堆 Sleep 连接。4. 核心链路实现登录、分页、事务处理从代码量上看这个项目真正的复杂度在事务和分页。登录只是一个 Servlet 加一张表查询教材列表是查询加翻页入库和发放则要同时改书库库存和追加流水任何一步失败都必须回滚。这一章我直接给出可运行的骨架代码你照着搭一遍就能理解整条链路。4.1 数据库连接工具类与实体类把 JDBC 固定样板抽出来实体类是数据载体不写任何数据库操作。字段名与表列名保持一致这样 JSP 里用 EL 表达式${user.realName}就能直接取到值。下面是一个简化版 User 实体实际代码中还需要补全 getter、setter 和无参构造器// User.java package com.school.textbook.entity; public class User { private int id; private String username; private String password; private String realName; private int role; // 本类省略 getter/setter实际代码中请补全 }// BookDao.java —— 分页查询教材列表 package com.school.textbook.dao; import com.school.textbook.entity.Book; import com.school.textbook.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class BookDao { public ListBook findPage(String keyword, int offset, int limit) throws SQLException { String sql SELECT id, book_name, isbn, author, publisher, price, stock, category FROM book WHERE book_name LIKE ? ORDER BY id DESC LIMIT ?, ?; ListBook list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setInt(2, offset); ps.setInt(3, limit); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Book b new Book(); b.setId(rs.getInt(id)); b.setBookName(rs.getString(book_name)); b.setIsbn(rs.getString(isbn)); b.setAuthor(rs.getString(author)); b.setPublisher(rs.getString(publisher)); b.setPrice(rs.getBigDecimal(price)); b.setStock(rs.getInt(stock)); b.setCategory(rs.getString(category)); list.add(b); } } } return list; } }这段 DAO 展示了两个关键习惯一是用 PreparedStatement 而不是字符串拼接 SQL防止 SQL 注入这也是面试里必问的为什么不用 Statement二是 try-with-resources 自动关闭 Connection、PreparedStatement、ResultSet省掉在 finally 里手写 close 的老样板。LIMIT ?, ? 的两个参数分别是偏移量和每页条数offset 计算公式是(currentPage - 1) * pageSize。排序用 ORDER BY id DESC 能走主键索引新录入的教材排在前面。分页还需要一条 COUNT 语句算出总页数常见的写法是SELECT COUNT(*) FROM book WHERE book_name LIKE ?然后用(total pageSize - 1) / pageSize算出总页数这个是向上取整的技巧保证最后一页内容不满一页时也能正常显示页码。4.2 登录与权限拦截Session 判断和 Filter 的 10 行代码登录逻辑虽然简单但有不少细节决定系统能不能用。参数读取前必须设置请求编码否则中文用户名直接乱登录成功后的跳转要区分 forward 和 sendRedirect前者浏览器地址栏不变、刷新会重复提交后者会重新发起一次 GET 请求适合登录成功这种场景。// LoginServlet.java package com.school.textbook.servlet; import com.school.textbook.dao.UserDao; import com.school.textbook.entity.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { private UserDao userDao new UserDao(); 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); User user userDao.login(username, password); if (user null) { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /book/list); } }登录失败用 forward 回到 login.jsp并能把错误消息通过 request attribute 带回去登录成功把 user 对象放进 session后续 JSP 用${sessionScope.loginUser.realName}显示当前用户。sendRedirect 里拼接了getContextPath()这是防止项目部署在非根路径下跳转 404 的关键。登录之后的页面需要权限拦截。一个简单的 Filter 就能完成不需要引入权限框架// AuthFilter.java package com.school.textbook.servlet; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.*; import java.io.IOException; WebFilter(/*) public class AuthFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 放行登录接口和登录页以及静态资源 if (uri.endsWith(/login) || uri.endsWith(/login.jsp) || uri.contains(/css/) || uri.contains(/js/)) { chain.doFilter(req, resp); return; } // 未登录用户全部重定向到登录页 if (request.getSession().getAttribute(loginUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }这个 Filter 拦截了所有请求但把登录接口、登录页、静态资源放行。注意放行条件里既匹配了uri.endsWith(/login)也要匹配uri.contains(/css/)否则浏览器加载样式和脚本也被拦回登录页页面会变得光秃秃一片。角色权限可以在 Filter 里继续扩展如果 Session 里 user 的 role 等于 1教师访问/admin/开头的路径就返回 403这是区分管理员和普通用户最直接的做法。对教材管理系统来说Filter 挡到必须登录这一层就够了按钮级别的权限控制放到 JSP 里用c:if test${sessionScope.loginUser.role 0}来控制显隐。4.3 入库与发放的事务setAutoCommit 与行锁的配合教材发放是最容易出现账实不符的操作学生提交申领单管理员审核通过扣减库存的同时写 stock_log 流水。如果扣库存成功但流水写失败或者库存扣成负数整个系统就失去可信度。因此这一段必须用事务// GrantService.java 中发放教材的核心逻辑 public void grant(int applyId, int operatorId) throws Exception { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 查询申领单并锁定对应教材行防止并发超发 String sqlApply SELECT book_id, quantity, status FROM apply WHERE id? FOR UPDATE; // 执行后校验 status 必须为 0 待审核 // 2. 扣减库存用 stockquantity 条件原子判断 String sqlStock UPDATE book SET stockstock-? WHERE id? AND stock?; int rows executeUpdate(conn, sqlStock, quantity, bookId, quantity); if (rows 0) { throw new RuntimeException(库存不足); } // 3. 写流水 String sqlLog INSERT INTO stock_log(book_id, change_type, change_qty, before_stock, after_stock, operator_id, remark) VALUES (?, 2, ?, ?, ?, ?, 发放); // 4. 更新申领单状态为已发放 String sqlUpdate UPDATE apply SET status1, audit_timeNOW(), audit_by? WHERE id?; executeUpdate(conn, sqlUpdate, operatorId, applyId); conn.commit(); } catch (Exception e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }这是整个系统最关键的一处代码。这里用了三条规则第一FOR UPDATE把 select 命中的 book 行锁住并发请求同时发放同一本教材时后到的请求会等前一个事务提交避免超发第二扣减库存的 UPDATE 语句带上AND stock?如果变更行数为 0 就说明库存不够直接抛异常回滚第三所有写操作共用同一个 Connection在 commit 之前不释放保证扣库存-写流水-更状态要么全部成功要么全部不生效。关闭连接时还要把 setAutoCommit 恢复 true否则连接池里的连接下次会被带着未提交事务的状态复用这是连接池场景最常见的事务翻车点。事务执行完后如果在日志里看到 Incorrect DECIMAL value 或者 Cannot add or update a child row 这类异常优先检查前台表单是否有空字符串传了进来、外键关联的 id 是否存在。把异常信息先打出来再谈优化不要一上来就猜 JSP 写错。业务侧还有一个容易被忽略的点入库操作同样要走事务不仅 insert 进 book 表还要在 stock_log 里记一条 change_type1 的流水。入库单号、供应商、采购日期这些字段如果以后要扩展可以单独建一张 purchase 表但最小可用版本里把入库流水合并到 stock_log 就够用。5. 避坑指南JSPMysql 项目最常见的 5 个翻车现场这一章是给所有照着前面步骤搭环境的人准备的。每个坑都按现象 → 原因 → 解决的顺序写排序依据是实战里出现的频率。第一坑MySQL 8.0 下报 ClassNotFoundException: com.mysql.jdbc.Driver。现象Tomcat 启动后访问页面后台日志抛出 ClassNotFoundException指向 com.mysql.jdbc.Driver。原因jar 包里已经没有旧类名。MySQL 8.0 的驱动正式类名是 com.mysql.cj.jdbc.Driver旧类名虽然保留了继承关系但没有在 SPI 配置里注册Class.forName 还是找不到。解决把代码里的类名改成 com.mysql.cj.jdbc.Driver同时确认 mysql-connector-java 的版本是 8.x而不是老的 5.x jar。如果只是把 5.x jar 换成 8.x还要同步调整连接 URL参考 3.3 节。同样的报错还可能来自 WEB-INF/lib 下驱动包没拷进去先检查 Tomcat 部署目录的 lib 里有没有 jar再检查代码里的类名。第二坑中文全部变成。现象浏览器页面上表单提交的教材名称存进 MySQL 后显示为问号或者从数据库里查询出来的中文乱码。原因字符集在四个环节里有一处不一致。页面编码、请求解码、连接串 characterEncoding、数据库表字符集必须全部是 UTF-8/utf8mb4。最常见的是连接串少了 characterEncodingutf8以及建表时没有指定 DEFAULT CHARSET utf8mb4。解决统一四段配置。JSP 顶部写% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %Servlet 读参前执行 request.setCharacterEncoding(UTF-8)连接 URL 加 characterEncodingutf8建库建表用 utf8mb4。如果你用了过滤器来做编码一定要确认过滤器在业务 Servlet 之前执行并且调用 chain.doFilter 前已经设置好编码。这个坑的病根是编码链路长建议一套配置从头复制到尾不要一处在页面改一处在 Java 改。第三坑点击菜单 404JSP 页面路径对不上。现象登录成功跳转到 /book/list浏览器地址栏显示 404或者直接访问 list.jsp 却报资源不存在。原因Servlet 地址写成了 /book/list而 book 表对应的 JSP 放在 /book/list.jsp这两种资源之间并没有自动关联。更隐蔽的是项目 ContextPath 不为空直接写 /book/list 会丢掉应用上下文前缀。解决确认 pageContext.request.contextPath 或 request.getContextPath() 拼接 URL跳转用response.sendRedirect(request.getContextPath() /book/list)。JSP 一般都放到 WebContent 下不带 WEB-INF 前缀如果要强制安全访问可以塞进 WEB-INF但那时 JSP 不能直接走 URL必须通过 Servlet forward 进入。遇到 404 先别急着改代码在浏览器开发者工具里看请求的完整地址确认前缀、Servlet 路径、文件路径三段都对得上。第四坑Tomcat 8080 端口被占用启动报 Port busy。现象Eclipse/IDEA 启动 Tomcat 失败日志提示 Port 8080 required by Tomcat v9.0 Server at localhost is already in use。原因Windows 上经常是其他开发软件占了 8080或者上一次 Tomcat 异常退出残留了 Java 进程。解决netstat -ano | findstr 8080找到占用进程 PIDtaskkill /PID xxx /F清理或者把 Tomcat 的 HTTP/1.1 端口改成 8081。改端口后访问地址要同步改这个坑属于一看就会、忘了就翻车那种建议优先杀进程而不是改端口免得后面部署时上下文路径又错位。Linux 服务器上则是lsof -i:8080查进程确认是不是你自己上次启动的 Tomcat 没关干净。第五坑发放教材时库存没扣、流水却写了或者反过来。现象管理员点完通过审核后列表里流水出现了一条发放记录但 book 表的 stock 没有变化或者库存已经减了但流水里找不到记录。原因两个操作用了两条不同的数据库连接每个连接各自自动提交没有整体事务。最常见是手动分开写了两个 DAO 方法每个方法内部 getConnection() 然后自动提交。这就是事务边界被拆散导致的账实不符。解决把扣库存和写流水收敛到 4.3 节同一个 Connection 的事务里而不是各调各的 DAO。代码写好之后人为在扣库存后抛一个 RuntimeException观察流水是否被正确回滚这一点就是事务生效的验证方法。顺带一提检查 MySQL 里事务是否真的开启可以执行SHOW ENGINE INNODB STATUS看当前事务列表或者用SELECT * FROM information_schema.innodb_trx看有没有长时间没提交的事务。这五个坑不是全部但覆盖了项目从零到能跑的大多数报错。真遇到日志里看不懂的异常先把完整堆栈贴到搜索引擎把异常类名 你的 MySQL 小版本 Tomcat 版本放在一起搜答案往往比翻书快得多。写代码之前把 MySQL 的安装配置教程和版本差异先看一遍能避开一半以上的环境问题这属于过来人的血泪经验。6. 进阶库存预警、报表与上线前的验证顺序6.1 库存预警与统计的一张 SQL主流程跑通之后最值得加的两个功能是库存预警和学期发放统计。库存预警直接对 book 表做一次查询就行不需要额外建表-- 低库存教材清单 SELECT book_name, isbn, stock, threshold FROM book WHERE stock threshold ORDER BY stock ASC;把 threshold 字段做成在教材编辑页面可调管理员就能按低于 20 本提醒我这种节奏来管理。学期发放统计要稍微 join 一下流水表-- 某学期发放教材汇总 SELECT b.book_name, SUM(l.change_qty) AS total_out FROM stock_log l JOIN book b ON l.book_id b.id WHERE l.change_type 2 AND YEAR(l.create_time) ? GROUP BY b.book_name ORDER BY total_out DESC;这个 SQL 的?参数可以在 Servlet 里传入学年用来回答今年上半年哪本教材发出去最多。数据量不大时直接跑没问题数据量大了记得让 create_time 上的索引生效。如果想做 Excel 导出常见做法是用 Apache POI在 Servlet 里生成 .xlsx 然后设置响应头Content-Disposition: attachment让浏览器下载。6.2 上线前的验证顺序与回归清单上线前我习惯按业务链路完整点一遍顺序是初始化数据 → 管理员登录 → 新增教材 → 入库 → 教师提交申领 → 管理员发放 → 核对库存和流水 → 换角色验证权限。每一步都要实际点过去不要只看代码写对了。回归清单里最值得盯的三处登录成功后跳转的 URL 是否带了上下文前缀发放教材后 book.stock 和 stock_log 里 before_stock、after_stock 是否对得上换个浏览器访问受限页面是否被 Filter 正确踢回登录页。另外把 war 包部署到一台干净的 Tomcat 上跑一遍能暴露所有只在 IDE 里能跑的问题。带过这么多届课程设计我最大的教训是先跑通再优化。很多人一上来就纠结事务和索引结果环境还没搭好就放弃了。我自己第一次做这种系统时数据库连接串少了 serverTimezone查时间差了 8 小时愣是排查了很久才发现。后来养成的习惯是每写完一个功能马上用浏览器点一遍完整链路而不是攒着到最后一起测。希望这套从表结构到事务的拆解能帮到你至少让你在复现这个 JavaJSPMysql 的 Web 学校教材管理系统时少走我当年走过的弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑