ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaWeb教务管理系统完整案例:Servlet+JSP+MySQL从零部署

JavaWeb教务管理系统完整案例:Servlet+JSP+MySQL从零部署 简介面向JavaWeb初学者的教务管理系统完整项目基于MVC与DAO模式整合Servlet、JSP、Filter及MySQL数据库覆盖登录、找回密码、修改与注销学生成绩查询、选修与考级报名、学籍信息修改教师与教务员信息维护以及管理员管理等典型模块适合课程设计或毕业设计参考。运行环境为Windows、Tomcat 7.0.84与JDK1.8数据库可通过Navicat导入SQL快速搭建。资源共223个文件含82个Java源文件与对应class文件、38个JSP页面以及XML配置、SQL脚本和jar依赖包压缩包仅2.6MBEclipse工程结构清晰。已有11656人学习下载。解压后先阅读README可快速启动系统项目预留扩展空间便于继续完善功能初学者可对照源码理解分层开发思路并在此基础上添加新的业务模块。1. 教务管理系统 javaweb 项目为什么这个老案例至今值得完整做一遍刚学完 Servlet、JSP 和 JDBC 的人十有八九卡在同一个问题上知识点都学过但不知道一个完整项目怎么串起来。教务管理系统 javaweb 项目恰好是来补这块短板的——它业务清晰、需求固定、表关系直观又能一次性覆盖登录会话、权限区分、CRUD、分页查询和 MySQL 落库这些 JavaWeb 高频考点。这套东西适合三类人拿它当课设或毕设基础、准备面试前用来回顾 Servlet 生命周期与 Filter 的工作方式、或者给团队做内部后台练手。下面以“JavaWeb 项目完整案例 MySQL”为方向展开从技术选型讲到建表再落到代码骨架和部署排错。跟着完整走一遍你不只是“看懂了”而是真能交付一个能跑起来的系统。2. 技术栈与工程骨架先锁版本再写代码2.1 为什么这个教务管理系统继续走 Servlet JSP教务管理系统的核心操作是表单提交、列表查询、权限入口恰好都是 JavaWeb 最简单的三个动作。常见实现方案有三类纯 ServletJSP、Spring Boot、以及前后端分离。我一般会建议新手做 javaweb 项目时优先选纯 ServletJSP 路线原因不是 Spring Boot 不好而是这套项目的目的本来就是让你看见底层请求怎么进 Servlet、Session 怎么存登录状态、Response 怎么把 JSP 渲染出去。Spring Boot 把这些全部打包成了“约定”出了问题就是个黑匣子反而不利于复习 Servlet 和 Filter 这些面试必考点。当然用 JSP 不等于回到十年前。在实际工程里我会把 JSP 只放在webapp/WEB-INF/views下Servlet 充当控制器Service 层处理业务逻辑DAO 层访问数据库entity 对应表结构。这样分层的好处是以后你从这个项目迁往 Spring Boot只需要把控制器换成 Controller、把 DAO 换成 MyBatis Mapper业务逻辑基本不用动。如果你手头有黑马 javaweb 笔记它推荐的经典分层也正是这套跟着笔记里的数据核对业务场景会发现页面、表、接口之间的对应关系非常工整。提示这个技术方案适合并发不高、年请求量在十万以内的内部系统。如果你做的是全校抢课这类高并发场景请直接考虑 Spring Boot Redis 消息队列不要让纯 Servlet 硬扛。2.2 Maven 依赖与最简 pom.xml三个版本坑要先避纯 Servlet 项目不需要 Spring 那一大串依赖但也不能空手写。我会在 pom.xml 里放五类东西servlet-api、jsp-api、jstl、mysql-connector-java、连接池。连接池我习惯用 dbcp2它在 Tomcat 环境里兼容性最好。下面是一份能在 JDK8 Tomcat9 MySQL8 上直接跑的最小 pom.xmlproject xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.edu/groupId artifactIdedu-admin/artifactId version1.0.0/version packagingwar/packaging properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties dependencies !-- Servlet 与 JSP API 由 Tomcat 提供必须 provided -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 8 必须用 8.x 驱动旧驱动类名不兼容 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.30/version /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-dbcp2/artifactId version2.9.0/version /dependency /dependencies /project这里有两处参数要特别注意。第一servlet-api 和 jsp-api 的 scope 必须是provided因为 Tomcat 自带这两套 API如果打进 war 包会与 Tomcat 的类冲突轻则报警告重则启动报java.lang.VerifyError。第二mysql-connector-java 用 8.x 时驱动注册类名是com.mysql.cj.jdbc.Driver旧版的com.mysql.jdbc.Driver在驱动 jar 里只保留兼容壳能跑但会打出一大段 deprecation 日志不致命却很碍眼。如果你在 JDK8 下照抄这份依赖版本冲突问题几乎为零。依赖解析完再顺手把 war 插件加上避免 IDEA 在打包阶段漏掉 web.xml 或资源文件build finalNameedu-admin/finalName plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-war-plugin/artifactId version3.4.0/version /plugin /plugins /buildfinalName 会决定生成的 war 包名也间接决定部署到 Tomcat 后的默认访问路径。比如这里生成edu-admin.war默认根路径就是http://localhost:8080/edu-admin/如果你希望访问/edu要么改 finalName要么在 IDEA 的 Deployment 配置里手动改 Application context。2.3 IDEA 运行 javaweb 项目的三处配置IDEA 里跑 javaweb 项目最容易翻车的地方不是代码而是 Artifact 配置。我当年第一次上手在这里卡了整整一个晚上。核心要确认三处。第一Project Structure → Artifacts 里要有一个类型为Web Application: Exploded的 war 包而不是打包好的归档 war。为什么归档 war 每次都要整包拷到 Tomcat 再解压调试时改一行代码要等很久exploded 形式直接把编译后的 classes 和 webapp 资源热部署到 Tomcat 的部署目录IDEA 里保存后按 CtrlF10 就能看到新效果。这是 javaweb 项目调试体验差距最大的一步。第二Run/Debug Configurations 里的 Application context 要和项目访问地址一致。我会统一把它设置为/edu。这样在后端代码里重定向时只要用request.getContextPath()拼接地址就不会出现“项目里写死/edu、换台机器部署路径又变了”的尴尬。第三Deployment 页签里要确认把 Maven 依赖的 jar 放进WEB-INF/lib。否则你明明在 pom 里写了 mysql-connector运行时 Tomcat 却报ClassNotFoundException: com.mysql.cj.jdbc.Driver。这属于“编译能过、运行就挂”的典型配置问题也是最容易被忽略的一环。2.4 目录结构一个能应对“老师要扩展功能”的骨架教务管理系统有个特点管理员、教师、学生三个角色看到的功能不一样。如果所有代码都塞进一个 controller 大包后面每加一个模块都要在几千行里找方法。我一般会使用下面的分层目录edu-admin/ ├── pom.xml └── src/main/ ├── java/com/edu/ │ ├── controller/ # Servlet只做参数接收和页面跳转 │ ├── service/ # 业务逻辑比如登录校验、成绩约束 │ ├── dao/ # JDBC 数据访问 │ ├── entity/ # Student、Teacher、Course 等实体 │ └── util/ # DBUtils、StringUtils 等工具 ├── resources/ │ └── db.properties # 数据库连接配置 └── webapp/ ├── WEB-INF/ │ ├── web.xml # Filter/ Servlet 注册如果用注解可留最小配置 │ └── views/ # 统一放 JSP禁止直接访问 ├── css/ ├── js/ └── login.jsp # 登录页允许直接访问这个结构的核心规则是controller 里不写 SQLdao 里不写页面跳转service 只管业务和事务。很多 javaweb 项目烂尾不是因为功能难而是因为所有代码都堆在 Servlet 的 doPost 里三个月后没人敢改。你做课设时把这条规则写进 README答辩时还能多讲一个“分层设计”的亮点。3. 数据模型先行教务系统的五张表与 MySQL 落地3.1 教务系统的核心实体关系教务管理系统 javaweb 项目的业务场景很固定管理员维护基础数据教师录入成绩学生查看课表和成绩。围绕这个流程最少需要五张表student、teacher、course、course_student、score。它们的职责与关键字段如下表名用途关键字段说明student学生基本信息id, student_no, name, password, class_name, phone学号唯一登录账号就是学号teacher教师基本信息id, teacher_no, name, password, title教师用工号登录course课程信息id, course_no, course_name, credit, teacher_idteacher_id 关联教师表course_student选课关系id, student_id, course_id, selected_time学生与课程多对多关联score成绩记录id, student_id, course_id, score_value, remark同一学生同一课程只保留一条我在实际设计中会用一个看似偷懒但很实用的原则不建物理外键只建普通索引。原因后面单独讲。先看表结构本身这五张表覆盖了大部分后台管理系统的关联关系够你扩展出考试管理、考勤记录等新模块。3.2 建表 SQL字符集、索引与初始化数据下面是这套表在 MySQL 8 上的落地 SQL。配合“JavaWeb 项目完整案例 MySQL”这个方向初始化数据也一并给出方便你启动后立刻能看到页面效果。CREATE DATABASE IF NOT EXISTS edu_admin DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE edu_admin; CREATE TABLE student ( id INT NOT NULL AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, password VARCHAR(64) NOT NULL COMMENT 密码建议存MD5, class_name VARCHAR(50) DEFAULT COMMENT 班级, phone VARCHAR(20) DEFAULT COMMENT 手机号, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE teacher ( id INT NOT NULL AUTO_INCREMENT, teacher_no VARCHAR(20) NOT NULL COMMENT 工号, name VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL, title VARCHAR(30) DEFAULT 讲师, PRIMARY KEY (id), UNIQUE KEY uk_teacher_no (teacher_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( id INT NOT NULL AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1) NOT NULL DEFAULT 2.0, teacher_id INT, PRIMARY KEY (id), KEY idx_teacher_id (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course_student ( id INT NOT NULL AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, selected_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id, course_id), KEY idx_course_id (course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE score ( id INT NOT NULL AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score_value DECIMAL(5,2) COMMENT 成绩保留2位小数, remark VARCHAR(255) DEFAULT , PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;初始化数据里我准备三个测试账号分别是管理员、教师、学生全部用 MD5 后的 123456 作为密码INSERT INTO student (student_no, name, password, class_name) VALUES (2024001, 张三, MD5(123456), 计算机2401班), (2024002, 李四, MD5(123456), 计算机2401班); INSERT INTO teacher (teacher_no, name, password, title) VALUES (T1001, 王老师, MD5(123456), 副教授); INSERT INTO course (course_no, course_name, credit, teacher_id) VALUES (C001, JavaWeb程序设计, 4.0, 1); INSERT INTO course_student (student_id, course_id) VALUES (1, 1), (2, 1);上面 SQL 有几个设计决策值得说清楚。第一密码字段长度是 64给 MD5 留足空间如果你想升级成加盐 MD5 或 SHA-256也不用改表结构。第二score_value用DECIMAL(5,2)最多存 999.99教务场景完全够用绝对不要用FLOAT否则 89.5 这种成绩在浮点运算里可能变成 89.499999。第三course_student 和 score 都加了唯一索引uk_student_course从数据库层面就防止了“同一学生重复选同一门课”和“同一门课被录入两次成绩”。3.3 为什么不建物理外键上面 SQL 里我只写了普通索引没有FOREIGN KEY。这是我在做过多个 javaweb 项目后踩坑换来的经验。物理外键看起来安全但它有四个问题删除学生时如果选课表里有记录外键约束会直接报错你被迫要写一套“先删选课再删学生”的代码如果图省事用了ON DELETE CASCADE误删一个学生他的选课记录和成绩全部跟着消失没有后悔药选课、退课这类高频操作外键校验会额外查一次索引数据量上来后对写入延迟有影响项目迁到 Spring Boot MyBatis 时Mapper 里的批量操作经常被外键顺序卡住。所以我的做法是表间关联关系由 Service 层代码保证表里只建普通索引。比如删除学生前Service 先删除 course_student 再删 student录入成绩前Service 捕获唯一索引引发的 DuplicateKeyException提示“该生成绩已录入”。这样既保持数据安全又不会被数据库约束绑死业务逻辑。3.4 初始化数据的执行坑黑马 javaweb 笔记里的数据脚本大多基于旧版本 MySQL 或旧驱动如果你直接把脚本粘进 MySQL 8最常见的现象是SQL 执行成功但 SELECT 出来中文全是问号。原因有三层一是库表字符集不是 utf8mb4二是命令行客户端连接时缺--default-character-setutf8mb4三是 JDBC 连接串里少了 characterEncoding 参数。在 DBeaver 里执行通常没问题但如果用 mysql 命令行导入命令要写成mysql -u root -p --default-character-setutf8mb4 init.sql执行后再验证一次SHOW CREATE TABLE student\G SELECT student_no, name FROM student;如果 name 显示正常说明字符集链路没问题如果还是问号优先排查 JDBC 连接串和页面编码。连接池里的连接串配置会在后面代码部分专门说明。4. 核心功能实现登录、权限与分页的代码骨架4.1 登录 Servlet从参数校验到 Session 写入登录是教务管理系统的门面也是最容易写出漏洞的位置。先把登录流程拆成四步接收请求参数、校验用户名密码、从数据库查用户、把用户对象写入 Session。下面是 LoginServlet 的核心代码WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); // 1. 验证码校验code 存在 Session 中与输入比对 String inputCode req.getParameter(captcha); String sessionCode (String) req.getSession().getAttribute(captcha); if (inputCode null || !inputCode.equalsIgnoreCase(sessionCode)) { req.setAttribute(error, 验证码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } // 2. 接收账号、密码先做基本参数校验 String username req.getParameter(username); String password req.getParameter(password); if (username null || username.trim().isEmpty() || password null || password.trim().isEmpty()) { req.setAttribute(error, 账号或密码不能为空); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } // 盐值建议放到配置文件中不要硬编码 User user userService.login(username, DigestUtils.md5DigestAsHex((password SALT).getBytes(StandardCharsets.UTF_8))); if (user null) { req.setAttribute(error, 账号或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } if (user.getStatus() ! 1) { req.setAttribute(error, 账号已被禁用); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } // 3. 登录成功将安全信息写入 Session HttpSession session req.getSession(); session.setAttribute(user, user); session.setAttribute(role, user.getRole()); resp.sendRedirect(req.getContextPath() /dashboard); } }这段代码里有几处不是随手写的。第一req.setCharacterEncoding(UTF-8)必须出现在读取任何参数之前否则 POST 表单里的中文必乱。第二密码不要明文比对我用password SALT后取 MD5。教务管理系统没有极端安全要求MD5 加盐是“够用且好教”的方案如果要应对等保测评可以换成 BCrypt接口调用方式不变。第三登录成功后的 Session 里只放必要信息JSP 里展示时也不要随手把 password 字段打印出来。在 DAO 层查询用户时如果用了字符串拼接 SQL登录接口就会被万能密码打穿。下面这种写法是最低要求String sql SELECT * FROM student WHERE student_no ? AND password ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { // 映射 User 对象 } }这里唯一的要点是?占位符配合setStringJDBC 驱动会帮你完成转义单引号、反斜杠都不会被当成 SQL 语法解析。凡是手拼 SQL 的地方都是在给系统留后门。4.2 Filter 权限拦截一个 Filter 管三种角色教务管理系统里三种角色的菜单差异很大如果每个 Servlet 都去判断 Session 里的 role代码会重复到没法维护。用 Filter 统一拦截是标准做法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.startsWith(request.getContextPath() /css/) || uri.startsWith(request.getContextPath() /js/)) { chain.doFilter(req, resp); return; } // 未登录一律跳转登录页 HttpSession session request.getSession(false); User user session null ? null : (User) session.getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } // 角色细分管理员才能访问 /admin/ 开头路径 if (uri.startsWith(request.getContextPath() /admin/) !admin.equals(user.getRole())) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(req, resp); } }这段 Filter 的拦截原理是未登录一律跳回登录页管理员资源只放行 admin 角色教师和学生的细分权限靠各自模块里的角色判断完成。注意request.getSession(false)这个参数false 表示如果没有 Session 就返回 null不会偷偷新建一个 Session。如果你写成无参版本攻击者每次访问都能在服务端留下一个 Session 对象日积月累会撑出内存压力。一个容易踩的细节WebFilter(/*)会把 JSP 也拦截。如果 JSP 放在WEB-INF/views下外部请求根本访问不到Servlet forward 进去不受影响所以更安全。如果你的 JSP 放在 webapp 根目录就必须在 Filter 白名单里放行登录页否则会出现“登录页刚打开就被拦回登录页”的死循环。4.3 分页查询学生列表参数、SQL 与页面回显教务系统的学生列表几乎必然超过一页分页查询是必考点。实现分页需要三个数据当前页码 page、每页条数 limit、总条数 total。对应的 DAO 方法如下public ListStudent findPage(int page, int limit, String keyword) { String sql SELECT * FROM student WHERE name LIKE ? ORDER BY id LIMIT ? OFFSET ?; try (Connection conn dbUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setInt(2, limit); ps.setInt(3, (page - 1) * limit); // 执行并映射结果 } } public int count(String keyword) { String sql SELECT COUNT(*) FROM student WHERE name LIKE ?; // 返回总数 }参数说明LIMIT ? OFFSET ?里的 OFFSET 从 0 开始所以第一页的偏移量是(page - 1) * limit。这个公式一旦写错第二页会重复显示第一页的数据是一眼就能看出但很容易忽视的翻车点。count 查询必须走一次否则页面无法计算总页数。JSP 拿到 total 后计算总页数我用(total limit - 1) / limit这样可以避免total恰好整除时多出一页的边界问题。页码导航里的上下页链接要把搜索关键字参数带着传否则翻到第二页搜索条件就丢了。这个“搜索条件丢”的问题在答辩演示时几乎必被问到提前处理掉会让代码成熟度高一大截。5. 部署运行避坑从 IDEA 到 Tomcat 的五条经验5.1 404 与路径映射对不上先看 Application context现象IDEA 里启动 Tomcat 后访问http://localhost:8080/edu/login.jsp显示 404但 Tomcat 日志里没有任何异常。原因多半不是代码问题而是部署路径和 Application context 对不上。IDEA 启动 Tomcat 时页面实际地址由项目部署名和 Application context 两个值共同决定。如果项目名是 edu-admin 且没改 context实际访问地址就是/edu-admin/login.jsp而不是你记忆中的/edu/login.jsp。解决在 Run/Debug Configurations 里把 Application context 改为/edu之后所有访问路径统一使用/edu。改完重启再看 IDEA 控制台里 Tomcat 启动日志输出的部署路径以它为准不要再靠猜。5.2 中文乱码三个链路关卡逐一检查现象数据库表里中文正常页面提交的中文到 MySQL 变成问号或者 JSP 页面本身渲染出来就是乱码。原因乱码是某一环字符集配置断了。整条链路是浏览器编码 → Tomcat 请求编码 → JDBC 连接编码 → MySQL 表编码。任何一环不一致都会出问题。解决依次做三件事。第一每个 JSP 顶部加% page contentTypetext/html;charsetUTF-8 %。第二所有处理表单提交的 Servlet 第一行执行request.setCharacterEncoding(UTF-8)。第三JDBC 连接串写完整参数这是最常被忽略的一环jdbc:mysql://localhost:3306/edu_admin?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai连接串里的serverTimezoneAsia/Shanghai在 MySQL 8 驱动下几乎必填不写会导致连接时区异常useSSLfalse不是必需但本地开发时加上能省去证书验证的日志噪音。5.3 Tomcat 启动报 ClassNotFoundException依赖没有进入 Artifact现象Maven 依赖列表里明明有 mysql-connector-javaTomcat 启动后一执行数据库操作就报ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因Maven 依赖只在编译期有效运行时 classpath 需要把依赖 jar 复制到WEB-INF/lib。IDEA 的 war exploded 部署不会默认包含所有 Maven 依赖需要在 Artifact 配置里手动确认。解决进入 Project Structure → Artifacts选中你的 exploded war在 Output Layout 里检查WEB-INF/lib下是否存在 mysql-connector-java、jstl 等 jar。没有的话从右侧 Available Elements 里的 Library Files 中选中依赖右键 Add to WEB-INF/lib。改完必须重启 Tomcat因为 Artifact 结构变化后热部署经常不生效。5.4 登录后跳转 404相对路径与绝对路径混乱现象登录页能打开登录成功后浏览器地址栏跳到一个类似/login.jsp/login的地址结果 404。原因前端表单 action 写的是相对路径比如actionlogin。浏览器会以当前路径为基准拼接当前 URL 是/edu/login.jsp提交目标就变成了/edu/login.jsp/login而 Servlet 映射是/login自然对不上。解决在 JSP 里用 EL 表达式拼出项目根路径form action${pageContext.request.contextPath}/login methodpost${pageContext.request.contextPath}在运行时解析为/edu无论页面在哪一层提交地址都不会错。Servlet 里写sendRedirect时也是一样统一使用response.sendRedirect(request.getContextPath() /dashboard)避免嵌套目录下相对路径出错。5.5 删除学生被选课表拦住逻辑外键加业务事务现象管理员删除一个已有选课记录的学生时控制台报Cannot delete or update a parent row: a foreign key constraint fails或者反过来配置了 ON DELETE CASCADE 后误删一条学生记录导致成绩全没了。原因物理外键约束或级联策略与业务行为不匹配。学生“退学”不只是删一条 student 记录还要同步处理 course_student 和 score 里的数据如果顺序没设计好数据库就会用报错来保护数据。解决在 Service 层写一个事务方法先删关联表再删主表public void deleteStudent(int studentId) { Connection conn dbUtils.getConnection(); try { conn.setAutoCommit(false); courseStudentDao.deleteByStudentId(conn, studentId); scoreDao.deleteByStudentId(conn, studentId); studentDao.deleteById(conn, studentId); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } }这里用setAutoCommit(false)加手动 commit/rollback 实现事务目的就是让三步删除要么全部成功要么全部失败防止出现“选课记录没了但学生还在”的半截状态。数据库物理外键可以不建但代码事务必须有这是逻辑外键方案的底线。6. 进阶用法自检脚本与连接池改造让项目变稳每改完一轮代码重新部署我很少直接开浏览器手动点而是先跑一个接口冒烟脚本用 curl 完成登录和页面访问。脚本的好处是每次执行的路径完全一样不会漏点功能#!/bin/bash BASEhttp://localhost:8080/edu rm -f /tmp/edu_cookies.txt # 登录保存会话 Cookie curl -s -c /tmp/edu_cookies.txt -d username2024001password123456 \ -X POST $BASE/login -L -o /tmp/login_result.html # 带上 Cookie 访问学生列表 curl -s -b /tmp/edu_cookies.txt $BASE/student/list?page1limit10 \ -o /tmp/student_list.html grep -q 张三 /tmp/student_list.html echo PASS || echo FAIL这里有两个细节-c是保存 Cookie-b是发送 Cookie两者一起用才能模拟浏览器会话URL 里的必须用双引号包起来否则 shell 会把limit10当成后台任务执行。如果脚本输出 PASS就说明登录、会话、分页查询、页面渲染这条链路是通的。把它加入部署流程后能拦截大量“改完代码重启后某个页面白屏”的回归问题。另一个让项目变稳的改造点是连接池参数。db.properties 里这几项值得认真设置jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/edu_admin?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordroot jdbc.initialSize2 jdbc.maxTotal20 jdbc.maxIdle10 jdbc.maxWaitMillis3000maxTotal20是连接池最大连接数maxWaitMillis3000表示拿不到连接时最多等 3 秒超过就抛异常。如果页面偶尔变慢但 MySQL CPU 不高十有八九是连接池满了或等待时间设置过长这时候调大 maxTotal 比加服务器内存更有效。我现在的习惯是每次把 javaweb 项目工程交给别人都会附上三样东西建表 SQL、IDEA 部署配置截图、上面的自检脚本。纯 Servlet 项目最怕的不是代码逻辑而是“在我机器上是好的”这句话——环境差异会让同一个项目在不同机器上有完全不同的表现。把这些验证工具带上你的项目才算真正交付而不是只交了一个源码包。希望帮到你。本文还有配套的精品资源点击获取
返回列表