JavaWeb体育竞赛管理系统毕设:技术选型、数据库设计与答辩避坑指南

发布时间:2026/10/9 8:24:37
JavaWeb体育竞赛管理系统毕设:技术选型、数据库设计与答辩避坑指南
简介这是一套基于JavaWeb技术栈的体育竞赛管理系统毕业设计完整源码面向计算机相关专业学生与JavaWeb初学者帮助解决赛事报名、成绩统计与用户权限管理等场景的开发与学习需求。压缩包共2662个文件约29.37MB涵盖jsp页面、java源码、class编译文件、jar依赖包、css与js前端资源以及html、png、gif等静态素材和sql数据库脚本结构完整便于直接导入IDE运行调试。系统按运动员、管理员、裁判员三类角色划分模块包含在线报名、资格审核、成绩录入与排名公示、用户权限分配等功能采用JSP嵌入Java代码实现动态交互配合MySQL存储竞赛数据。目前已有903人学习下载适合作为课程设计或毕业设计的参考模板也可用于练习Servlet、DAO分层与数据库操作快速理解JavaWeb项目的目录组织与前后端协作流程。1. 毕设选体育竞赛管理系统为什么 JavaWeb 这套组合拳还没过时每年到了毕设选题季计算机专业本科毕设题目里总有一批人盯着「体育竞赛管理系统」这几个字。原因很实在业务场景清晰角色分明数据表结构不复杂答辩时老师一听就懂。但真动手的时候很多人卡在第一步——用 JavaWeb 还是 SpringBoot用 JSP 还是前后端分离MySQL 表怎么设计才不会被答辩老师追问到哑口无言我带过几届学生的毕设代码审查也帮人排查过 idea 运行 javaweb 项目配置的各种玄学问题。一个很反直觉的结论是对于体育竞赛管理系统这种中等复杂度的毕设纯 JavaWebServlet JSP JDBC反而比 SpringBoot 更容易讲清楚技术栈也更容易在答辩时展示你对底层原理的理解。SpringBoot 的自动配置确实省事但老师问一句「你的请求是怎么从浏览器到数据库的」很多人就答不上来了。这篇笔记面向的是正在做毕设选题、需要一套能跑通、能讲清、能应对答辩追问的 JavaWeb 项目的人。我会从技术选型、数据库设计、核心功能实现、常见翻车点几个角度把体育竞赛管理系统这个方向拆开讲。不是给你一份源码让你复制而是让你理解每一步为什么这么做参数怎么调出了问题看哪里。读完你至少能判断这个方向值不值得做怎么做才不会被卡住。2. 技术选型与开发环境Servlet 还是 SpringBootJSP 还是 Thymeleaf2.1 为什么体育竞赛管理系统适合用原生 JavaWeb 打底先说不选 SpringBoot 的理由。体育竞赛管理系统的核心业务无非是赛事发布、报名管理、成绩录入、排名统计、用户权限。这些功能的请求量在毕设演示环境下几乎可以忽略不计SpringBoot 的自动配置和起步依赖带来的便利在这个场景下反而会掩盖你对 Servlet 生命周期、请求转发与重定向、Session 管理的理解。我一般会建议学生用 Servlet JSP JDBC 这套组合。原因有三第一代码结构透明从 web.xml 或注解配置到 Servlet 的 doGet/doPost再到 JDBC 的 Connection、PreparedStatement整条链路你都能在答辩时画出来第二调试方便Tomcat 的日志直接告诉你哪个 Servlet 抛了什么异常不像 SpringBoot 的异常被层层包装第三MySQL 连接池用 Druid 或简单的 DBCP 就够不需要引入复杂的 ORM 框架。当然如果你已经对 SpringBoot 很熟用 SpringBoot MyBatis Thymeleaf 也完全可以。但要注意毕设答辩时老师更看重你对业务逻辑的梳理能力而不是框架的堆砌。我见过太多用 SpringBoot 做毕设的同学被问到「你的拦截器是怎么实现的」时只能回答「加了个注解」这就很被动。2.2 开发环境搭建IDEA 配置 Tomcat 与 MySQL 连接的完整步骤环境配置是第一个翻车高发区。很多人卡在 idea 运行 javaweb 项目配置这一步要么 Tomcat 启动报 404要么 MySQL 驱动加载失败。下面是我常用的配置流程按步骤走基本不会出问题。第一步创建项目。在 IDEA 里新建一个 Java Enterprise 项目勾选 Web Application不要选 Spring Initializr。项目结构里会有 src 和 web 两个目录web 目录下放 WEB-INF 和 index.jsp。第二步配置 Tomcat。在 Run/Debug Configurations 里添加 Tomcat ServerLocal。在 Deployment 标签页里添加 Artifact选择 your-project:war exploded。Application context 建议设为 /sports这样访问路径就是 http://localhost:8080/sports/。第三步导入依赖。在 WEB-INF 下新建 lib 目录把 mysql-connector-java、druid、jstl 的 jar 包放进去然后在 Project Structure 的 Modules 里把这些 jar 添加到依赖。注意不要只靠 Maven 的 pom.xmlJavaWeb 项目在 Tomcat 里跑的时候WEB-INF/lib 才是最终生效的类路径。# 项目目录结构参考 sports-system/ ├── src/ │ └── com/sports/ │ ├── dao/ # 数据访问层 │ ├── service/ # 业务逻辑层 │ ├── servlet/ # 控制层 │ └── util/ # 工具类如 DBUtil ├── web/ │ ├── WEB-INF/ │ │ ├── web.xml # 部署描述符 │ │ ├── lib/ # 第三方 jar 包 │ │ └── views/ # JSP 页面 │ ├── css/ │ ├── js/ │ └── index.jsp └── database/ └── sports_db.sql # 建表脚本第四步配置数据库连接。在 src 下新建 db.properties 文件内容如下drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/sports_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 usernameroot passwordyour_password initialSize5 maxActive20这里有几个参数容易踩坑。serverTimezone 必须设否则 MySQL 8.0 会报时区错误。useSSL 设为 false 避免证书警告。characterEncoding 设为 utf8 保证中文不乱码。initialSize 和 maxActive 是 Druid 连接池的参数毕设演示 5 到 20 足够了。第五步写 DBUtil 工具类。用 Druid 的 DruidDataSourceFactory 读取配置文件获取 Connection。注意在 finally 块里关闭连接或者用 try-with-resources。很多人的项目跑着跑着就报「Too many connections」就是因为 Connection 没关。提示IDEA 里如果 Tomcat 启动后访问 404先检查 Deployment 里 Artifact 有没有加再检查 Application context 是不是设成了 /sports。这两个地方是最常见的配置遗漏。2.3 前端选 JSP 还是 Thymeleaf一个影响答辩难度的决定JSP 的好处是跟 Servlet 天然集成request.setAttribute 之后直接在 JSP 里用 EL 表达式取学习成本低。坏处是 JSP 本质上是在 HTML 里写 Java 代码页面逻辑容易乱。Thymeleaf 更现代模板语法干净但需要额外配置视图解析器对纯 JavaWeb 项目来说多了一层。我的建议是如果你的 JavaWeb 基础一般用 JSP JSTL。JSTL 的 c:forEach 和 c:if 足够应付体育竞赛管理系统的所有列表和条件展示。如果你已经用过 Thymeleaf那用 SpringBoot Thymeleaf 也行但记得在答辩时能说清楚 Thymeleaf 的模板解析流程。不管选哪个前端页面建议用 Bootstrap 5 快速搭。体育竞赛管理系统的页面无非是表格、表单、按钮Bootstrap 的 table、form-control、btn 类直接套省下来的时间花在业务逻辑上更划算。3. 数据库设计从赛事表到成绩表怎么建才不会被答辩追问3.1 核心表结构用户、赛事、报名、成绩四张表的关系体育竞赛管理系统的数据库设计是整个项目的骨架。表建得好后面写 SQL 和 Java 代码都顺表建得不好改一处动全身。下面是我常用的四张核心表覆盖了 90% 的毕设需求。用户表userid、username、password、real_name、role、phone、create_time。role 字段区分管理员、裁判、运动员三种角色。password 存 MD5 加密后的值不要明文存。赛事表competitionid、name、type、start_time、end_time、location、max_participants、status、create_by。type 字段区分田径、球类、游泳等。status 字段控制赛事状态0 未开始、1 报名中、2 进行中、3 已结束。报名表registrationid、competition_id、user_id、register_time、status。status 字段0 待审核、1 已通过、2 已拒绝。competition_id 和 user_id 建联合唯一索引防止同一人重复报名同一赛事。成绩表resultid、competition_id、user_id、score、rank、record_time、judge_id。score 字段用 decimal(10,2) 存rank 字段在成绩录入后统一计算更新。judge_id 记录录入成绩的裁判。-- 建表脚本核心片段 CREATE TABLE user ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(64) NOT NULL, real_name varchar(50) DEFAULT NULL, role tinyint NOT NULL DEFAULT 0 COMMENT 0运动员 1裁判 2管理员, phone varchar(20) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE competition ( id int NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, type varchar(20) DEFAULT NULL, start_time datetime DEFAULT NULL, end_time datetime DEFAULT NULL, location varchar(100) DEFAULT NULL, max_participants int DEFAULT 100, status tinyint DEFAULT 0 COMMENT 0未开始 1报名中 2进行中 3已结束, create_by int DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE registration ( id int NOT NULL AUTO_INCREMENT, competition_id int NOT NULL, user_id int NOT NULL, register_time datetime DEFAULT CURRENT_TIMESTAMP, status tinyint DEFAULT 0 COMMENT 0待审核 1已通过 2已拒绝, PRIMARY KEY (id), UNIQUE KEY uk_comp_user (competition_id,user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE result ( id int NOT NULL AUTO_INCREMENT, competition_id int NOT NULL, user_id int NOT NULL, score decimal(10,2) DEFAULT NULL, rank int DEFAULT NULL, record_time datetime DEFAULT CURRENT_TIMESTAMP, judge_id int DEFAULT NULL, PRIMARY KEY (id), KEY idx_comp (competition_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时注意几个细节。字符集用 utf8mb4不要用 utf8否则 emoji 和某些生僻字会出问题。引擎用 InnoDB支持事务和外键。registration 表的联合唯一索引是必须的答辩时老师很可能问「怎么防止重复报名」这个索引就是答案。result 表的 rank 字段不要用关键字 rank 做字段名MySQL 8.0 里 rank 是保留字虽然加反引号能用但容易忘建议改成 ranking 或 rank_num。3.2 成绩排名怎么算SQL 窗口函数与 Java 内存排序的取舍成绩录入之后要算排名这是体育竞赛管理系统的核心逻辑之一。有两种做法一种是在 SQL 里用窗口函数 ROW_NUMBER() 或 RANK() 直接算另一种是把成绩查出来在 Java 里排序再更新。MySQL 8.0 支持窗口函数写法如下-- 按赛事分组按成绩降序排名成绩越高排名越前 UPDATE result r JOIN ( SELECT id, ROW_NUMBER() OVER (PARTITION BY competition_id ORDER BY score DESC) AS rn FROM result WHERE competition_id ? ) t ON r.id t.id SET r.rank t.rn;这段 SQL 的逻辑是先按 competition_id 分组组内按 score 降序用 ROW_NUMBER() 生成行号然后把行号更新到 rank 字段。注意 ROW_NUMBER() 和 RANK() 的区别ROW_NUMBER() 是连续序号即使成绩相同也是 1、2、3RANK() 是跳跃排名成绩相同会并列比如 1、1、3。体育比赛通常用 RANK() 更合理因为并列名次是常见需求。如果 MySQL 版本低于 8.0或者你不想在 SQL 里写复杂逻辑可以在 Java 里做。查出一个赛事的所有成绩用 List 存然后 Collections.sort 按 score 降序排再遍历设置 rank。注意处理并列情况如果当前成绩等于上一个成绩rank 不变否则 rank 等于当前索引加一。// Java 内存排序计算排名 ListResult list resultDao.findByCompetitionId(compId); list.sort((a, b) - b.getScore().compareTo(a.getScore())); int rank 1; for (int i 0; i list.size(); i) { if (i 0 list.get(i).getScore().compareTo(list.get(i - 1).getScore()) 0) { rank i 1; } list.get(i).setRank(rank); resultDao.updateRank(list.get(i).getId(), rank); }两种做法各有优劣。SQL 窗口函数效率高一次更新完成但要求 MySQL 8.0 以上而且答辩时如果老师不熟悉窗口函数你得花时间解释。Java 内存排序逻辑直观兼容所有 MySQL 版本但数据量大时性能差。毕设场景下数据量通常几十到几百条两种都够用。我一般会推荐 SQL 窗口函数因为代码简洁而且能体现你对数据库高级特性的掌握。注意更新 rank 字段时一定要加事务。如果更新到一半失败排名数据就乱了。在 Service 层用 Connection.setAutoCommit(false)全部更新完再 commitcatch 到异常就 rollback。3.3 赛事状态流转用定时任务还是手动触发赛事状态从「未开始」到「报名中」再到「进行中」「已结束」这个流转怎么控制常见做法有两种一种是用 Spring Task 或 Quartz 定时扫描 competition 表根据 start_time 和 end_time 自动更新 status另一种是在管理员后台提供手动切换按钮。毕设环境下我建议用定时任务加手动触发结合。定时任务每分钟跑一次检查当前时间是否在报名时间段内自动把 status 从 0 改成 1或者从 1 改成 2。同时保留管理员手动修改的入口防止自动逻辑出问题时能兜底。// 定时任务每分钟检查赛事状态 Scheduled(cron 0 * * * * ?) public void updateCompetitionStatus() { ListCompetition list competitionDao.findAll(); Date now new Date(); for (Competition c : list) { if (now.after(c.getStartTime()) now.before(c.getEndTime())) { if (c.getStatus() 0) { competitionDao.updateStatus(c.getId(), 1); // 改为报名中 } } else if (now.after(c.getEndTime())) { if (c.getStatus() ! 3) { competitionDao.updateStatus(c.getId(), 3); // 改为已结束 } } } }如果不用 Spring纯 JavaWeb 项目可以用 ServletContextListener 加 Timer 实现。在 contextInitialized 里启动一个 TimerTask每隔 60 秒执行一次上面的逻辑。注意在 contextDestroyed 里取消 Timer否则 Tomcat 关闭时会报线程泄漏警告。4. 核心功能实现报名审核、成绩录入与权限拦截4.1 报名审核的 Servlet 与 JSP 配合从表单提交到列表展示报名功能是体育竞赛管理系统里最典型的增删改查场景。运动员在前台点击「报名」提交到 RegistrationServletServlet 调用 Service 和 DAO 插入 registration 表然后跳转到「我的报名」页面。管理员在后台看到待审核列表点击「通过」或「拒绝」更新 status 字段。先看 Servlet 的 doPost 方法// RegistrationServlet.java 核心代码 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String action request.getParameter(action); HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (register.equals(action)) { int competitionId Integer.parseInt(request.getParameter(competitionId)); // 检查是否已报名 if (registrationService.isRegistered(competitionId, user.getId())) { request.setAttribute(msg, 您已报名该赛事请勿重复提交); request.getRequestDispatcher(/views/competition/list.jsp).forward(request, response); return; } Registration reg new Registration(); reg.setCompetitionId(competitionId); reg.setUserId(user.getId()); reg.setStatus(0); // 待审核 registrationService.add(reg); response.sendRedirect(request.getContextPath() /registration?actionmyList); } else if (audit.equals(action)) { // 管理员审核逻辑 int regId Integer.parseInt(request.getParameter(regId)); int status Integer.parseInt(request.getParameter(status)); registrationService.updateStatus(regId, status); response.sendRedirect(request.getContextPath() /registration?actionauditList); } }这段代码有几个关键点。第一request.setCharacterEncoding(UTF-8) 必须放在获取参数之前否则中文参数会乱码。第二用 session.getAttribute(loginUser) 获取当前登录用户这是权限控制的基础。第三报名前先查是否已报名虽然数据库有唯一索引兜底但提前检查能给用户更友好的提示。第四审核操作直接更新 status没有做额外的权限校验实际项目中应该在 Service 层判断当前用户 role 是否为管理员。对应的 JSP 页面用 JSTL 遍历报名列表!-- 我的报名列表 -- table classtable table-striped thead tr th赛事名称/th th报名时间/th th状态/th th操作/th /tr /thead tbody c:forEach items${registrationList} varreg tr td${reg.competitionName}/td tdfmt:formatDate value${reg.registerTime} patternyyyy-MM-dd HH:mm//td td c:choose c:when test${reg.status 0}span classbadge bg-warning待审核/span/c:when c:when test${reg.status 1}span classbadge bg-success已通过/span/c:when c:otherwisespan classbadge bg-danger已拒绝/span/c:otherwise /c:choose /td td c:if test${reg.status 0} a hrefregistration?actioncancelid${reg.id} classbtn btn-sm btn-outline-danger取消报名/a /c:if /td /tr /c:forEach /tbody /tableJSTL 的 c:choose 用来根据 status 显示不同颜色的徽章这是 Bootstrap 的 badge 类。fmt:formatDate 格式化时间注意引入 JSTL 的 fmt 标签库。如果 registrationList 为空页面会显示空表格可以在 table 外面加一个 c:if 判断显示「暂无报名记录」。4.2 成绩录入的批量提交与校验裁判端怎么少点几次鼠标成绩录入是裁判端的主要操作。一个赛事可能有几十个运动员如果每个成绩都单独提交一次裁判会点到手软。常见做法是提供一个批量录入页面表格里每行一个运动员裁判填完所有成绩后一次性提交。前端用 form 包裹整个表格每个输入框的 name 设为 score_${userId}提交到 ResultServlet。后端遍历 request.getParameterMap()解析出每个运动员的成绩。// ResultServlet.java 批量录入成绩 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); int competitionId Integer.parseInt(request.getParameter(competitionId)); User judge (User) request.getSession().getAttribute(loginUser); MapString, String[] paramMap request.getParameterMap(); ListResult resultList new ArrayList(); for (Map.EntryString, String[] entry : paramMap.entrySet()) { String key entry.getKey(); if (key.startsWith(score_)) { int userId Integer.parseInt(key.substring(6)); String scoreStr entry.getValue()[0]; if (scoreStr null || scoreStr.trim().isEmpty()) { continue; // 跳过未填写的成绩 } BigDecimal score new BigDecimal(scoreStr); // 校验成绩范围比如 0 到 1000 秒 if (score.compareTo(BigDecimal.ZERO) 0 || score.compareTo(new BigDecimal(1000)) 0) { request.setAttribute(msg, 成绩必须在 0 到 1000 之间); request.getRequestDispatcher(/views/result/input.jsp).forward(request, response); return; } Result r new Result(); r.setCompetitionId(competitionId); r.setUserId(userId); r.setScore(score); r.setJudgeId(judge.getId()); resultList.add(r); } } resultService.batchSave(resultList); // 保存后重新计算排名 resultService.calculateRank(competitionId); response.sendRedirect(request.getContextPath() /result?actionlistcompetitionId competitionId); }这段代码的核心是遍历 parameterMap用 key 的前缀 score_ 识别成绩字段。substring(6) 是因为 score_ 正好 6 个字符。校验成绩范围时用 BigDecimal 比较避免浮点数精度问题。batchSave 在 Service 层用事务批量插入calculateRank 调用前面说的排名更新逻辑。提示批量录入时如果某个运动员已经录过成绩应该走更新而不是插入。可以在 batchSave 里先查 result 表存在就 update不存在就 insert。或者用 MySQL 的 INSERT ... ON DUPLICATE KEY UPDATE前提是 competition_id 和 user_id 建了唯一索引。4.3 登录拦截与角色权限Filter 里该放行哪些路径权限控制是体育竞赛管理系统的安全底线。没有拦截的话运动员直接访问管理员页面就能改数据。JavaWeb 里用 Filter 实现拦截是最标准的做法。// AuthFilter.java 登录与角色拦截 public class AuthFilter implements Filter { // 不需要登录就能访问的路径 private static final String[] WHITE_LIST {/login.jsp, /login, /register.jsp, /user?actionregister}; 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(); String contextPath request.getContextPath(); String path uri.substring(contextPath.length()); // 放行静态资源 if (path.startsWith(/css/) || path.startsWith(/js/) || path.startsWith(/images/)) { chain.doFilter(req, resp); return; } // 放行白名单 for (String white : WHITE_LIST) { if (path.equals(white)) { chain.doFilter(req, resp); return; } } // 检查登录状态 HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { response.sendRedirect(contextPath /login.jsp); return; } // 管理员路径检查 if (path.startsWith(/admin/) user.getRole() ! 2) { response.sendError(403, 无权访问); return; } // 裁判路径检查 if (path.startsWith(/judge/) user.getRole() ! 1 user.getRole() ! 2) { response.sendError(403, 无权访问); return; } chain.doFilter(req, resp); } }Filter 在 web.xml 里配置或者用 WebFilter 注解。注意 urlPatterns 设为 /*拦截所有请求然后在 Filter 内部放行白名单和静态资源。角色判断用 user.getRole()管理员是 2裁判是 1运动员是 0。管理员可以访问所有路径裁判只能访问 /judge/ 和公共路径运动员只能访问公共路径。这里有个容易翻车的点如果 JSP 页面放在 WEB-INF 下浏览器不能直接访问必须通过 Servlet 转发。这是好事因为 WEB-INF 下的 JSP 天然受保护。但如果 JSP 放在 web 根目录下用户可以直接输入 URL 访问Filter 必须拦截 .jsp 请求。我一般会把所有 JSP 放在 WEB-INF/views/ 下只留 login.jsp 和 register.jsp 在根目录。5. 避坑与排查那些让毕设卡三天的典型问题5.1 中文乱码从 Tomcat 到 MySQL 的完整排查链现象表单提交中文数据库里存的是问号或者 JSP 页面显示中文变成乱码。原因乱码可能出现在三个环节——Tomcat 的 URI 编码、request 的字符编码、MySQL 的连接字符集。Tomcat 8 以后默认 URI 编码是 UTF-8但 request.getParameter 的默认编码是 ISO-8859-1。MySQL 的连接 URL 如果没加 characterEncodingutf8也会导致乱码。解决第一在 Servlet 里 request.setCharacterEncoding(UTF-8) 放在第一行。第二response.setContentType(text/html;charsetUTF-8)。第三MySQL URL 加 characterEncodingutf8。第四JSP 页面顶部加 % page contentTypetext/html;charsetUTF-8 languagejava %。第五如果用了 Tomcat 的 server.xmlConnector 标签加 URIEncodingUTF-8。这五步走完乱码基本消失。5.2 数据库连接池报错Too many connections 与连接泄漏现象项目跑一段时间后报「Too many connections」或者 Tomcat 日志里出现「Cannot get a connection, pool exhausted」。原因Connection 没有关闭或者关闭顺序不对。很多人写了 conn.close() 但忘了在 finally 里执行一旦中间抛异常连接就泄漏了。另外Druid 的 maxActive 设得太大MySQL 的 max_connections 默认是 151超过就拒绝。解决用 try-with-resources 自动关闭 Connection、PreparedStatement、ResultSet。Druid 的 maxActive 设为 20 到 50 之间initialSize 设为 5。在 DBUtil 里加一个 removeAbandoned 配置Druid 会自动回收长时间未关闭的连接。定期用 SHOW PROCESSLIST 查看 MySQL 的连接数如果发现大量 Sleep 状态的连接说明有泄漏。5.3 JSP 页面报 404路径到底该怎么写现象Servlet 转发到 JSP 时报 404或者 JSP 里的链接点击后跳转错误。原因路径分相对路径和绝对路径。response.sendRedirect 和 request.getRequestDispatcher 的路径规则不同。sendRedirect 是浏览器行为路径相对于当前 URLgetRequestDispatcher 是服务器行为路径相对于 context root。JSP 里的 和如果不加 contextPath部署到 Tomcat 后路径会错。解决统一用 request.getContextPath() 获取上下文路径。Servlet 里 sendRedirect 写 request.getContextPath() /target。JSP 里用 ${pageContext.request.contextPath}/target。getRequestDispatcher 写 /WEB-INF/views/xxx.jsp不要加 contextPath。记住一个原则服务器端转发用相对 context root 的路径客户端重定向和链接用带 contextPath 的绝对路径。5.4 事务不生效Service 层加注解为什么没回滚现象批量插入成绩时中间一条失败前面的数据却已经写入数据库没有回滚。原因纯 JavaWeb 项目没有 Spring 的事务管理Transactional 注解不生效。很多人从 SpringBoot 转过来习惯性加注解结果发现没用。另外如果 Service 里自己获取 Connection 并 setAutoCommit(true)事务也不会生效。解决在 Service 层手动控制事务。从 DBUtil 获取 ConnectionsetAutoCommit(false)执行多个 DAO 操作全部成功就 commitcatch 到异常就 rollbackfinally 里 close。注意 DAO 层的方法要接收 Connection 参数不要自己从连接池获取否则不在同一个事务里。// Service 层手动事务 public void batchSave(ListResult list) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); for (Result r : list) { resultDao.insert(conn, r); } conn.commit(); } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException(批量保存失败, e); } finally { DBUtil.close(conn); } }5.5 答辩被问「你的项目有什么创新点」怎么答现象答辩老师觉得体育竞赛管理系统太普通问创新点在哪。原因毕设选题确实常见但创新点不一定是技术上的颠覆可以是业务上的细化或实现上的优化。解决从三个角度准备。第一业务角度你做了赛事状态自动流转、成绩并列排名处理、报名冲突检测这些是实际比赛中的真实需求。第二技术角度你用了 Druid 连接池、Filter 权限拦截、事务手动控制能说清楚为什么这么选。第三数据角度你的表结构有联合唯一索引、有状态字段、有排名计算能画出 ER 图并解释每张表的作用。不要硬吹用了 AI 或大数据老师一听就知道是假的。6. 进阶技巧用 JMeter 压测报名接口并定位瓶颈项目功能跑通之后如果你想在毕设里体现一点性能意识可以用 JMeter 对报名接口做个简单压测。这不是必须的但能让你在答辩时多一个可量化的数据点。先下载 JMeter解压后运行 bin/jmeter.bat。新建测试计划添加线程组线程数设为 50Ramp-Up 设为 10 秒循环次数设为 10。添加 HTTP 请求服务器填 localhost端口 8080路径填 /sports/registration方法选 POST参数加 actionregister、competitionId1。再添加一个 HTTP Cookie 管理器因为报名需要登录 Session。最后添加聚合报告和查看结果树。跑完之后看聚合报告里的吞吐量和 90% 响应时间。如果 50 并发下响应时间超过 2 秒说明有瓶颈。常见瓶颈有三个数据库连接池不够用、SQL 没有索引、Session 同步开销。连接池问题把 maxActive 调到 50SQL 问题在 registration 表的 competition_id 和 user_id 上加索引Session 问题在 Tomcat 的 context.xml 里加 禁用 Session 持久化。我自己的习惯是每次改完 SQL 或连接池参数重新跑一次 JMeter对比聚合报告里的数据。有一次把 maxActive 从 20 调到 50吞吐量从 120 涨到 280但响应时间反而变长了原因是 MySQL 的 max_connections 没调连接排队了。后来把 MySQL 的 max_connections 改成 500问题才解决。这个排查过程写进毕设论文的「性能测试」章节比空谈理论有说服力得多。压测数据不用太漂亮毕设答辩老师更看重你知不知道瓶颈在哪、怎么定位、怎么优化。哪怕只把响应时间从 3 秒降到 1 秒只要能说清楚每一步改了什么、为什么改就是加分项。希望帮到你。本文还有配套的精品资源点击获取