多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

JSP+MySQL在线评测系统:高校OJ实战搭建与判题引擎设计

JSP+MySQL在线评测系统:高校OJ实战搭建与判题引擎设计 简介本资源是一套基于JSP与MySQL开发的课程设计级在线评测系统面向计算机专业本科生及Web开发初学者解决编程类课程中作业提交、自动判题与成绩管理等教学场景的实际需求。压缩包共171个文件含40个Java源码如UserServiceImpl、ContestResultServlet、40个编译后class文件、12个JSP页面涵盖登录、注册、比赛展示等核心交互、62个运行依赖jar包以及SQL建表脚本、CSS/JS前端资源和项目配置文件整体大小19.26MB结构完整可直接部署运行。已有114人学习下载资源提供从用户认证、题库管理、比赛组织到实时评测反馈的全链路实现包含权限控制逻辑LoginFilter、管理员后台入口、数据库DAO层封装UserDaoImpl、ProblemDaoImpl等及典型MVC分层结构是理解Web应用开发流程与教育信息化系统设计的优质实践案例。1. 为什么用 JSP MySQL 做在线评测系统不是“过时技术”而是稳扎稳打的工程选择你可能刚看到“JSP MySQL”就皱眉这不都是十年前的老古董Spring Boot 都卷到 3.3 了还写 Servlet但现实是——高校课程设计、职业院校实训平台、中小教育机构内部题库系统90% 的「能跑、能改、能交接、能维护」的在线评测系统底层仍是 JSP MySQL 构建。它不炫技但胜在零依赖容器、单机可部署、教师自己能改页面、运维只管 Tomcat 和 MySQL 服务。这不是怀旧是权衡当你要在一台 4 核 8G 的旧服务器上让 200 名学生同时提交代码、实时判题、查看排名、导出成绩单且未来三年没人专职维护时JSP 的 request/response 显式控制力、MySQL 的事务确定性、以及整个技术栈对 Windows Server / CentOS 7 的原生兼容性反而成了最硬的护城河。本项目编号【100012170】正是某省高职院校信息工程系真实落地的 OJOnline Judge系统支撑其《Java 程序设计》《数据结构》两门课连续 4 个学期的自动评测核心判题模块至今未重启过一次。下面我带你从零复现这个“不时髦但扛用”的系统重点不是教你怎么写% %而是讲清楚判题逻辑怎么嵌进 JSP 流程、MySQL 怎么存测试用例与运行日志、为什么不用连接池反而更稳、以及那些只有在真实机房断电后才暴露出的坑。2. 搭建最小可用环境Tomcat 9 MySQL 5.7 JDK 8三步验证是否 ready提示别急着下载最新版。本系统实测在 Tomcat 9.0.87 MySQL 5.7.44 JDK 1.8.0_391 组合下最稳定。MySQL 8.0 的默认认证插件caching_sha2_password会导致 JSP 连接失败而 Tomcat 10 的 Jakarta EE 命名空间会彻底破坏 JSP EL 表达式解析——这些不是 bug是生态断层。2.1 安装与校验用netstat和telnet确认端口真通不是“启动成功”幻觉先确认基础服务状态。很多翻车始于“以为启动了”# 检查 Tomcat 是否监听 8080注意不是看 startup.bat 弹窗是看端口 netstat -ano | findstr :8080 # 检查 MySQL 是否监听 3306Windows 下 netstat -ano | findstr :3306 # 用 telnet 实测连通性若无 telnet先启用dism /online /Enable-Feature /FeatureName:TelnetClient telnet localhost 8080 # 应返回 HTTP 头或空白响应 telnet localhost 3306 # 应返回 MySQL 协议握手包乱码开头但有响应如果telnet localhost 3306超时90% 是 MySQL 未配置bind-address 0.0.0.0或防火墙拦截若telnet localhost 8080失败检查conf/server.xml中Connector port8080是否被注释以及logs/catalina.out是否有SEVERE: Failed to initialize connector报错。2.2 数据库初始化建库、建表、插初始数据一条 SQL 不漏创建oj_system库并执行以下建表脚本注意字符集必须为utf8mb4否则中文题目描述会乱码-- 创建数据库显式指定字符集 CREATE DATABASE oj_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 切换数据库 USE oj_system; -- 用户表存储学生/教师账号密码用 MD5 存储非明文 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(32) NOT NULL, -- MD5 32位小写 role ENUM(student, teacher, admin) DEFAULT student, real_name VARCHAR(100), email VARCHAR(100), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 题目表核心字段设计直指评测需求 CREATE TABLE problems ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, description TEXT, input_desc TEXT, output_desc TEXT, sample_input TEXT, sample_output TEXT, time_limit INT DEFAULT 1000, -- 毫秒 memory_limit INT DEFAULT 65536, -- KB difficulty ENUM(easy, medium, hard) DEFAULT medium, status ENUM(visible, hidden) DEFAULT visible, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 提交记录表每次提交生成一条含判题关键字段 CREATE TABLE submissions ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, problem_id INT NOT NULL, code TEXT NOT NULL, language ENUM(java, cpp, python) DEFAULT java, status ENUM(pending, compiling, running, accepted, wrong_answer, time_limit_exceeded, memory_limit_exceeded, compile_error, runtime_error) DEFAULT pending, execute_time INT DEFAULT 0, -- 实际运行毫秒数 memory_used INT DEFAULT 0, -- 实际使用 KB judge_log TEXT, -- 编译错误/运行时异常堆栈 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 ); -- 测试用例表一对多关联题目支持多组输入输出 CREATE TABLE test_cases ( id INT PRIMARY KEY AUTO_INCREMENT, problem_id INT NOT NULL, input_data TEXT NOT NULL, expected_output TEXT NOT NULL, is_sample BOOLEAN DEFAULT FALSE, -- 是否为样例 FOREIGN KEY (problem_id) REFERENCES problems(id) ON DELETE CASCADE );执行后插入一条管理员用户和一道基础题目用于登录测试INSERT INTO users (username, password, role, real_name, email) VALUES (admin, 21232f297a57a5a743894a0e4a801fc3, admin, 系统管理员, adminoj.local); -- 密码 admin 的 MD5 小写值 INSERT INTO problems (title, description, input_desc, output_desc, sample_input, sample_output, time_limit, memory_limit) VALUES (AB Problem, 计算两个整数的和, 一行两个整数 a 和 b, 一行一个整数表示 ab, 1 2, 3, 1000, 65536);参数说明time_limit和memory_limit是判题机硬约束后续 Java 判题进程将严格按此限制启动status字段的枚举值必须与判题逻辑中返回的状态字符串完全一致否则前端无法映射图标test_cases表的is_sample字段用于前端区分“样例输入”和“隐藏测试点”避免学生靠样例硬编码。3. 核心判题引擎用 Java ProcessBuilder 启动沙箱进程而非调用 Runtime.exec判题不是简单javac java而是进程级隔离 资源限制 输出捕获 超时强杀。JSP 页面不能直接执行命令必须封装成 Java Bean在servlet或jsp:useBean中调用。3.1 编写JudgeEngine.java轻量但完整的判题逻辑新建src/com/oj/judge/JudgeEngine.java关键点在于使用ProcessBuilder而非Runtime.getRuntime().exec()前者可精确设置工作目录、环境变量、重定向流对 Java 提交先javac编译再java运行且类名强制为Main统一入口避免学生命名混乱对 C 提交用g -o main main.cpp编译再执行./main所有进程启动前设置ulimitLinux或JobObjectWindows限制 CPU 时间与内存这是防恶意死循环的核心输出捕获必须用BufferedReader分别读取stdout和stderr并设超时防止程序卡死不输出。// src/com/oj/judge/JudgeEngine.java package com.oj.judge; import java.io.*; import java.nio.file.*; import java.util.*; public class JudgeEngine { private static final String TEMP_DIR D:/oj_temp; // Windows 路径Linux 改为 /tmp/oj_temp public static JudgeResult judge(String code, String language, ListString inputs, ListString outputs) { JudgeResult result new JudgeResult(); Path tempDir null; try { // 1. 创建临时目录每个提交独立隔离 tempDir Files.createTempDirectory(Paths.get(TEMP_DIR), oj_); // 2. 写入源码文件 Path sourceFile writeSourceFile(tempDir, code, language); // 3. 编译若失败直接返回 compile_error if (!compile(sourceFile, language, tempDir)) { result.setStatus(compile_error); result.setJudgeLog(readFile(tempDir.resolve(compile.log))); return result; } // 4. 逐个运行测试用例 for (int i 0; i inputs.size(); i) { String input inputs.get(i); String expected outputs.get(i); ExecutionResult execRes execute(tempDir, language, input); if (!execRes.isSuccess()) { result.setStatus(execRes.getStatus()); result.setJudgeLog(execRes.getLog()); break; } // 比较输出忽略行尾空格、多余空行 if (!normalizeOutput(execRes.getOutput()).equals(normalizeOutput(expected))) { result.setStatus(wrong_answer); result.setJudgeLog(第 (i1) 组测试期望 expected 得到 execRes.getOutput() ); break; } // 记录本次耗时与内存取最大值 result.setExecuteTime(Math.max(result.getExecuteTime(), execRes.getTime())); result.setMemoryUsed(Math.max(result.getMemoryUsed(), execRes.getMemory())); if (i inputs.size() - 1) { result.setStatus(accepted); } } } catch (Exception e) { result.setStatus(runtime_error); result.setJudgeLog(判题引擎异常 e.getMessage()); } finally { // 清理临时目录重要否则磁盘爆满 if (tempDir ! null) { try { deleteDirectory(tempDir); } catch (IOException ignored) {} } } return result; } private static Path writeSourceFile(Path tempDir, String code, String language) throws IOException { String fileName; switch (language) { case java: fileName Main.java; break; case cpp: fileName main.cpp; break; case python: fileName main.py; break; default: throw new IllegalArgumentException(不支持的语言 language); } Path file tempDir.resolve(fileName); Files.write(file, code.getBytes(UTF-8)); return file; } private static boolean compile(Path sourceFile, String language, Path tempDir) throws IOException, InterruptedException { ListString cmd new ArrayList(); switch (language) { case java: cmd.addAll(Arrays.asList(javac, -encoding, UTF-8, -d, tempDir.toString(), sourceFile.toString())); break; case cpp: cmd.addAll(Arrays.asList(g, -o, tempDir.resolve(main).toString(), sourceFile.toString())); break; case python: return true; // Python 无需编译 } ProcessBuilder pb new ProcessBuilder(cmd); pb.directory(tempDir.toFile()); pb.redirectErrorStream(true); Process proc pb.start(); proc.waitFor(10, TimeUnit.SECONDS); // 编译超时 10 秒 if (proc.exitValue() ! 0) { Files.write(tempDir.resolve(compile.log), (编译失败命令 cmd \n readFile(tempDir.resolve(stdout))).getBytes(UTF-8)); return false; } return true; } private static ExecutionResult execute(Path tempDir, String language, String input) throws IOException, InterruptedException { ListString cmd new ArrayList(); switch (language) { case java: cmd.addAll(Arrays.asList(java, -cp, tempDir.toString(), Main)); break; case cpp: cmd.addAll(Arrays.asList(tempDir.resolve(main).toString())); break; case python: cmd.addAll(Arrays.asList(python, tempDir.resolve(main.py).toString())); break; } ProcessBuilder pb new ProcessBuilder(cmd); pb.directory(tempDir.toFile()); pb.redirectErrorStream(true); // 关键设置资源限制Windows 下需额外处理见避坑章 if (System.getProperty(os.name).toLowerCase().contains(win)) { // Windows 下用 PowerShell 启动带 JobObject 限制的进程简化版生产环境需用 JNI cmd.add(0, cmd.exe); cmd.add(1, /c); } Process proc pb.start(); // 写入输入 try (OutputStreamWriter writer new OutputStreamWriter(proc.getOutputStream(), UTF-8)) { writer.write(input); writer.flush(); } // 读取输出带超时 String output readWithTimeout(proc.getInputStream(), 5000); // 5秒读取超时 int exitCode proc.waitFor(10000); // 总执行时间上限 10 秒 String log ; if (exitCode ! 0 || output null) { log 进程异常退出退出码 exitCode 输出 output; } return new ExecutionResult( exitCode 0 output ! null, output null ? : output, log, 0, // 实际耗时需用 System.nanoTime() 计算此处简化 0 // 内存用量需用 /proc 或 GetProcessMemoryInfo 获取此处简化 ); } private static String readFile(Path file) throws IOException { return Files.exists(file) ? Files.readString(file, java.nio.charset.StandardCharsets.UTF_8) : ; } private static String readWithTimeout(InputStream is, long timeoutMs) throws IOException { ByteArrayOutputStream baos new ByteArrayOutputStream(); byte[] buffer new byte[1024]; long start System.currentTimeMillis(); try { while (System.currentTimeMillis() - start timeoutMs) { int len is.read(buffer); if (len -1) break; baos.write(buffer, 0, len); } } catch (IOException e) { if (System.currentTimeMillis() - start timeoutMs) { return null; // 超时 } throw e; } return baos.toString(UTF-8); } private static String normalizeOutput(String s) { return s null ? : s.trim().replaceAll(\\s, ); } private static void deleteDirectory(Path path) throws IOException { if (Files.isDirectory(path)) { for (Path child : Files.list(path).toArray(Path[]::new)) { deleteDirectory(child); } } Files.deleteIfExists(path); } // 内部类判题结果 public static class JudgeResult { private String status pending; private String judgeLog ; private int executeTime 0; private int memoryUsed 0; // getter/setter 略 public String getStatus() { return status; } public void setStatus(String status) { this.status status; } public String getJudgeLog() { return judgeLog; } public void setJudgeLog(String judgeLog) { this.judgeLog judgeLog; } public int getExecuteTime() { return executeTime; } public void setExecuteTime(int executeTime) { this.executeTime executeTime; } public int getMemoryUsed() { return memoryUsed; } public void setMemoryUsed(int memoryUsed) { this.memoryUsed memoryUsed; } } // 内部类执行结果 public static class ExecutionResult { private final boolean success; private final String output; private final String log; private final int time; private final int memory; public ExecutionResult(boolean success, String output, String log, int time, int memory) { this.success success; this.output output; this.log log; this.time time; this.memory memory; } public boolean isSuccess() { return success; } public String getOutput() { return output; } public String getLog() { return log; } public int getTime() { return time; } public int getMemory() { return memory; } public String getStatus() { return success ? running : runtime_error; } } }逻辑说明该类不依赖任何框架纯 JDK 实现确保在 Tomcat 任意版本下可运行writeSourceFile强制统一类名为Main规避学生public class HelloWorld导致编译失败execute方法中readWithTimeout是防卡死的关键避免学生while(true){}不输出导致线程挂起deleteDirectory在 finally 块中调用保证即使判题崩溃临时文件也能清理——这是线上系统磁盘不爆满的底线。3.2 在 JSP 中调用判题引擎用jsp:useBean% %实现请求驱动判题不是后台常驻服务而是HTTP 请求触发、同步返回结果。在submit.jsp中% page contentTypetext/html;charsetUTF-8 languagejava % % page importcom.oj.judge.JudgeEngine % % page importjava.util.* % % page importjava.sql.* % jsp:useBean idjudge classcom.oj.judge.JudgeEngine scopepage/ % // 1. 获取表单参数 String userId request.getParameter(user_id); String problemId request.getParameter(problem_id); String code request.getParameter(code); String language request.getParameter(language); // 2. 从数据库加载该题目的所有测试用例输入期望输出 ListString inputs new ArrayList(); ListString outputs new ArrayList(); Connection conn null; PreparedStatement ps null; ResultSet rs null; try { Class.forName(com.mysql.jdbc.Driver); conn DriverManager.getConnection(jdbc:mysql://localhost:3306/oj_system?useSSLfalseserverTimezoneUTC, root, your_password); ps conn.prepareStatement(SELECT input_data, expected_output FROM test_cases WHERE problem_id ?); ps.setInt(1, Integer.parseInt(problemId)); rs ps.executeQuery(); while (rs.next()) { inputs.add(rs.getString(input_data)); outputs.add(rs.getString(expected_output)); } } catch (Exception e) { out.println(加载测试用例失败 e.getMessage()); return; } finally { if (rs ! null) rs.close(); if (ps ! null) ps.close(); if (conn ! null) conn.close(); } // 3. 调用判题引擎注意此处是阻塞调用耗时取决于测试点数量 JudgeEngine.JudgeResult result judge.judge(code, language, inputs, outputs); // 4. 将结果存入 submissions 表 try { conn DriverManager.getConnection(jdbc:mysql://localhost:3306/oj_system?useSSLfalseserverTimezoneUTC, root, your_password); ps conn.prepareStatement(INSERT INTO submissions (user_id, problem_id, code, language, status, execute_time, memory_used, judge_log, submit_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())); ps.setInt(1, Integer.parseInt(userId)); ps.setInt(2, Integer.parseInt(problemId)); ps.setString(3, code); ps.setString(4, language); ps.setString(5, result.getStatus()); ps.setInt(6, result.getExecuteTime()); ps.setInt(7, result.getMemoryUsed()); ps.setString(8, result.getJudgeLog()); ps.executeUpdate(); } catch (Exception e) { out.println(保存提交记录失败 e.getMessage()); } finally { if (ps ! null) ps.close(); if (conn ! null) conn.close(); } // 5. 重定向到结果页 response.sendRedirect(result.jsp?submission_id conn.createStatement().executeQuery(SELECT LAST_INSERT_ID()).getInt(1)); %参数说明useSSLfalseserverTimezoneUTC是 MySQL 5.7 必加参数否则连接报时区错误LAST_INSERT_ID()获取刚插入的 submission ID用于跳转整个流程是同步的意味着用户点击“提交”后浏览器会等待判题完成通常 3 秒这对小规模系统足够大规模需改异步见最后一章。4. 避坑那些让系统上线后半夜被叫醒的 5 个真实问题4.1 现象学生提交 Java 代码判题返回compile_error但本地javac能编译通过原因JSP 运行在 Tomcat 的 JVM 中而ProcessBuilder启动的javac使用的是系统 PATH 中的 JDK。若服务器装了多个 JDK如 Tomcat 用 JDK 8系统 PATH 指向 JDK 17javac -version与java -version不一致导致字节码版本不兼容。解决在compile方法中显式指定javac路径例如C:/Program Files/Java/jdk1.8.0_391/bin/javac.exe并确保java命令也指向同一 JDK。4.2 现象MySQL 连接池耗尽Tomcat 日志刷屏Cannot create PoolableConnectionFactory原因JSP 中每次请求都DriverManager.getConnection()且未显式close()连接泄漏。JSP 的pagescope 无法自动管理 DB 连接生命周期。解决放弃连接池改用单连接 synchronized。在DBUtil.java中创建静态Connection用synchronized块包裹executeUpdate因为本系统并发量低 50 QPS单连接比连接池更稳——这是血泪经验HikariCP 在 Tomcat 9 下偶发Connection closed而裸连从不失效。4.3 现象Windows 服务器上判题进程不被杀死tasklist | findstr java一堆僵尸进程原因Process.destroy()在 Windows 下无法强杀子进程树如java Main启动的线程。学生代码Runtime.getRuntime().exec(notepad.exe)会逃逸。解决改用ProcessHandleJDK 9或 Windows 特定方案——在execute方法中用cmd /c start /min java ...启动再用taskkill /t /f /pid %PID%杀掉整个进程树。生产环境推荐用 Apache Commons Exec 的Watchdog。4.4 现象题目描述中的br标签在 JSP 页面显示为纯文本不换行原因JSP 默认对out.println()输出做 HTML 转义br变成lt;brgt;。解决在显示题目描述的 JSP 中用% problem.getDescription().replace(\n, br) %替换换行符并确保response.setContentType(text/html;charsetUTF-8)已设置禁用 EL 表达式的自动转义在web.xml中添加jsp-config el-expression-languagejsp/el-expression-language /jsp-config或在 JSP 顶部加% page isELIgnoredfalse %。4.5 现象MySQL 插入中文题目时报Incorrect string value: \xF0\x9F\x92\x96原因utf8字符集在 MySQL 中实际只支持 3 字节 UTF-8不支持 emoji而utf8mb4才是真正的 4 字节 UTF-8。解决建库时必须用CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci且 JDBC URL 加characterEncodingutf8mb4MyBatis 配置中jdbcTypeForNullOTHER。切记my.cnf中也要配置[client] default-character-set utf8mb4 [mysql] default-character-set utf8mb4 [mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ci5. 前端交互优化用 AJAX 实现“提交中”状态与实时进度条判题是耗时操作不能让用户干等白屏。用原生 JavaScript AJAX 替代全页面刷新提升体验。5.1 修改submit.jsp为纯 API 接口返回 JSON将判题逻辑抽离为judgeServlet.javaURL 映射为/judge返回标准 JSON// JudgeServlet.java WebServlet(/judge) public class JudgeServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String userId request.getParameter(user_id); String problemId request.getParameter(problem_id); String code request.getParameter(code); String language request.getParameter(language); // 加载测试用例同前 ListString inputs loadTestInputs(problemId); ListString outputs loadTestOutputs(problemId); // 判题 JudgeEngine.JudgeResult result new JudgeEngine().judge(code, language, inputs, outputs); // 保存到 DB同前 saveSubmission(userId, problemId, code, language, result); // 返回 JSON response.setContentType(application/json;charsetUTF-8); PrintWriter out response.getWriter(); out.print({\status\:\ result.getStatus() \,\execute_time\: result.getExecuteTime() ,\memory_used\: result.getMemoryUsed() ,\judge_log\:\ escapeJson(result.getJudgeLog()) \}); out.flush(); } private String escapeJson(String s) { return s null ? : s.replace(\\, \\\\).replace(\, \\\).replace(\n, \\n).replace(\r, \\r); } }5.2 前端用 fetch 实现无感提交与进度反馈在problem.jsp中div idsubmit-panel textarea idcode-editor placeholder请输入代码.../textarea select idlanguage-select option valuejavaJava/option option valuecppC/option option valuepythonPython/option /select button onclicksubmitCode()提交/button !-- 进度条 -- div idprogress-container styledisplay:none; div正在判题.../div divprogress idprogress-bar value0 max100/progress/div /div !-- 结果展示 -- div idresult-display styledisplay:none;/div /div script function submitCode() { const code document.getElementById(code-editor).value; const language document.getElementById(language-select).value; const problemId % request.getParameter(id) %; // 从 URL 获取题目 ID // 显示进度条 document.getElementById(progress-container).style.display block; document.getElementById(submit-panel).querySelector(button).disabled true; // 模拟进度真实判题无进度但用户需要感知 let progress 0; const interval setInterval(() { progress 10; document.getElementById(progress-bar).value progress; if (progress 100) clearInterval(interval); }, 200); // 发送请求 fetch(/judge, { method: POST, headers: { Content-Type: application/x-www-form-urlencoded }, body: user_id${getUserId()}problem_id${problemId}code${encodeURIComponent(code)}language${language} }) .then(response response.json()) .then(data { document.getElementById(progress-container).style.display none; document.getElementById(submit-panel).querySelector(button).disabled false; const resultDiv document.getElementById(result-display); resultDiv.innerHTML h3判题结果${getStatusText(data.status)}/h3 p运行时间${data.execute_time} ms/p p内存使用${data.memory_used} KB/p pre${data.judge_log}/pre ; resultDiv.style.display block; }) .catch(err { document.getElementById(progress-container).style.display none; document.getElementById(submit-panel).querySelector(button).disabled false; alert(提交失败 err.message); }); } function getStatusText(status) { const map { accepted: ✅ 通过, wrong_answer: ❌ 答案错误, time_limit_exceeded: ⏱️ 超时, memory_limit_exceeded: MemoryWarning, compile_error: 编译错误, runtime_error: 运行时错误 }; return map[status] || status; } // 简单获取用户 ID实际应从 session 读取 function getUserId() { return % session.getAttribute(user_id) ! null ? session.getAttribute(user_id) : 1 %; } /script技巧说明fetch替代XMLHttpRequest更简洁setInterval模拟进度条是用户体验刚需哪怕判题本身无中间态escapeJson防止judge_log中的双引号破坏 JSON 结构getStatusText用 emoji 提升可读性符合当前教育类系统 UI 趋势。6. 生产就绪技巧如何让这套 JSP 系统扛住期末考前 500 人并发提交最后说点硬核的。这套系统不是玩具它要真刀真枪跑在机房服务器上。我总结三条不写进教科书但保命的技巧6.1 Tomcat 调优不是加内存而是砍线程默认maxThreads200对 OJ 是灾难——每个判题请求占一个线程200 个线程同时判题内存炸、CPU 爆。正确做法是把maxThreads降到 10用队列缓冲。在conf/server.xml的Connector中Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 maxThreads10 acceptCount50 !-- 请求队列长度超 50 直接拒绝 -- minSpareThreads2 maxSpareThreads5 /这样500 人提交时只有 10 个在判题其余 490 人在队列里排队前端显示“排队中”系统不崩用户有预期。比盲目加机器实在。6.2 MySQL 专用配置关掉日志换 MyISAM仅限评测表submissions表写多读少InnoDB 的事务开销是累赘。在my.cnf中# 关闭慢查询日志除非调试 slow_query_log 0 # submissions 表用 MyISAM快 3 倍 # 注意MyISAM 不支持事务但提交记录不需要回滚 # 执行ALTER TABLE submissions ENGINEMyISAM;同时submissions表加复合索引INDEX idx_user_prob_status (user_id, problem_id, status)加速教师查某个学生某题状态。6.3 判题机隔离物理机跑判题Tomcat 跑 Web网络隔离最稳的架构是Web 服务器Tomcat和判题服务器纯 Java 判题进程分两台机器。Web 服务器只负责展示、接收代码、写submissions表判题服务器轮询submissions表WHERE statuspending执行判题更新status。用SELECT ... FOR UPDATE避免重复判题。这样判题卡死不影响 Web 访问且可横向扩展判题机。我当年在高职机房就是这么干的一台老 ThinkServer 做 Web三台报废笔记本装 Ubuntu 跑判题用 Redis 做任务队列轻量级替代 RabbitMQ。五年没重启过直到学校换新机。这套 JSP MySQL 的在线评测系统不是技术考古而是用确定性对抗不确定性——当你需要一个今天部署、明天能用、后天学生考完还能导出 Excel 成绩单的系统时它比任何新潮框架都可靠。我坚持用它不是因为不会 Spring Boot而是因为本文还有配套的精品资源点击获取
返回列表