资讯动态

JavaWeb毕业设计选题系统:JSP Servlet MySQL开发实战

发布时间:2026/10/9 12:57:26 来源:尧图企业网站定制
简介面向Java Web课程设计与毕业设计开发的完整选题系统项目基于JavaJspMysql实现包含管理员、教师、学生三种角色管理员可管理学生信息、教师信息与课题信息教师负责添加课题、查看课题学生可浏览并选择课题完整覆盖高校毕业设计选题业务中的主要流程。压缩包共221个文件整体仅5.57MB含98个JSP页面、12个Java源文件与24个class编译文件以及SQL数据库脚本、XML配置文件、GIF/JPG图片与CSS样式等目录结构清晰sql脚本位于sql文件夹导入后即可快速初始化数据库。系统经过多次测试运行无误并已获得2416人学习下载适合需要快速搭建选题平台或想学习传统JSPServletMySQL分层架构的读者参考可作课程设计基底或毕设项目模板。1. JavaJSPMySQL 的毕业设计选题系统它到底替你解决了什么每到毕业季导师和教务老师最怕的不是论文写不完而是选题阶段几十个学生在微信群里反复改志愿Excel 表格传来传去最后统计时发现两个人选了同一个题目。标题里这套系统就是把这件事搬上 Web学生登录看题目、提交选题教师登录审核管理员统一导出结果。技术栈锁死在 Java、JSP、MySQL不碰 Spring Boot是高校 Web 方向最常见的课程设计与毕业设计题目之一。这套方案适合两类人一类是拿“毕业设计选题系统”当毕设题目的在校生一类是想快速交付一个可演示 Web 应用的全栈初学者。整篇内容会沿着建库、写 Servlet、写 JSP 页面、部署 Tomcat 这条完整链路展开最后把我在真实运行中踩过的坑一并交代清楚。读完你可以跟着复现也能够明白这套结构为什么能跑得起来。2. 开发环境与项目骨架JDK、Tomcat、MySQL 的版本搭配与最小可运行配置2.1 为什么是 JSP Servlet MySQL而不是 Spring Boot标题既然写死 JavaJspMysql最合理的技术栈就是 Servlet JSP JDBC最多加一个 JSTL 标签库。Spring Boot 也能做选题系统但它默认把 JSP 支持放在嵌入式容器里配置起来比传统 war 包部署更别扭反而绕远了。JSP 本质是 Servlet 的模板化封装第一次被访问时由 Tomcat 的 Jasper 引擎编译成 Servlet 再执行所以“JSP 页面 → Servlet 类 → 数据库操作”这条链路特别适合理解 Web 项目的基础原理。从交付角度看传统动态 Web 项目的骨架很轻一个能跑 Tomcat 的电脑、一个 MySQL 实例、一个 Maven 环境就够了。不需要搞微服务不需要上消息队列更不需要为这种业务写缓存中间件。你只需要把“请求怎么进来、业务怎么处理、数据怎么落库”这三件事理清楚系统就已经完成大半。2.2 版本搭配一套可用的组合与为什么这样选版本搭配是新手翻车最多的第一关。网上能找到的 MySQL 安装教程多半是 5.7 或 8.0 两个版本Tomcat 又分 8.5、9.0、10.x驱动坐标也一直在变。我建议直接用一套经过验证的组合组件推荐版本说明JDK1.8 或 11大多数教学环境和 Tomcat 8.5 都兼容避免 JDK 17 的模块化麻烦Tomcat8.5.x 或 9.0.x支持 Servlet 4.0 / JSP 2.3部署 war 包最简单MySQL5.7 或 8.08.0 需要注意驱动类名和时区参数下文避坑章会细说Maven3.6 以上管理依赖和打包 war 用Tomcat 10.x 不建议在这个项目里碰因为它的包名从javax.servlet换成了jakarta.servlet网上的教程、代码片段大部分还停留在javax新手照着写会立刻编译报错。要让项目快速跑起来选“老一点但资料多”的组合比选“最新但没人踩过坑”的组合可靠得多。2.3 用 Maven 搭一个动态 Web 项目骨架在 IDEA 或 Eclipse 里新建 Maven 项目时packaging 要选war因为我们要部署到外部 Tomcat。项目建好后需要补上 Web 目录结构最终长这样topic-select/ ├── pom.xml ├── src/main/java/ │ ├── com/example/dao/ # JDBC 数据访问 │ ├── com/example/entity/ # User、Topic、Selection 实体 │ ├── com/example/web/servlet/ # LoginServlet、SubmitSelectionServlet 等 │ └── com/example/util/ # DBUtil、MD5Util 等工具 └── src/main/webapp/ ├── WEB-INF/ │ └── web.xml # Servlet 3.0 可用 WebServlet此文件可留空壳 ├── login.jsp ├── student/ ├── teacher/ └── admin/pom.xml是这套骨架的核心依赖声明直接决定后面会不会遇到 ClassNotFound 或版本冲突project 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.example/groupId artifactIdtopic-select/artifactId version1.0.0/version packagingwar/packaging properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies !-- Servlet 和 JSP 由 Tomcat 自己提供scope 必须是 provided -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- JSTLJSP 页面里做循环和条件判断 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 驱动MySQL 8.0 必须配 8.x 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- 数据库连接池不用每次请求都创建 Connection -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependencies build finalNametopic-select/finalName /build /project两个关键点必须理解javax.servlet-api和javax.servlet.jsp-api的 scope 必须是provided因为 Tomcat 自身已经带了这些类如果打成 jar 放进WEB-INF/lib启动时会和容器类冲突mysql-connector-java8.x 的包名是com.mysql.cj.jdbc.Driver不再是 5.x 时代的com.mysql.jdbc.Driver。finalName决定 war 包的名称也决定部署后的访问路径/topic-select。2.4 在 Tomcat 里跑通第一个 JSP 页面项目骨架建好后先在webapp下放一个最简单的index.jsp用来验证整条链路是否通% page contentTypetext/html;charsetUTF-8 languagejava % !DOCTYPE html html head title毕业设计选题系统/title /head body h3选题系统启动成功/h3 a href${pageContext.request.contextPath}/login.jsp进入登录页/a /body /html页面里用了${pageContext.request.contextPath}这是 JSP 的 EL 表达式会自动拼上当前应用的上下文路径。不要手写死/topic-select否则以后改 war 包名所有链接都会断掉。用 Maven 打包后把target/topic-select.war复制到 Tomcat 的webapps/目录启动bin/startup.shWindows 下是bin/startup.bat浏览器访问http://localhost:8080/topic-select/。能看到页面就说明 Tomcat、JSP 编译、war 包结构三者都正常。这一步跑不通后面所有业务功能都无从谈起。3. 数据库设计用户表、题目表与选题记录表的字段级拆解3.1 先画明白关系一个教师发布多个题目一个学生只能有一个待审选题数据库设计是这类系统的地基表结构理不清楚后面 Servlet 代码会写得非常别扭。核心表只有三张用户表、题目表、选题记录表。用户表里用role字段区分学生、教师、管理员而不是拆成t_student、t_teacher两张表——因为登录入口只有一个拆表会让登录查询变得啰嗦。题目表挂在教师 ID 上表示“这个题是谁发布的”。选题记录表挂在学生 ID 和题目 ID 上表示“谁选了哪个题、当前状态是什么”。这三张表的关系用一句话说就是一个教师可以发布多个题目一个题目可以被多个学生选通过max_students控制名额但一个学生同一时间只能有一条处于“待审核/通过”状态的选题记录。状态不要用字符串比如waiting、approved直接定义成tinyint枚举0 待审核、1 通过、2 拒绝、3 已撤销。字符串状态在写 SQL 时会因为大小写和拼写问题埋坑而且改状态逻辑时 Java 代码里全是魔法字符串容易看漏。3.2 建表 SQLutf8mb4、唯一索引与自增主键下面的 SQL 可以直接在 MySQL 里执行。字符集要选utf8mb4不是utf8因为utf8在 MySQL 里最多存 3 字节遇到表情符号会报错utf8mb4才是完整的 UTF-8。CREATE DATABASE topic_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE topic_db; CREATE TABLE t_user ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(128) NOT NULL COMMENT 密码建议存 MD5 加盐后的值, role TINYINT NOT NULL COMMENT 1-学生 2-教师 3-管理员, real_name VARCHAR(50) DEFAULT NULL COMMENT 姓名, student_no VARCHAR(20) DEFAULT NULL COMMENT 学号仅学生使用, teacher_title VARCHAR(50) DEFAULT NULL COMMENT 职称仅教师使用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB COMMENT用户表; CREATE TABLE t_topic ( id INT NOT NULL AUTO_INCREMENT COMMENT 题目主键, teacher_id INT NOT NULL COMMENT 发布教师关联 t_user.id, title VARCHAR(200) NOT NULL COMMENT 题目名称, content TEXT COMMENT 题目要求与验收目标, max_students INT NOT NULL DEFAULT 1 COMMENT 可容纳人数, selected_count INT NOT NULL DEFAULT 0 COMMENT 当前已选人数, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-开放 0-关闭, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_teacher (teacher_id), CONSTRAINT fk_topic_teacher FOREIGN KEY (teacher_id) REFERENCES t_user (id) ) ENGINEInnoDB COMMENT选题表; CREATE TABLE t_selection ( id INT NOT NULL AUTO_INCREMENT COMMENT 选题记录主键, student_id INT NOT NULL COMMENT 学生用户ID, topic_id INT NOT NULL COMMENT 题目ID, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待审核 1-通过 2-拒绝 3-已撤销, apply_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 提交时间, audit_time DATETIME DEFAULT NULL COMMENT 审核时间, audit_note VARCHAR(255) DEFAULT NULL COMMENT 审核备注, PRIMARY KEY (id), UNIQUE KEY uk_student_topic (student_id, topic_id), KEY idx_topic (topic_id), CONSTRAINT fk_sel_student FOREIGN KEY (student_id) REFERENCES t_user (id), CONSTRAINT fk_sel_topic FOREIGN KEY (topic_id) REFERENCES t_topic (id) ) ENGINEInnoDB COMMENT选题记录表;注意uk_student_topic (student_id, topic_id)这个唯一索引它保证同一学生对同一题目只能存在一条记录。如果学生被拒绝后想重新申请正确做法是 UPDATE 原记录把status改回 0而不是再 INSERT 一条。这个设计配合第 4 章的提交逻辑能从数据库层面挡住重复插入。学生选不同题目是可以的但业务上只允许保留一个活跃申请这个靠事务代码控制。3.3 初始化数据管理员、教师、学生账号系统中必须先有账号才能测试登录直接用 SQL 插入三个基础账号。密码演示用 MySQL 的MD5()函数正式上线建议用加盐哈希比如 SHA-256 加随机盐但这个项目用 MD5 足够教学演示INSERT INTO t_user (username, password, role, real_name, student_no) VALUES (admin, MD5(admin123), 3, 系统管理员, NULL), (teacher01, MD5(teacher123), 2, 张老师, NULL), (student01, MD5(student123), 1, 李四, 20210001); INSERT INTO t_topic (teacher_id, title, content, max_students) VALUES (2, 基于JavaWeb的宿舍报修系统, 实现报修单提交、派单、状态跟踪和评价闭环。, 4), (2, 基于JSP的毕业论文过程管理系统, 实现论文选题、开题报告、中期检查的信息化管理。, 2);很多 MySQL 安装教程会默认建好 root 账号但项目代码绝不能直接用 root 去连数据库。正式做法是创建一个专用账号只授予topic_db的权限这样即使连接串泄露也不会把整个 MySQL 实例暴露出去。3.4 索引与事务防止重复选题的两道闸门倒回去看业务规则学生只能有一个待审核或通过的选题题目不能超员。这两条规则靠代码校验无法在并发下保证必须靠数据库配合。第一道闸门是唯一索引uk_student_topic防止同一学生对同一题重复插入。第二道闸门是事务加上条件 UPDATE。扣名额的 SQL 写成update t_topic set selected_count selected_count 1 where id? and status1 and selected_count max_students并且检查返回的更新行数——如果影响行数为 0说明题目已满或已关闭当前事务回滚。先用 SELECT 查再用 UPDATE 是典型的并发漏洞两个请求同时读到名额还剩 1最后两个人都插入成功。所有跟名额相关的操作必须在事务里并且用带条件的 UPDATE 原子地完成这是整套系统里最不能省的一步。4. 核心功能实现登录鉴权、选题流程和 JSP 页面串联4.1 登录逻辑UserDao 查询 Session 保存角色登录是整个系统的大门。表单提交到LoginServletServlet 从请求里取用户名密码调用 DAO 查询用户成功后把用户对象塞进 Session再按角色跳转。这里的关键点有两个req.setCharacterEncoding(UTF-8)必须放在读取参数之前密码校验要放在 DAO 的 SQL 里不要把数据库里所有用户查出来再在 Java 里遍历比对。package com.example.web.servlet; import com.example.dao.UserDao; import com.example.entity.User; 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 java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { private final UserDao userDao new UserDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 编码设置必须在 getParameter 之前 req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); User user userDao.findByUsernameAndPassword(username, password); if (user null) { req.setAttribute(error, 用户名或密码错误); req.setAttribute(username, username); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } HttpSession session req.getSession(); session.setAttribute(loginUser, user); session.setAttribute(role, user.getRole()); // 按角色分发首页 if (user.getRole() 1) { resp.sendRedirect(req.getContextPath() /student/topicList.jsp); } else if (user.getRole() 2) { resp.sendRedirect(req.getContextPath() /teacher/topicList.jsp); } else { resp.sendRedirect(req.getContextPath() /admin/index.jsp); } } }代码里 forward 和 sendRedirect 的区别要分清楚登录失败用 forward把 request 里的错误信息带给 login.jspURL 不变登录成功用 sendRedirect让浏览器重新发起一个新请求避免刷新页面时重复提交表单。这也是一种防止“重复提交”的基础手段。UserDao里对应的查询方法写法如下public User findByUsernameAndPassword(String username, String password) { String sql select * from t_user where username? and passwordmd5(?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery(); if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setRole(rs.getInt(role)); user.setRealName(rs.getString(real_name)); return user; } } catch (SQLException e) { throw new RuntimeException(查询用户失败, e); } return null; }这里用PreparedStatement传参而不是拼接字符串是防止 SQL 注入的底线。之前看到过有同学把登录写成select * from t_user where username username 输入 or 11 --直接绕过密码这是 Java Web 项目实战里最丢分的翻车现场。4.2 一个 Filter 同时做编码处理和登录拦截JSP 页面、Servlet、Filter 都需要处理编码只在一个地方设置没用。最简单可靠的做法是写一个全局 Filter拦截所有请求先设置 UTF-8再做登录状态检查。为什么不用两个 Filter因为多个WebFilter(/*)的执行顺序不直观合并成一个少踩顺序的坑。package com.example.web.filter; import com.example.entity.User; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; WebFilter(/*) public class AppFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; // 先统一编码 req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); String uri req.getRequestURI(); String contextPath req.getContextPath(); String target uri.substring(contextPath.length()); // 放行登录页、登录接口和静态资源 if (target.equals(/login.jsp) || target.equals(/login) || target.startsWith(/css/) || target.startsWith(/js/)) { chain.doFilter(request, response); return; } // 未登录一律跳回登录页 HttpSession session req.getSession(false); if (session null || session.getAttribute(loginUser) null) { resp.sendRedirect(contextPath /login.jsp); return; } // 角色路径隔离学生不能访问教师目录 User user (User) session.getAttribute(loginUser); if (target.startsWith(/teacher/) user.getRole() ! 2) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } if (target.startsWith(/admin/) user.getRole() ! 3) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(request, response); } }这个 Filter 里体现了一个重要原则JSP 页面本身没有保护能力用户可以直接在浏览器输入 URL 访问/student/topicList.jsp所以必须在 Servlet 容器层面做拦截。req.getSession(false)里的 false 表示如果没有 Session 就返回 null 而不是新建一个避免未登录访问也被创建出无用 Session。角色路径隔离做了两层一层是登录状态一层是角色目录这样学生即使猜到教师页面的 URL 也进不去。4.3 选题提交事务、条件 UPDATE 和状态校验选题提交是整个系统最核心的业务逻辑。一个学生在页面上点“选这个”表单 POST 到SubmitSelectionServlet。这里不能只做insert还要做三步校验该学生是否已有活跃的选题记录、题目是否开放且未满员、插入是否成功。三步必须在一个事务里完成否则会出现“名额扣了但记录没插上”这类数据不一致问题。package com.example.web.servlet; import com.example.entity.User; import com.example.util.DBUtil; 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 java.io.IOException; import java.sql.*; WebServlet(/student/submitSelection) public class SubmitSelectionServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); HttpSession session req.getSession(); User loginUser (User) session.getAttribute(loginUser); int studentId loginUser.getId(); int topicId Integer.parseInt(req.getParameter(topicId)); Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 第一步检查该学生是否已有待审核或通过的选题 int activeCount 0; try (PreparedStatement ps conn.prepareStatement( select count(*) from t_selection where student_id? and status in (0,1))) { ps.setInt(1, studentId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { activeCount rs.getInt(1); } } } if (activeCount 0) { conn.rollback(); req.setAttribute(msg, 你已有一个待审核或通过的选题不能重复提交); req.getRequestDispatcher(/student/result.jsp).forward(req, resp); return; } // 第二步原子扣减名额不满足条件则影响行数为 0 String updateSql update t_topic set selected_count selected_count 1 where id? and status1 and selected_count max_students; int rows; try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setInt(1, topicId); rows ps.executeUpdate(); } if (rows 0) { conn.rollback(); req.setAttribute(msg, 题目已关闭或名额已满); req.getRequestDispatcher(/student/result.jsp).forward(req, resp); return; } // 第三步插入选题记录唯一索引 uk_student_topic 做最后兜底 try (PreparedStatement ps conn.prepareStatement( insert into t_selection(student_id, topic_id, status) values(?,?,0))) { ps.setInt(1, studentId); ps.setInt(2, topicId); ps.executeUpdate(); } conn.commit(); resp.sendRedirect(req.getContextPath() /student/topicList.jsp); } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } throw new ServletException(选题失败, e); } finally { try { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } } }这段代码有两个值得细看的点。第一是conn.setAutoCommit(false)之后手动控制 commit 和 rollback所有数据库操作全部走同一个连接这样三个步骤要么全部成功要么全部不生效。第二是第二步用 UPDATE 而不是先 SELECT 再 UPDATEselected_count max_students这个条件直接在数据库里判断两个学生同时提交时其中一个人的 UPDATE 会等待行锁另一个提交后再执行时条件已经不满足影响行数为 0自然被回滚。这是 MySQL 事务里防超选的标准做法。4.4 用 EL JSTL 渲染题目列表避免 JSP 里写 Java 脚本前面 Servlet 负责取数据JSP 只负责展示。有些老项目习惯在 JSP 里写% for(...) %这种脚本片段虽然能跑但页面里嵌着大量 Java 代码调试和修改都很痛苦。这里用 JSTL 的c:forEach配合 EL 表达式页面干净读起来也更接近 HTML 模板的习惯。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title可选题目列表/title /head body h3当前开放选题/h3 table border1 cellpadding8 tr th题目名称/th th指导教师/th th已选/名额/th th操作/th /tr c:forEach items${topicList} vart tr td${t.title}/td td${t.teacherName}/td td${t.selectedCount}/${t.maxStudents}/td td form methodpost action${pageContext.request.contextPath}/student/submitSelection input typehidden nametopicId value${t.id} button typesubmit选这个/button /form /td /tr /c:forEach /table /body /html${topicList}是 Servlet 里通过request.setAttribute(topicList, list)放进去的数据EL 表达式会自动调用 getter${t.title}等价于t.getTitle()。每个题目旁边放了提交表单用隐藏字段带题目 ID。这里必须用contextPath拼接 URL否则在/topic-select/student/topicList.jsp页面里写死/student/submitSelection请求会跑到http://localhost:8080/student/submitSelection少了一层上下文路径直接 404。在展示教师姓名时注意TeacherName这个字段不在t_topic表里而是在查询时 JOINt_user表后放到Topic对象的属性里。一个简单的做法是select t.*, u.real_name as teacher_name from t_topic t left join t_user u on t.teacher_id u.id where t.status 1 order by t.id desc;查询结果通过rs.getString(teacher_name)映射到Topic.teacherName属性页面里就能直接用${t.teacherName}。字段的命名映射要前后一致teacher_name和teacherName只差一个下划线却最容易因为拼写不一致导致数据读不出来。5. 部署调试避坑从 404 到中文乱码的六个常见故障排法5.1 ClassNotFoundExceptionMySQL 8.0 驱动类名和时区设置现象项目部署后访问登录接口页面抛java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因MySQL 8.0 的驱动包把入口类从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver同时连接 URL 需要显式声明时区参数。解决在DBUtil里加载驱动和拼 JDBC URL 时统一按 MySQL 8.0 的规范写Class.forName(com.mysql.cj.jdbc.Driver); String url jdbc:mysql://localhost:3306/topic_db ?characterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse;如果你是 MySQL 5.7驱动用 5.1.47 版本类名还是com.mysql.jdbc.DriverURL 不需要serverTimezone。检查项目里到底用的是哪个版本直接看pom.xml里mysql-connector-java的版本号不要凭印象猜。5.2 依赖冲突servlet-api 和 jsp-api 不能塞进 WEB-INF/lib现象Tomcat 启动时抛java.lang.ClassCastException: org.apache.jasper.servlet.JspServlet cannot be cast to javax.servlet.Servlet或者启动日志里有SEVERE: The required Server component failed to start。原因WEB-INF/lib下同时存在javax.servlet-api或jsp-api依赖与 Tomcat 自带的同名类冲突导致类加载器里出现了两份javax.servlet.Servlet。解决检查pom.xml确保这两项的 scope 是provided然后重新mvn clean package清空target目录。如果你不是用 Maven 而是手动往lib里拷 jar那直接删掉servlet-api.jar和jsp-api.jar。这是一个非常典型的翻车点新手几乎都会踩一次。5.3 中文乱码JSP 页面、连接串、Filter 三层缺一不可现象数据库里中文正常但页面上显示问号或者提交的“题目名称”到了数据库变成乱码。原因一段中文从浏览器表单到 MySQL 要经过三处编码转换请求体的解码、JDBC 传输的编码、数据库表的字符集任何一层不一致都会乱码。解决按顺序检查三层。 第一层所有 JSP 头部必须写contentTypetext/html;charsetUTF-8并且页面文件本身是 UTF-8 编码IDEA 右下角可以确认文件编码。 第二层Filter 里request.setCharacterEncoding(UTF-8)必须在getParameter之前执行前面 4.2 节的AppFilter已经做了这件事。 第三层JDBC 连接 URL 里加characterEncodingutf8数据库建库时用utf8mb4。三层都对了中文不会乱码。如果是 5.1.47 的驱动注意 MySQL 参数名是characterEncoding不要写成characterencoding大小写不对直接连不上。5.4 404上下文路径与页面跳转 URL 不一致现象登录成功后跳转地址是http://localhost:8080/student/topicList.jsp页面 404而手动访问http://localhost:8080/topic-select/student/topicList.jsp却能打开。原因war 包名是topic-selectTomcat 部署后应用上下文是/topic-select所有请求都必须带这个前缀。代码里如果用绝对路径/student/xxx跳转时没有拼上上下文前缀。解决统一使用req.getContextPath()或${pageContext.request.contextPath}拼接路径。所有sendRedirect、forward、form action、href都要带上 contextPath。还有一个细节Forward到 JSP 时如果写/student/topicList.jsp系统内部会自动找应用内的路径不用带上下文前缀但sendRedirect是给浏览器一个新的地址必须带contextPath两者机制不同容易混。5.5 并发下的重复选题与超选代码校验挡不住同时提交现象测试时两个人同时点击“选这个”名额只有 1 个结果selected_count变成了 2两个人都显示成功。原因如果逻辑是先select count(*)看名额是否够再insert两个请求可能同时读到名额还有 1然后都通过校验先后插入成功。解决必须用事务 条件 UPDATE也就是 4.3 节里的update ... where ... selected_count max_students。这样数据库行锁会串行化两个请求后执行的 update 影响行数为 0事务回滚。同样的思路也适用于教师审核审核时把status从 0 改成 1应该写成update t_selection set status1 where id? and status0防止同一记录被两个审核页面重复处理。5.6 Session 失效与越权访问页面要通过 Filter 才能保护现象直接浏览器访问/teacher/topicList.jsp也能看到教师页面或者关掉浏览器再打开还能看到登录后的页面。原因JSP 文件放在 webapp 目录下是公开的Servlet 里的登录校验不会自动保护 JSP而 Session 默认由 Tomcat 管理关浏览器不一定立刻失效JSESSIONID 藏在 Cookie 里重新打开浏览器可能还带着旧凭证。解决任何需要登录的 JSP 都不能放在 public 根目录下“裸奔”要么用WEB-INF目录存放页面并 forward 访问要么用 4.2 节的AppFilter统一拦截。越权问题要靠角色路径隔离学生访问/teacher/目录直接返回 403。Session 过期时间可以在web.xml里配置session-config一般设 30 分钟。测试时用无痕窗口最干净避免旧 Session 干扰。6. 部署验证与进阶技巧从本机 war 包到能演示的完整闭环6.1 打包部署与一条可执行的验证清单所有功能在 IDEA 里跑通后最终要能以 war 包形式部署。命令行执行mvn clean package成功后target/topic-select.war就是最终产物扔到 Tomcatwebapps目录启动即可。没有装 IDE 的环境里这也是把项目交出去最省事的方式。验证时按下面清单过一遍基本可以判断系统是否合格角色操作期望结果管理员登录查看题目统计能进入管理员首页统计数据正确教师发布新题目设置名额题目出现在学生可见列表学生01选择一个题目名额扣减状态为待审核学生01再选另一个题目提示已有待审核选题不能重复教师审核通过学生01选题状态变为通过统计页正确匿名用户直接访问教师页面被 Filter 拦截回登录页6.2 提升完成度的小技巧状态机、导出与密码安全真正拿去答辩时系统不能只有一个“能选”的功能。建议把选题状态从代码里抽出来用一个SelectionStatus常量类或枚举管理待审核、通过、拒绝、已撤销所有状态流转只允许合法路径比如“待审核 → 通过”“通过 → 已撤销”不允许“拒绝 → 通过”这种跳跃。这样代码里没有散落的魔法数字答辩时也能讲清楚业务边界。导出功能是加分项用 Apache POI 把审核通过的选题结果导出成 Excel值班老师可以直接拿去打印。密码安全上注册和初始化数据的密码至少要用MD5(username password)做一层扰动不要直接存明文更讲究一点可以用 SHA-256 加随机盐把盐存在 user 表单独字段里。最后提醒一句Tomcat 默认管理页面和数据库端口不要暴露到公网部署到云服务器上时conf/server.xml里非必要端口一律关闭这是 Web 服务器安全最低成本的防线。我第一次做这类系统时把状态字段设计成 varchar 随便填结果教师审核逻辑越写越乱最后推倒重来才明白状态机要提前定死。名额扣减也是吃了并发亏之后才补上事务条件和 UPDATE。这些坑非得自己踩一遍才会长记性希望这篇文章能帮你把不该踩的部分提前绕开。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑