☰
JSP+MySQL在线评测系统搭建实战指南
2026/10/1 11:50:54 网站建设 项目流程

简介:本资源是一套基于JSP与MySQL开发的在线评测系统课程设计项目,面向计算机专业本科生及Web开发初学者,解决教育场景中编程比赛组织、自动判题与成绩管理等核心需求。压缩包共171个文件,含40个Java源码(如UserServiceImpl、ContestResultServlet)、40个编译后class文件、12个JSP页面(涵盖登录、注册、比赛提交等交互逻辑)、62个运行依赖jar包,以及SQL建表脚本、CSS/JS前端资源和项目配置文件,整体大小19.26MB,结构完整,可直接部署运行。已有114人学习下载,资源提供从用户认证、题库管理、实时评测到管理员后台的全链路实现,包含DAO层数据操作封装、Filter权限控制、JDBC连接池配置及典型判题流程逻辑,代码规范、模块职责清晰,适合作为JSP+MySQL综合实践范例深入理解Web应用分层架构与数据库协同设计。

1. 为什么还在用 JSP + MySQL 做在线评测系统?——不是怀旧,是稳得住、改得动、跑得久

你可能在头歌实训里写过“JSP 用户登录页”,在毕设选题列表里扫过“基于 JSP 的 XX 系统”,甚至刚被导师拍着桌子说:“别整 Spring Boot,就用 JSP + MySQL,部署到学校老服务器上!”——这不是技术倒退,而是一类真实存在的交付场景:校内教学平台、课程实验环境、低运维预算的教务子系统、JavaWeb 教学闭环项目。这类系统不追求高并发、不卷微服务、不碰云原生,但必须满足四个硬指标:能本地一键启动、数据库结构清晰可查、代码逻辑直白可调试、教师能自己改题目和判题规则。JSP + MySQL 正是这种“教学级生产环境”的黄金组合:JSP 天然贴合 HTML 渲染逻辑,学生改个<%=score%>就能看见分数;MySQL 不用 Docker 编排、不配 SSL、不设主从,装好服务、建库授权、导入 SQL 就能跑;判题核心用 Java 写成独立类,和前端解耦,测试时直接java JudgeMain input.txt output.txt就能验证逻辑。本文不讲“为什么不用 Spring Boot”,而是带你从零搭起一个可运行、可扩展、可交差的在线评测系统骨架:包含用户管理、题目发布、代码提交、本地沙箱判题(非 Docker)、实时状态反馈。所有代码在 Windows/Linux 下均可复现,MySQL 版本锁定 5.7–8.0(兼容性已实测),JSP 运行容器用 Tomcat 9.0(不依赖 IDEA 内置插件,纯 war 包部署)。适合大三课程设计、毕业设计中期交付、教师自建练习平台。


2. 搭建最小可运行环境:Tomcat + MySQL + 项目结构三件套

2.1 Tomcat 9.0 部署与 JSP 支持验证

JSP 不是独立语言,它依赖 Servlet 容器解析。Tomcat 9.0 是当前最稳定、对 JSP 2.3 规范支持最完整的版本,且无需额外配置即可运行 JSP 页面。不要用 Tomcat 10+—— 其包名从javax.*升级为jakarta.*,会导致大量老式 JSP 标签库(如 JSTL)报错,而教学环境中 JSTL(<c:if>、<c:forEach>)几乎是标配。

下载地址: https://tomcat.apache.org/download.cgi → 选择Apache Tomcat 9→tar.gz(Linux)或zip(Windows)
解压后进入bin/目录:

# Linux/macOS chmod +x startup.sh shutdown.sh ./startup.sh
:: Windows startup.bat

访问http://localhost:8080,看到 Apache Tomcat 默认首页即成功。关键验证点:在webapps/ROOT/下新建test.jsp:

<%@ page contentType="text/html;charset=UTF-8" %> <html> <body> <h2>Tomcat + JSP 连通成功!</h2> <%= new java.util.Date() %> </body> </html>

刷新页面,若显示当前时间,说明 JSP 引擎工作正常。注意:此步骤必须完成,否则后续所有 JSP 页面将 500 报错,且错误日志藏在logs/catalina.out中,新手极易卡在此处。

2.2 MySQL 5.7–8.0 安装与基础配置

教学环境首选 MySQL 5.7(兼容性最强)或 8.0(需关闭严格模式)。避坑重点:MySQL 8.0 默认启用caching_sha2_password插件,而老版 JDBC 驱动(如mysql-connector-java-5.1.47.jar)不支持,导致Connection refused。解决方案分两步:

  1. 安装时跳过密码强度校验(仅限教学环境):
    在my.cnf(Linux)或my.ini(Windows)的[mysqld]段下添加:

    validate_password=OFF default_authentication_plugin=mysql_native_password
  2. 创建评测专用数据库与用户(避免用 root 操作):

    -- 登录 MySQL 后执行 CREATE DATABASE oj_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'oj_user'@'localhost' IDENTIFIED BY 'oj_pass123'; GRANT ALL PRIVILEGES ON oj_db.* TO 'oj_user'@'localhost'; FLUSH PRIVILEGES;

提示:utf8mb4是必须项,否则用户昵称含 emoji(如 👨‍💻)会存为?;FLUSH PRIVILEGES不可省略,否则权限不生效。

2.3 项目目录结构:拒绝“一个 index.jsp 打天下”

传统 JSP 项目易陷入“所有逻辑堆在 JSP 里”的泥潭。我们采用轻量 MVC 分层:

oj-web/ ├── WEB-INF/ │ ├── web.xml ← Servlet 配置中枢 │ ├── lib/ ← JDBC 驱动、JSTL 等 jar │ └── classes/ ← 编译后的 Java 类(含 DAO、Service、Judge) ├── static/ ← CSS/JS/图片(不经过 Servlet) ├── jsp/ │ ├── login.jsp ← 前端页面 │ ├── problem_list.jsp │ └── submit_result.jsp └── src/ ← Java 源码(开发时放此处,编译后进 classes)

关键文件WEB-INF/web.xml必须存在且正确(Tomcat 9 要求 Servlet 3.1+):

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <display-name>Online Judge System</display-name> <welcome-file-list> <welcome-file>jsp/login.jsp</welcome-file> </welcome-file-list> </web-app>

注意:version="4.0"对应 Servlet 4.0,Tomcat 9 完全支持;<welcome-file>指定入口,避免访问根路径 404。


3. 数据库设计:聚焦评测核心实体,拒绝过度工程

在线评测系统(OJ)本质是“题目-提交-判题”三元关系。教学项目不必模仿 LeetCode 的复杂标签体系,6 张表足矣,且全部字段可控、索引明确、无冗余:

表名用途关键字段(带注释)
user用户信息id(PK),username(UNIQUE),password(SHA256 加密),role(ENUM: 'student','teacher')
problem题目库id(PK),title,description(TEXT),time_limit(ms),memory_limit(KB),input_sample,output_sample
submission提交记录id(PK),user_id(FK),problem_id(FK),code(LONGTEXT),language(ENUM: 'java','cpp'),status(ENUM: 'pending','accepted','wrong_answer','compile_error'),submit_time(DATETIME)
judge_log判题日志id(PK),submission_id(FK),judge_time(DATETIME),exit_code,runtime_ms,memory_kb,error_output(TEXT)
testcase测试用例id(PK),problem_id(FK),input_data(TEXT),output_data(TEXT),is_sample(TINYINT)
problem_tag题目标签(可选)id(PK),problem_id(FK),tag_name

建表 SQL 示例(problem 表):

CREATE TABLE `problem` ( `id` INT NOT NULL AUTO_INCREMENT, `title` VARCHAR(100) NOT NULL COMMENT '题目标题', `description` TEXT NOT NULL COMMENT '题目描述(支持 Markdown 解析)', `time_limit` INT NOT NULL DEFAULT 1000 COMMENT '时间限制(毫秒)', `memory_limit` INT NOT NULL DEFAULT 65536 COMMENT '内存限制(KB)', `input_sample` TEXT COMMENT '输入样例', `output_sample` TEXT COMMENT '输出样例', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

注意:TEXT类型用于存储长文本(题目描述、代码),不要用VARCHAR(10000)—— MySQL 对VARCHAR有行大小限制(65535 字节),超长内容会截断;AUTO_INCREMENT保证 ID 递增,方便前端分页;DEFAULT CURRENT_TIMESTAMP自动记录创建时间,比 Java 代码写入更可靠。

初始化数据脚本init_data.sql(放入项目根目录,供教师一键导入):

INSERT INTO `problem` (`title`, `description`, `time_limit`, `memory_limit`, `input_sample`, `output_sample`) VALUES ('A+B Problem', '计算两个整数的和', 1000, 65536, '1 2', '3'), ('Hello World', '输出固定字符串', 500, 1024, '', 'Hello, OJ!');

执行方式:mysql -u oj_user -p oj_db < init_data.sql


4. 核心判题模块:本地沙箱执行,不依赖 Docker

在线评测最怕“判题服务崩了学生交不了作业”。教学环境必须规避 Docker、K8s 等重依赖,采用Java ProcessBuilder + 临时目录隔离 + 资源限制的轻量沙箱方案。原理:为每次提交创建独立进程,在限定时间内执行编译/运行,捕获 stdout/stderr,比对输出。

4.1 判题主流程 Java 类(JudgeService.java)

// src/com/oj/service/JudgeService.java package com.oj.service; import java.io.*; import java.nio.file.*; import java.util.concurrent.*; public class JudgeService { // 编译 & 运行超时(毫秒) private static final long COMPILE_TIMEOUT = 3000; private static final long RUN_TIMEOUT = 5000; public JudgeResult judgeSubmission(String code, String language, String input, String expectedOutput) { JudgeResult result = new JudgeResult(); Path tempDir = null; try { // 1. 创建临时目录(保证隔离) tempDir = Files.createTempDirectory("oj_judge_"); // 2. 根据语言生成文件并编译 if ("java".equals(language)) { Path javaFile = tempDir.resolve("Main.java"); Files.write(javaFile, code.getBytes()); // 编译 Process compileProc = new ProcessBuilder("javac", "Main.java") .directory(tempDir.toFile()) .start(); if (compileProc.waitFor(COMPILE_TIMEOUT, TimeUnit.MILLISECONDS)) { int exitCode = compileProc.exitValue(); if (exitCode != 0) { result.setStatus("compile_error"); result.setErrorOutput(getProcessOutput(compileProc.getErrorStream())); return result; } } else { result.setStatus("compile_error"); result.setErrorOutput("编译超时"); return result; } } // 3. 运行程序(统一用 Main.class) Process runProc = new ProcessBuilder("java", "Main") .directory(tempDir.toFile()) .redirectInput(new ByteArrayInputStream(input.getBytes())) .start(); // 4. 设置运行超时并捕获输出 if (runProc.waitFor(RUN_TIMEOUT, TimeUnit.MILLISECONDS)) { int exitCode = runProc.exitValue(); String actualOutput = getProcessOutput(runProc.getInputStream()); String errorOutput = getProcessOutput(runProc.getErrorStream()); if (exitCode == 0 && actualOutput.trim().equals(expectedOutput.trim())) { result.setStatus("accepted"); } else if (exitCode != 0) { result.setStatus("runtime_error"); result.setErrorOutput(errorOutput); } else { result.setStatus("wrong_answer"); result.setActualOutput(actualOutput); } } else { result.setStatus("time_limit_exceeded"); runProc.destroyForcibly(); // 强制终止 } } catch (Exception e) { result.setStatus("system_error"); result.setErrorOutput("判题异常: " + e.getMessage()); } finally { // 5. 清理临时目录(重要!否则磁盘爆满) if (tempDir != null) { try { FileUtils.deleteDirectory(tempDir.toFile()); } catch (IOException ignored) {} } } return result; } private String getProcessOutput(InputStream is) throws IOException { StringBuilder sb = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { String line; while ((line = reader.readLine()) != null) { sb.append(line).append("\n"); } } return sb.toString().trim(); } }

逻辑说明:

  • Files.createTempDirectory()保证每次判题目录唯一,避免多提交冲突;
  • ProcessBuilder.directory()指定工作目录,使javac/java命令在临时目录内执行;
  • redirectInput()将测试用例输入注入进程 stdin;
  • waitFor(timeout)是核心防卡死机制,超时立即destroyForcibly();
  • FileUtils.deleteDirectory()来自 Apache Commons IO(需引入commons-io-2.11.0.jar),不能用tempDir.toFile().delete()—— 递归删除失败率高。

4.2 判题结果封装类(JudgeResult.java)

// src/com/oj/model/JudgeResult.java package com.oj.model; public class JudgeResult { private String status; // accepted / wrong_answer / compile_error ... private String actualOutput; // 实际输出(用于 debug) private String errorOutput; // 错误信息(编译错误/运行时异常) private long runtimeMs; // 实际运行时间(毫秒) private long memoryKb; // 内存占用(需 Linux ps 命令配合,此处简化为 0) // getter/setter 省略 }

关键参数说明:

  • COMPILE_TIMEOUT = 3000:Java 编译通常 < 1s,设 3s 防止网络挂载 NFS 目录等慢盘场景;
  • RUN_TIMEOUT = 5000:教学题目简单,5s 足够;若需支持 C++,可增至 10s;
  • expectedOutput.trim():比对前去除首尾空格换行,避免学生多打空格被判错;
  • actualOutput.trim().equals(...):必须 trim,否则"3\n"和"3"判为 WA。

5. 避坑指南:那些让答辩前夜崩溃的 5 个真实问题

5.1 现象:JSP 页面中文乱码,显示??或某些文字

原因:JSP 文件本身编码、Tomcat 请求编码、MySQL 连接编码三者未统一。常见于 Windows 记事本保存的.jsp文件默认 ANSI 编码。
解决:

  1. 用 VS Code 或 IDEA 重存所有.jsp文件为UTF-8 without BOM;
  2. 在每个 JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" %>;
  3. web.xml中添加过滤器(WEB-INF/web.xml):
    <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

5.2 现象:MySQL 连接报错Access denied for user 'oj_user'@'localhost'

原因:MySQL 8.0 默认root用户使用caching_sha2_password,而oj_user创建时未指定插件,或localhost解析为127.0.0.1导致 host 不匹配。
解决:

  1. 登录 MySQL 后执行:
    ALTER USER 'oj_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'oj_pass123'; FLUSH PRIVILEGES;
  2. JDBC URL 显式指定serverTimezone=GMT%2B8(防止时区错乱):
    String url = "jdbc:mysql://localhost:3306/oj_db?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=utf8";

5.3 现象:提交 Java 代码后判题一直pending,日志无报错

原因:ProcessBuilder启动javac/java时未指定完整路径,Linux 下PATH不含 JDK bin 目录,Windows 下JAVA_HOME未设或指向 JRE(无 javac)。
解决:

  • Linux:在 Tomcat 启动脚本setenv.sh中添加:
    export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 export PATH=$JAVA_HOME/bin:$PATH
  • Windows:在setenv.bat中添加:
    set JAVA_HOME=C:\Program Files\Java\jdk-11.0.20 set PATH=%JAVA_HOME%\bin;%PATH%
  • 终极保险:在JudgeService.java中硬编码路径(开发阶段):
    // 替换 ProcessBuilder("javac", ...) 为: ProcessBuilder pb = new ProcessBuilder("/usr/lib/jvm/java-11-openjdk-amd64/bin/javac", "Main.java");

5.4 现象:<c:forEach>标签不解析,页面原样输出<c:forEach items="${list}" var="item">

原因:JSTL 标签库未正确引入或版本不匹配。Tomcat 9 需要jstl-1.2.jar(非jstl-api-1.2.jar)。
解决:

  1. 下载jstl-1.2.jar( Maven Repository );
  2. 放入WEB-INF/lib/;
  3. JSP 页面顶部声明:
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

5.5 现象:判题时java.lang.OutOfMemoryError: Java heap space

原因:学生提交无限循环代码(如while(true){}),ProcessBuilder未限制 JVM 堆内存,导致 Tomcat 进程 OOM。
解决:

  • 不推荐:在java命令中加-Xmx128m(因ProcessBuilder启动的是新 JVM,不影响 Tomcat);
  • 正确做法:用ulimit限制子进程资源(Linux):
    # 在 Tomcat 启动前执行(或写入 startup.sh) ulimit -v 131072 # 虚拟内存 128MB ulimit -t 5 # CPU 时间 5 秒
  • Windows 无ulimit,改用psexec(需提前安装):
    psexec -d -e -i cmd /c "java -Xmx128m Main"

6. 进阶技巧:让系统真正可用的 3 个实战补丁

6.1 题目描述支持 Markdown 渲染(不引入 JS 库)

学生写题目描述时习惯用**加粗**、*斜体*、- 列表,但 JSP 直接out.print(description)会原样输出。不用引入 marked.js 或 showdown—— 用 Java 正则做轻量转换,兼顾安全与性能:

// src/com/oj/util/MarkdownRenderer.java public class MarkdownRenderer { public static String render(String markdown) { if (markdown == null) return ""; // 1. 换行转 <br> markdown = markdown.replaceAll("\n", "<br>"); // 2. **粗体** → <strong>粗体</strong> markdown = markdown.replaceAll("\\*\\*(.*?)\\*\\*", "<strong>$1</strong>"); // 3. *斜体* → <em>斜体</em> markdown = markdown.replaceAll("\\*(.*?)\\*", "<em>$1</em>"); // 4. - 列表 → <ul><li>...</li></ul> markdown = markdown.replaceAll("- (.*?)(<br>|$)", "<li>$1</li>"); markdown = "<ul>" + markdown + "</ul>"; // 5. 过滤 XSS(移除 script/style 标签) markdown = markdown.replaceAll("(?i)<(script|style)[^>]*>.*?</\\1>", ""); return markdown; } }

在 JSP 中调用:

<%@ page import="com.oj.util.MarkdownRenderer" %> ${MarkdownRenderer.render(problem.description)}

优势:无前端依赖、渲染快、防 XSS 基础防护;缺点:不支持表格/代码块,教学场景够用。若需增强,再引入commonmark-java库(<dependency><groupId>org.commonmark</groupId><artifactId>commonmark</artifactId><version>0.21.0</version></dependency>)。

6.2 提交记录分页优化:避免LIMIT 10000,20性能雪崩

当提交量 > 10 万条,SELECT * FROM submission ORDER BY id DESC LIMIT 10000,20会扫描前 10020 行,极慢。用游标分页(Cursor-based Pagination)替代偏移分页:

-- 第一页(取最新 20 条) SELECT * FROM submission ORDER BY id DESC LIMIT 20; -- 第二页(假设上一页最后 id=10020) SELECT * FROM submission WHERE id < 10020 ORDER BY id DESC LIMIT 20;

Java DAO 层实现:

public List<Submission> getSubmissionsAfterId(int lastId, int limit) { String sql = "SELECT * FROM submission WHERE id < ? ORDER BY id DESC LIMIT ?"; return jdbcTemplate.query(sql, new SubmissionRowMapper(), lastId, limit); }

原理:利用主键id有序性,用WHERE id < ?替代OFFSET,MySQL 可走主键索引,速度提升百倍。必须确保排序字段有索引(ALTER TABLE submission ADD INDEX idx_id_desc (id DESC);)。

6.3 教师后台:一键导出学生提交统计 Excel

教师常需导出“某题 AC 率”、“某学生提交明细”给教务处。用 Apache POI 生成.xlsx,避免 CSV 编码问题:

// src/com/oj/servlet/ExportReportServlet.java protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String type = req.getParameter("type"); // "problem_ac_rate" or "student_submissions" Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet("Report"); if ("problem_ac_rate".equals(type)) { List<Object[]> data = problemDao.getAcRateByProblem(); Row header = sheet.createRow(0); header.createCell(0).setCellValue("题目ID"); header.createCell(1).setCellValue("题目标题"); header.createCell(2).setCellValue("AC 数"); header.createCell(3).setCellValue("总提交数"); header.createCell(4).setCellValue("AC 率"); int rowNum = 1; for (Object[] row : data) { Row r = sheet.createRow(rowNum++); r.createCell(0).setCellValue((Integer) row[0]); r.createCell(1).setCellValue((String) row[1]); r.createCell(2).setCellValue((Long) row[2]); r.createCell(3).setCellValue((Long) row[3]); r.createCell(4).setCellValue((Double) row[4]); } } resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); resp.setHeader("Content-Disposition", "attachment; filename=oj_report.xlsx"); workbook.write(resp.getOutputStream()); }

关键点:Workbook用XSSFWorkbook(.xlsx),非HSSFWorkbook(.xls,不支持 > 65536 行);Content-Disposition设为attachment强制下载;务必关闭流(workbook.write()已处理)。

我带过三届课程设计,最深的教训是:别在答辩前三天重构判题模块。曾有个组把ProcessBuilder换成DockerClient,结果 Docker 在学校机房虚拟机里根本装不上,最后连夜回滚到本地沙箱,靠ulimit和try-catch硬扛。现在我的习惯是:先跑通javac+java沙箱,再加功能;数据库字段宁少勿多,problem表删掉difficulty字段,等学生真需要分级再加;所有 JSP 页面必加<%@ page contentType="text/html;charset=UTF-8" %>,哪怕只是复制粘贴。这些不是最佳实践,是血泪经验换来的最小可行路径。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询