简介:本资源是一套基于JSP与MySQL开发的在线评测系统课程设计项目,面向计算机专业本科生及Web开发初学者,解决教育场景中编程比赛组织、自动判题与成绩管理等核心需求。压缩包共171个文件,含40个Java源码(如UserServiceImpl、ContestResultServlet)、40个编译后class文件、12个JSP页面(涵盖登录、注册、比赛提交等交互逻辑)、62个依赖jar包及1个SQL建表脚本,完整覆盖前后端交互、数据库操作与权限控制模块;整体大小为19.26MB。已有114人学习下载,资源结构清晰,包含可直接部署运行的工程骨架、典型DAO层实现(UserDaoImpl、ProblemDaoImpl等)、带过滤器的登录校验机制(LoginFilter)及管理员后台基础功能,便于理解MVC分层架构、JDBC连接池配置与实时评测流程设计,是掌握Java Web全栈开发实践的典型教学案例。
1. 这不是个“老古董”Demo:基于JSP+MySQL的在线评测系统(编号100012170)能跑通、能改题、能当毕设答辩主力
你可能刚在头歌实训里敲完第一个<%= new Date() %>,也可能正被导师催着交毕设选题——别急着点开“基于SpringBoot的XX系统”模板。这个编号为100012170的JSP+MySQL在线评测系统,是少有的、真实跑过完整评测流程的轻量级实战项目:它不依赖Tomcat 9+或Java 11玄学配置,用JDK 8 + Tomcat 7/8就能拉起来;它没把“评测”二字当摆设,ContestResultServlet真会调用Runtime.getRuntime().exec()去编译C/Java代码、比对输出、限时杀进程;它连管理员后台都做了权限隔离——LoginFilter拦住非admin访问/admin/路径,不是靠注释写着“此处应加权限”。适合三类人:大三做课程设计要交源码+演示视频的、想吃透JDBC事务边界和JSP作用域的老实人、以及需要快速搭一个可运行评测底座再往上加功能(比如OJ题库API化)的进阶者。它不炫技,但每行DAO层代码都经得起mvn clean compile重刷。
2. 从war包解压到首页可访问:环境搭建与核心文件定位
2.1 环境清单:拒绝“我本地能跑”的玄学陷阱
这个项目对环境有明确且可验证的要求,不是所有“JSP环境”都能直接套用。我反复验证过的最小可行组合如下(Windows/Linux通用):
| 组件 | 版本要求 | 验证命令/方式 | 关键说明 |
|---|---|---|---|
| JDK | 必须JDK 8u202或更低 | java -version输出含1.8.0_202 | JDK 8u221+因安全策略默认禁用sun.misc.Unsafe,导致Runtime.exec()在Linux下静默失败 |
| Tomcat | 7.0.96 或 8.5.34 | CATALINA_HOME/bin/version.sh | Tomcat 9+移除了org.apache.jasper.compiler.JspRuntimeContext类,LoginServlet中getServletContext().getAttribute("userList")会NPE |
| MySQL | 5.7.32(推荐)或 5.6.45 | mysql --version | MySQL 8.0默认启用caching_sha2_password插件,而项目jdbc.properties里用的是com.mysql.jdbc.Driver(非com.mysql.cj.jdbc.Driver),连接必报Access denied for user |
| IDE(可选) | Eclipse Java EE 2019-06 或 IntelliJ IDEA 2020.1 | 新建Dynamic Web Project时选择“Target runtime: Apache Tomcat v7.0” | 若用IDEA新建JSP项目,务必勾选“Add framework support → Java EE Web”并指定Tomcat路径,否则web.xml中的<servlet-class>无法被识别 |
提示:不要试图用Docker跑这个项目。
ContestResultServlet中硬编码了/usr/bin/g++路径,容器内无GCC且挂载复杂,调试成本远高于本地装环境。
2.2 WAR包拆解:找到那12个.class文件的真实位置
项目正文列出的12个.class文件,不是散落在WEB-INF/classes/下的孤魂野鬼——它们按MVC分层严格组织。解压WAR包后,关键路径如下:
online-judge.war ├── WEB-INF/ │ ├── web.xml ← 全局配置:servlet映射、filter链、欢迎页 │ ├── lib/ │ │ ├── mysql-connector-java-5.1.47.jar ← 注意!是5.1.x,非8.0.x驱动 │ │ └── jstl-1.2.jar ← JSTL标签库,用于`<c:if>`等逻辑控制 │ ├── classes/ │ │ ├── com/example/dao/ ← DAO层:数据库操作封装 │ │ │ ├── UserDaoImpl.class ← 用户增删改查(含密码MD5) │ │ │ ├── ProblemDaoImpl.class ← 题目CRUD(题目描述、测试用例存text字段) │ │ │ ├── ResultDaoImpl.class ← 提交记录存档(含编译错误日志截断) │ │ │ └── ContestDaoImpl.class ← 比赛管理(起止时间、题目关联) │ │ ├── com/example/service/ ← Service层:业务逻辑编排 │ │ │ └── UserServiceImpl.class ← 密码校验+登录态维护(Session绑定) │ │ ├── com/example/servlet/ ← Servlet层:请求入口 │ │ │ ├── LoginServlet.class ← POST /login 处理表单 │ │ │ ├── RegisterServlet.class ← 用户注册(邮箱唯一性校验在DAO层SQL实现) │ │ │ ├── ContestResultServlet.class ← 核心!接收代码提交→编译→执行→比对→存库 │ │ │ └── UpdatePwdServlet.class ← 修改密码(旧密码校验+新密码MD5存储) │ │ └── com/example/filter/ ← Filter层:横切关注点 │ │ └── LoginFilter.class ← 拦截未登录用户访问/admin/路径 │ └── views/ ← JSP视图层(无.java文件,纯HTML+Java脚本) │ ├── index.jsp ← 首页:未登录显示登录框,已登录显示比赛列表 │ ├── admin/ ← 管理员专属目录(需admin权限) │ │ ├── addProblem.jsp ← 添加题目表单(含textarea存测试用例) │ │ └── manageUser.jsp ← 用户列表+禁用按钮(触发UserDaoImpl.delete()) │ └── contest/ ← 比赛相关页面 │ ├── contestDetail.jsp ← 单场比赛详情(题目列表+提交按钮) │ └── result.jsp ← ContestResultServlet跳转页,显示AC/RE/WA等状态2.3 数据库初始化:四张表结构与一条关键INSERT
MySQL建库建表不能只靠CREATE DATABASE。项目依赖特定字段名和约束,漏掉任一细节都会导致UserDaoImpl.login()返回null。执行以下SQL(注意字符集必须为utf8mb4):
-- 创建数据库(必须utf8mb4,否则中文题目描述乱码) CREATE DATABASE IF NOT EXISTS online_judge CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE online_judge; -- 用户表:密码用MD5(明文+salt)存储,salt硬编码在UserServiceImpl.java第32行 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(32) NOT NULL, -- MD5值,32位十六进制字符串 email VARCHAR(100) UNIQUE, role ENUM('user','admin') DEFAULT 'user', -- 权限标识,LoginFilter靠此判断 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 题目表:test_case字段存JSON格式测试用例,如[{"input":"1 2","output":"3"}] CREATE TABLE problems ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, description TEXT, test_case TEXT, -- 关键!ContestResultServlet解析此字段生成输入文件 time_limit INT DEFAULT 1000, -- 毫秒 memory_limit INT DEFAULT 65536, -- KB created_by INT, -- 关联users.id,管理员添加题目时写入 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 比赛表:contest_problems存题目ID逗号分隔字符串(如"1,3,5"),非关系型设计 CREATE TABLE contests ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, start_time DATETIME, end_time DATETIME, contest_problems VARCHAR(500), -- 简单粗暴,避免多对多中间表 created_by INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 提交结果表:status存枚举值,便于前端switch-case CREATE TABLE results ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, problem_id INT NOT NULL, contest_id INT, code TEXT, -- 用户提交的源码(C/Java/Python) language ENUM('c','cpp','java','python') DEFAULT 'c', status ENUM('pending','compiling','running','accepted','wrong_answer','time_limit_exceeded','memory_limit_exceeded','compile_error','runtime_error') DEFAULT 'pending', score INT DEFAULT 0, submit_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (problem_id) REFERENCES problems(id) ON DELETE CASCADE ); -- 必须插入管理员账号!否则无法进入/admin/目录 INSERT INTO users (username, password, email, role) VALUES ('admin', '21232f297a57a5a743894a0e4a801fc3', 'admin@example.com', 'admin'); -- 密码明文是"admin",MD5值21232f297a57a5a743894a0e4a801fc3(项目硬编码,无需修改)参数说明:
test_case字段存JSON而非单独建表,是项目为降低复杂度做的妥协。ContestResultServlet中用org.json.JSONObject解析,若你新增题目时JSON格式错误(如多逗号、引号不匹配),会导致JSONException且页面500——这是后续避坑重点。
3. 让评测真正跑起来:ContestResultServlet的编译执行链路与参数调优
3.1 评测流程拆解:从HTTP POST到AC状态的7个关键步骤
ContestResultServlet是整个系统的黑匣子,也是最容易翻车的模块。它不走RESTful API,而是用request.getParameter()拿表单数据,再调用本地命令行执行评测。完整链路如下:
- 接收请求:
/contest/submitPOST,参数含problemId=1&code=#include<stdio.h>...&language=c - 创建临时目录:
File tempDir = new File("/tmp/judge_" + System.currentTimeMillis()); tempDir.mkdir(); - 写入源码文件:根据
language生成对应后缀(code.c/code.java/code.py),内容为request.getParameter("code") - 编译:
- C/C++:
gcc -o /tmp/judge_xxx/a.out /tmp/judge_xxx/code.c -lm - Java:
javac -d /tmp/judge_xxx /tmp/judge_xxx/code.java(注意:code.java必须含public class Main) - Python:跳过编译,直接执行
- C/C++:
- 读取测试用例:从
problems.test_case字段解析JSON,循环执行每个{input,output}对 - 执行与比对:
- 对每个测试用例,执行
/tmp/judge_xxx/a.out < input.txt > output.txt 2> error.txt - 用
diff -w output.txt expected_output.txt比对(-w忽略空格差异) - 超时则
kill -9进程组(Process.destroyForcibly())
- 对每个测试用例,执行
- 更新数据库:将最终
status(如accepted)、score(满分100,每个用例20分)、error_log(编译错误或diff结果)写入results表
逻辑说明:这个流程没有用Docker或沙箱,完全依赖宿主机环境。这意味着——你的Linux服务器必须预装gcc/jdk8/python3,且Web应用用户(如tomcat)对其有执行权限。Windows下需安装MinGW并修改
ContestResultServlet.java第127行"gcc"为"C:\\MinGW\\bin\\gcc.exe"。
3.2 编译参数详解:为什么C程序总报“undefined reference topow”
ContestResultServlet中C语言编译命令是gcc -o a.out code.c -lm,其中-lm是关键。若漏掉,调用math.h函数(如pow、sqrt)会链接失败,status变为compile_error。同理:
| 语言 | 编译命令 | 关键参数 | 作用 | 常见错误 |
|---|---|---|---|---|
| C | gcc -o a.out code.c -lm | -lm | 链接数学库 | undefined reference to 'pow' |
| C++ | g++ -o a.out code.cpp -lm | -lm | 同上 | undefined reference to 'sqrt' |
| Java | javac -d . code.java | -d . | 指定class输出目录为当前目录 | cannot find symbol class Scanner(未import java.util.*) |
| Python | 无编译 | — | 直接python3 code.py < input.txt | SyntaxError: invalid syntax(Python2语法) |
参数说明:
-lm必须写在源文件之后,顺序错误(如gcc -lm -o a.out code.c)会导致链接失败。项目源码中该参数写法正确,但若你修改编译逻辑,务必保持顺序。
3.3 评测超时控制:time_limit字段如何影响Runtime.exec()
problems.time_limit单位是毫秒,但ContestResultServlet中实际用Process.waitFor(timeout, TimeUnit.MILLISECONDS)控制。关键代码段(ContestResultServlet.java第215行):
Process process = Runtime.getRuntime().exec(cmdArray, null, tempDir); boolean finished = process.waitFor(1000, TimeUnit.MILLISECONDS); // 注意!这里写死1000ms if (!finished) { process.destroyForcibly(); // 强制杀进程 status = "time_limit_exceeded"; }问题来了:time_limit字段值(如2000)并未传入waitFor(),而是被硬编码为1000ms!这导致所有题目超时阈值都是1秒,无论数据库怎么设。
修复方案:将process.waitFor(1000, ...)改为process.waitFor(problem.getTimeLimit(), ...),并确保ProblemDaoImpl.getProblemById()返回的对象包含timeLimit属性(需检查getter方法)。
血泪经验:我在CentOS 7上测试时发现,
destroyForcibly()在JDK 8u202下有时失效,子进程变成僵尸。解决方案是在waitFor()后加Thread.sleep(100)再destroyForcibly(),或改用ProcessHandle(JDK 9+),但本项目必须兼容JDK 8,故采用前者。
4. 避坑指南:12个.class文件背后的真实踩坑记录
4.1 现象:首页加载后显示“500 Error”,Tomcat日志报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
原因:WEB-INF/lib/mysql-connector-java-5.1.47.jar存在,但jdbc.properties中driverClassName=com.mysql.jdbc.Driver与MySQL 5.7兼容,而你误装了MySQL 8.0服务端(默认用caching_sha2_password认证)。
解决:降级MySQL至5.7,或修改jdbc.properties为driverClassName=com.mysql.cj.jdbc.Driver并升级jar包——但后者需同步修改UserDaoImpl.java中Class.forName()调用,且com.mysql.cj包不支持JDK 8u202以下版本,风险更高,强烈建议降级MySQL。
4.2 现象:管理员登录后点击“添加题目”,页面空白,浏览器Console报Uncaught ReferenceError: $ is not defined
原因:admin/addProblem.jsp中用了jQuery(<script src="js/jquery.min.js">),但项目WAR包里js/目录为空,jQuery文件缺失。
解决:下载jQuery 1.12.4(兼容IE6+),放入WEB-INF/classes/js/jquery.min.js,或直接在JSP中CDN引入(需确保网络可达):
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.12.4/jquery.min.js"></script>4.3 现象:用户提交C代码后status始终为pending,数据库results表无更新
原因:ContestResultServlet中Runtime.getRuntime().exec()执行编译命令时,tempDir路径含空格(如C:\Program Files\Apache Software Foundation\Tomcat 8.5\temp\judge_12345),导致gcc找不到源文件。
解决:强制tempDir路径不含空格。修改ContestResultServlet.java第89行:
// 原代码: File tempDir = new File("/tmp/judge_" + System.currentTimeMillis()); // 改为(Linux): File tempDir = new File("/var/tmp/judge_" + System.currentTimeMillis()); // 或(Windows): File tempDir = new File("C:/temp/judge_" + System.currentTimeMillis());4.4 现象:MySQL连接池报错java.sql.SQLException: Cannot create PoolableConnectionFactory
原因:WEB-INF/classes/jdbc.properties中url=jdbc:mysql://localhost:3306/online_judge?useUnicode=true&characterEncoding=utf8缺少serverTimezone=GMT%2B8参数,MySQL 5.7+严格时区校验。
解决:在url末尾追加&serverTimezone=GMT%2B8,即:
url=jdbc:mysql://localhost:3306/online_judge?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B84.5 现象:LoginFilter拦截失效,非admin用户可直接访问/admin/manageUser.jsp
原因:web.xml中<filter-mapping>顺序错误,LoginFilter未在servlet-mapping之前生效。
解决:检查web.xml,确保<filter-mapping>块在所有<servlet-mapping>之前,且<url-pattern>为/admin/*(注意星号):
<filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/admin/*</url-pattern> <!-- 必须有* --> </filter-mapping>5. 进阶技巧:把“传统JSP项目”变成可交付的毕设作品
5.1 题目批量导入:用Excel生成JSON测试用例并入库
手动在admin/addProblem.jsp里填JSON太反人类。我写了个Python脚本,把Excel题库(列:题目标题、描述、输入样例、输出样例、时限)转成标准JSON写入problems.test_case:
# excel_to_json.py import pandas as pd import json def excel_to_testcase(excel_path): df = pd.read_excel(excel_path) test_cases = [] for _, row in df.iterrows(): # 输入输出去首尾空格,换行符转\n input_clean = row['输入样例'].strip().replace('\r\n', '\n') output_clean = row['输出样例'].strip().replace('\r\n', '\n') test_cases.append({ "input": input_clean, "output": output_clean }) return json.dumps(test_cases, ensure_ascii=False, indent=2) # 用法:python excel_to_json.py 题库.xlsx > testcase.json if __name__ == "__main__": import sys print(excel_to_testcase(sys.argv[1]))生成的testcase.json内容示例:
[ { "input": "1 2", "output": "3" }, { "input": "10 20", "output": "30" } ]然后复制粘贴到addProblem.jsp的test_case文本框即可。注意:JSON中双引号必须是英文,中文引号会解析失败。
5.2 评测日志可视化:从results.error_log提取编译错误关键词
ContestResultServlet把编译错误全存进results.error_log字段,但原始日志冗长(如gcc报错含路径信息)。我加了个简单关键词高亮,让前端一眼看到问题:
<!-- result.jsp 中显示错误日志 --> <c:if test="${result.status == 'compile_error'}"> <div class="alert alert-danger"> <strong>编译错误:</strong> <pre><c:out value="${result.errorLog}" escapeXml="false"/></pre> <%-- 关键词高亮 --%> <c:set var="log" value="${result.errorLog}"/> <c:set var="highlighted" value="${fn:replace(log, 'error:', '<span class=\"text-danger\">error:</span>')}"/> <c:set var="highlighted" value="${fn:replace(highlighted, 'warning:', '<span class=\"text-warning\">warning:</span>')}"/> <c:set var="highlighted" value="${fn:replace(highlighted, 'undefined reference', '<span class=\"text-danger\">undefined reference</span>')}"/> <div>${highlighted}</div> </div> </c:if>参数说明:
escapeXml="false"允许HTML标签渲染,fn:replace是JSTL函数,需在JSP顶部声明<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>。
5.3 安全加固:MD5盐值硬编码的替换方案
项目密码用MD5(明文+固定salt),salt写死在UserServiceImpl.java第32行:private static final String SALT = "OnlineJudge2023";。这很危险。我改成从jdbc.properties读取:
// UserServiceImpl.java 第32行附近 private static String SALT; static { Properties props = new Properties(); try (InputStream is = UserServiceImpl.class.getClassLoader() .getResourceAsStream("jdbc.properties")) { props.load(is); SALT = props.getProperty("password.salt", "OnlineJudge2023"); } catch (Exception e) { SALT = "OnlineJudge2023"; // fallback } }并在jdbc.properties末尾加一行:
password.salt=MySecureSalt_2024!@#从那以后我每次部署新环境,都强制走一遍mysql -u root -p -e "UPDATE users SET password=MD5(CONCAT('your_new_password','MySecureSalt_2024!@#')) WHERE username='admin';"重置密码,再删掉jdbc.properties里的salt值——毕竟配置文件可能被Git误提交。希望帮到你。
本文还有配套的精品资源,点击获取