news 2026/10/4 8:08:24

JSP+MySQL搭建在线评测系统实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP+MySQL搭建在线评测系统实战指南

简介:本资源是一套基于JSP与MySQL开发的在线评测系统课程设计项目,面向计算机专业本科生及Web开发初学者,解决教育场景中编程比赛组织、自动判题与用户成绩管理等核心需求。压缩包共171个文件,含40个Java源码(如UserServiceImpl、ContestResultServlet)、40个编译后class文件、12个JSP页面(涵盖登录、注册、比赛提交等交互逻辑)、62个依赖jar包,以及SQL建表脚本、CSS/JS前端资源和项目配置文件,整体大小19.26MB,结构完整,可直接部署运行。已有114人学习下载,资源提供从用户认证、题库管理、比赛调度到实时判题反馈的全链路实现方案,尤其在JDBC数据库操作、Filter权限控制、Servlet业务分层及MySQL表结构设计(含用户、题目、比赛、结果四类主表)等方面具备典型教学参考价值,适合课程设计复现、技术栈整合实践与Web应用架构理解。

1. 为什么用 JSP + MySQL 做在线评测系统,现在还值得投入?

不是所有“老技术栈”都该被淘汰——当你需要快速交付一个支持百人并发、题库可动态增删、判题逻辑可本地执行、后台管理界面不求炫酷但必须稳定可靠的在线评测系统(Online Judge, OJ)时,JSP + MySQL 这套组合反而成了最省心的“生产级捷径”。它不依赖复杂中间件、不强绑定云服务、不引入额外运维负担,一个 Tomcat + MySQL 实例就能跑通从用户注册、题目浏览、代码提交、编译运行、结果反馈到管理员审核的全链路。我去年在某高校计算机实验中心落地的这套系统,至今每天处理 3200+ 提交,平均响应时间 1.8s,故障率低于 0.3%。它不适合做 LeetCode 级别的高并发实时判题,但对课程实训、期末机考、校内编程赛这类场景,恰恰是“够用、可控、可审计”的黄金平衡点。如果你正被 Spring Boot 启动慢、Docker 网络配置翻车、判题沙箱权限失控等问题困扰,不妨回头看看这个被低估的组合:它不是过时,而是被误读;它的价值不在前沿性,而在确定性。


2. 搭建最小可行环境:Tomcat 9 + MySQL 5.7 的实操闭环

2.1 为什么选 Tomcat 9 而非 10+?关键兼容性取舍

JSP 是 Java EE 规范的一部分,而 Tomcat 10+ 默认启用 Jakarta EE 9+ 命名空间(jakarta.servlet.*),所有javax.*包名全部废弃。但绝大多数存量 JSP 标签库(如 JSTL 1.2)、自定义 Taglib、甚至部分 IDE 自动生成的 JSP 模板,仍硬编码javax.servlet.http.HttpServletRequest。强行升级会触发ClassNotFoundException: javax.servlet.jsp.JspFactory这类致命错误。Tomcat 9.0.x(推荐 9.0.87)完美兼容javax.*命名空间,且已通过 Oracle JDK 8u291+ 和 OpenJDK 11 长期验证。安装只需三步:

# 下载并解压(Linux 示例) wget https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.87/bin/apache-tomcat-9.0.87.tar.gz tar -xzf apache-tomcat-9.0.87.tar.gz export CATALINA_HOME=/opt/tomcat

提示:Windows 用户请务必使用apache-tomcat-9.0.87-windows-x64.zip,避免 Cygwin 兼容性问题;启动前检查JAVA_HOME是否指向 JDK(非 JRE),否则startup.bat会静默失败。

2.2 MySQL 5.7 安装与 OJ 专用初始化配置

MySQL 5.7 是 JSP 应用事实上的“安全基线”:JDBC 驱动mysql-connector-java-5.1.47.jar对其兼容性经过十年以上生产验证,而 MySQL 8.0+ 的默认caching_sha2_password认证插件常导致Access denied for user错误(即使密码正确)。安装后必须执行以下四步初始化:

  1. 创建 OJ 专用数据库与用户(避免 root 权限直连):

    CREATE DATABASE oj_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'oj_app'@'localhost' IDENTIFIED BY 'StrongPass2024!'; GRANT SELECT, INSERT, UPDATE, DELETE ON oj_db.* TO 'oj_app'@'localhost'; FLUSH PRIVILEGES;
  2. 关闭严格模式(JSP 表单提交空字符串时避免Data truncation异常):

    -- 修改 my.cnf,在 [mysqld] 段落添加 sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION" -- 替换为 sql_mode = ""
  3. 启用远程连接(仅限内网):

    UPDATE mysql.user SET host='192.168.1.%' WHERE user='oj_app'; FLUSH PRIVILEGES;
  4. 验证连接(用 JDBC URL 测试):

    jdbc:mysql://localhost:3306/oj_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false

    参数说明:useSSL=false是必须项(MySQL 5.7 默认要求 SSL,但本地开发无需);serverTimezone防止java.sql.SQLException: The server time zone value 'XXX' is unrecognized;characterEncoding=utf8确保中文题目描述、用户昵称不乱码。

2.3 JSP 工程结构骨架:按 OJ 业务域分层组织

一个可维护的 JSP 项目绝不能是.jsp文件堆砌。我采用经典三层分层(无框架)结构,目录树如下:

/webapp ├── /WEB-INF │ ├── web.xml ← Servlet 配置中枢 │ ├── /lib ← 放入 mysql-connector-java-5.1.47.jar │ └── /classes │ └── /dao ← 数据访问对象(含 ConnectionUtil) │ └── /bean ← POJO(User.java, Problem.java, Submission.java) │ └── /servlet ← 控制器(LoginServlet.java, SubmitServlet.java) ├── /static │ ├── /css ← Bootstrap 4.6(轻量、兼容 IE11) │ └── /js ← jQuery 3.6.0 + 自定义 judge.js ├── /pages │ ├── index.jsp ← 首页(题目列表) │ ├── problem_detail.jsp ← 单题详情页 │ ├── submit.jsp ← 提交表单页 │ └── admin/ ← 后台管理(需 login_check.jsp 拦截) └── /upload └── test_cases/ ← 存放题目测试用例(输入/输出文件)

关键设计原则:

  • DAO 层强制单例 ConnectionUtil:避免每次请求新建连接(MySQL 默认最大连接数 151,百人并发必超限);
  • JSP 页面禁止嵌入 Java 代码块<% %>:所有业务逻辑移至 Servlet,JSP 只负责展示(用 JSTL<c:forEach>渲染题目列表);
  • 静态资源走/static路径:避免被web.xml中的/*拦截器误处理。

3. 核心功能实现:从用户登录到代码判题的完整链路

3.1 用户认证与 Session 管理:用 Filter 拦截未登录访问

JSP 原生 Session 易被伪造,必须用 Filter 做统一校验。LoginFilter.java是安全第一道闸门:

// src/dao/filter/LoginFilter.java public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 白名单路径:登录页、静态资源、验证码接口 String path = request.getServletPath(); if (path.startsWith("/static/") || path.equals("/login.jsp") || path.equals("/LoginServlet")) { chain.doFilter(req, resp); return; } // 检查 Session 中是否存在 user_id HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user_id") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp?msg=not_login"); return; } chain.doFilter(req, resp); } }

web.xml中注册:

<filter> <filter-name>LoginFilter</filter-name> <filter-class>dao.filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

为什么不用request.getSession(true)?
强制创建 Session 会导致未登录用户也生成 Session ID,被恶意刷爆内存。getSession(false)仅检查已有 Session,符合“登录后才分配”的安全契约。

3.2 题目管理模块:MySQL 表结构与 JSP 动态渲染

OJ 的核心是题目(Problem)数据模型。problem表必须支持多语言描述、时间/内存限制、可见性控制:

CREATE TABLE `problem` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '题目标题', `description` text NOT NULL COMMENT '题目描述(支持HTML)', `input_format` text COMMENT '输入格式说明', `output_format` text COMMENT '输出格式说明', `sample_input` text COMMENT '样例输入', `sample_output` text COMMENT '样例输出', `time_limit` int(11) NOT NULL DEFAULT '1000' COMMENT '毫秒', `memory_limit` int(11) NOT NULL DEFAULT '65536' COMMENT 'KB', `difficulty` enum('easy','medium','hard') DEFAULT 'medium', `visible` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1=公开,0=私有', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

JSP 页面用 JSTL 渲染题目列表(index.jsp):

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:forEach items="${problemList}" var="p"> <div class="card mb-3"> <div class="card-header"> <span class="badge bg-${p.difficulty=='easy'?'success':p.difficulty=='medium'?'warning':'danger'}"> ${p.difficulty} </span> <a href="problem_detail.jsp?id=${p.id}">${p.title}</a> </div> <div class="card-body"> <p class="card-text">${fn:substring(p.description, 0, 120)}...</p> <div class="d-flex justify-content-between"> <small class="text-muted">时限: ${p.time_limit}ms | 内存: ${p.memory_limit}KB</small> <a href="submit.jsp?pid=${p.id}" class="btn btn-sm btn-primary">提交</a> </div> </div> </div> </c:forEach>

关键细节:fn:substring防止长描述撑爆页面;<c:forEach>替代<% while(rs.next()) { %>,彻底分离逻辑与视图;difficulty字段用 ENUM 而非 INT,MySQL 层面保证数据一致性。

3.3 代码提交与判题引擎:本地沙箱执行的核心逻辑

判题不是调用Runtime.getRuntime().exec()就完事——那是生产环境的定时炸弹。必须实现进程隔离、资源限制、超时熔断。JudgeEngine.java是核心:

// src/dao/JudgeEngine.java public class JudgeEngine { private static final long TIME_LIMIT_MS = 2000; // 题目设置的 time_limit * 2 private static final long MEMORY_LIMIT_KB = 131072; // 128MB public static JudgeResult run(String code, String language, int problemId) { String workDir = "/var/oj/judge/" + System.currentTimeMillis(); new File(workDir).mkdirs(); // 1. 生成源文件(根据 language 决定后缀) String fileName = "Main." + (language.equals("java") ? "java" : "cpp"); File sourceFile = new File(workDir, fileName); try (FileWriter writer = new FileWriter(sourceFile)) { writer.write(code); } // 2. 编译(Java: javac; C++: g++) ProcessBuilder compilePB = new ProcessBuilder( language.equals("java") ? "javac" : "g++", fileName ).directory(new File(workDir)); Process compileProc = compilePB.start(); int compileExit = compileProc.waitFor(); if (compileExit != 0) { return new JudgeResult("CE", "编译错误", 0, 0); } // 3. 执行(带 ulimit 限制) String execCmd = language.equals("java") ? "java Main" : "./a.out"; ProcessBuilder runPB = new ProcessBuilder("sh", "-c", String.format("ulimit -t %d; ulimit -v %d; %s", TIME_LIMIT_MS / 1000, MEMORY_LIMIT_KB * 1024, execCmd) ).directory(new File(workDir)); Process runProc = runPB.start(); Future<Integer> exitFuture = Executors.newSingleThreadExecutor() .submit(() -> runProc.waitFor()); try { int exitCode = exitFuture.get(TIME_LIMIT_MS + 500, TimeUnit.MILLISECONDS); // 读取 stdout/stderr 判断 AC/RE/WA... return parseResult(runProc, problemId, workDir); } catch (TimeoutException e) { runProc.destroyForcibly(); return new JudgeResult("TLE", "超时", TIME_LIMIT_MS, 0); } } }

血泪经验:ulimit -v限制虚拟内存(非物理内存),避免malloc失败却未触发 OOM;destroyForcibly()必须调用,否则僵尸进程堆积;parseResult()需比对标准输出与用户输出的逐行精确匹配(忽略末尾空格/空行),这是 WA(Wrong Answer)判定的唯一依据。


4. 避坑指南:JSP + MySQL 在线评测系统 5 大高频翻车现场

4.1 现象:提交代码后页面卡死,Tomcat 日志报java.lang.OutOfMemoryError: Metaspace

原因:JSP 页面每次修改都会被 Jasper 编译成 Servlet Class,旧 Class 不卸载导致 Metaspace 溢出。尤其在开发阶段频繁改 JSP,Metaspace 默认大小(128MB)很快耗尽。

解决:

  • 启动 Tomcat 时增加 JVM 参数:
    export JAVA_OPTS="-XX:MaxMetaspaceSize=512m -XX:MetaspaceSize=256m"
  • 生产环境禁用 JSP 自动重编译(web.xml中添加):
    <init-param> <param-name>development</param-name> <param-value>false</param-value> </init-param>

4.2 现象:MySQL 插入中文题目描述后显示为???,但SELECT查看字段值正常

原因:MySQL 客户端连接层字符集未统一。即使数据库、表、字段都是utf8mb4,JDBC URL 若漏掉characterEncoding=utf8,驱动仍用平台默认编码(如 Windows-1252)发送 SQL。

解决:

  • 检查 JDBC URL 是否包含characterEncoding=utf8&useUnicode=true;
  • 在 MySQL 命令行执行SHOW VARIABLES LIKE 'character_set%';,确认character_set_client、character_set_connection、character_set_results全部为utf8mb4;
  • 若仍异常,在my.cnf的[client]段落追加:
    [client] default-character-set = utf8mb4

4.3 现象:用户提交 C++ 代码,判题返回RE(Runtime Error),但本地测试完全正常

原因:Linux 环境下g++编译的二进制依赖libstdc++.so.6,而 Tomcat 运行用户(如tomcat)的LD_LIBRARY_PATH未包含/usr/lib64。

解决:

  • 在JudgeEngine.java的ProcessBuilder中显式指定库路径:
    runPB.environment().put("LD_LIBRARY_PATH", "/usr/lib64:/lib64");
  • 或更彻底:用ldd ./a.out检查缺失库,用cp将所需.so文件复制到/var/oj/judge/lib/并在runPB中加载。

4.4 现象:管理员后台删除题目后,前端题目列表仍显示已删题目

原因:浏览器缓存了index.jsp的 HTML,未强制刷新。JSP 页面默认无 Cache-Control 头,CDN 或代理服务器可能缓存。

解决:

  • 在index.jsp顶部添加:
    <% response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); response.setHeader("Pragma", "no-cache"); response.setDateHeader("Expires", 0); %>
  • 或更优:在web.xml中全局配置静态资源缓存策略,动态 JSP 页面禁用缓存。

4.5 现象:MySQL 连接池耗尽,Connection closed异常频发,提交请求大量超时

原因:DAO 层未正确关闭ResultSet、Statement、Connection。JSP 页面中直接new ConnectionUtil().getConnection()却未在finally块中close(),导致连接泄漏。

解决:

  • 强制使用 try-with-resources(Java 7+):
    try (Connection conn = ConnectionUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { // 处理结果集 } // 自动 close 所有资源
  • 在ConnectionUtil中添加连接数监控:
    private static final AtomicInteger activeCount = new AtomicInteger(0); public static Connection getConnection() throws SQLException { activeCount.incrementAndGet(); System.out.println("Active connections: " + activeCount.get()); return dataSource.getConnection(); }

5. 进阶技巧:让判题更准、响应更快、运维更稳的 3 个硬核实践

5.1 判题结果精准归因:用 diff 工具替代字符串 equals()

OJ 最怕“明明输出一样却判 WA”。根本原因是浮点数精度、行末空格、制表符等不可见字符差异。String.equals()无法识别这些。解决方案:集成diff命令做逐行比对。

// JudgeEngine.java 中 parseResult() 方法重构 private static JudgeResult compareOutput(File userOut, File stdOut) throws IOException { // 用 diff -w 忽略空格差异,-B 忽略空行 ProcessBuilder pb = new ProcessBuilder("diff", "-wB", userOut.getAbsolutePath(), stdOut.getAbsolutePath()); Process p = pb.start(); int exitCode = p.waitFor(); if (exitCode == 0) { return new JudgeResult("AC", "答案正确", 0, 0); } else if (exitCode == 1) { // diff 返回 1 表示有差异,但非错误 return new JudgeResult("WA", "答案错误", 0, 0); } else { // diff 返回 2 表示内部错误(如文件不存在) return new JudgeResult("JE", "判题错误", 0, 0); } }

为什么不用 Java 自带 diff 库?
Apache Commons Text 的DiffUtils在大文件(>1MB)比对时内存暴涨,而系统diff命令是 C 实现,内存占用恒定。实测 10MB 输出文件比对,Java 版耗时 3.2s 占用 1.8GB 堆内存,diff命令仅 0.4s 占用 2MB。

5.2 提交队列削峰:用文件队列替代数据库轮询

高并发提交时,SELECT * FROM submission WHERE status='pending' ORDER BY id LIMIT 1会成为 MySQL 瓶颈。改用基于文件的 FIFO 队列:

# 创建队列目录 mkdir -p /var/oj/queue/pending # 提交 Servlet 将任务写入文件 echo '{"pid":101,"uid":2001,"code":"#include<cstdio>..."}' > /var/oj/queue/pending/20240520_152344_12345.json # 判题守护进程(judge_daemon.sh)监听目录 inotifywait -m -e create /var/oj/queue/pending | while read path action file; do jq -r '.pid' "/var/oj/queue/pending/$file" | xargs -I {} java JudgeWorker {} rm "/var/oj/queue/pending/$file" done

优势:

  • 避免数据库锁竞争;
  • 提交操作变成毫秒级文件写入;
  • 判题进程可横向扩展(启多个judge_daemon.sh);
  • 故障时队列文件可人工干预(重放/删除)。

5.3 MySQL 主从热备:用 mysqldump + binlog 实现零停机升级

OJ 系统最怕停机维护。MySQL 5.7 支持基于 binlog 的主从复制,实现读写分离与故障切换:

# 主库(master)配置 my.cnf [mysqld] server-id = 1 log-bin = mysql-bin binlog-format = ROW expire_logs_days = 7 # 从库(slave)配置 my.cnf [mysqld] server-id = 2 relay-log = mysql-relay-bin # 主库上执行(获取 binlog 位置) mysql> FLUSH TABLES WITH READ LOCK; mysql> SHOW MASTER STATUS; -- 记录 File 和 Position # 此时导出全量数据 mysqldump -u root -p --all-databases --master-data=2 > full_backup.sql mysql> UNLOCK TABLES; # 从库导入并启动复制 mysql> source full_backup.sql; mysql> CHANGE MASTER TO MASTER_HOST='master_ip', MASTER_USER='repl_user', MASTER_PASSWORD='repl_pass', MASTER_LOG_FILE='mysql-bin.000001', MASTER_LOG_POS=12345; mysql> START SLAVE;

运维技巧:

  • SHOW SLAVE STATUS\G中Seconds_Behind_Master: 0表示同步完成;
  • 主库宕机时,立即在从库执行STOP SLAVE; RESET MASTER;升级为主库;
  • 应用层连接池配置两个数据源,用AbstractRoutingDataSource动态路由读/写请求。

我坚持用这套方案,不是因为怀旧,而是因为它把“不确定性”压缩到了最低——没有 Docker 网络玄学,没有 Spring Boot 启动慢的等待,没有判题沙箱权限失控的深夜救火。当你的目标是让 200 个学生在 90 分钟内顺利完成《数据结构》期末上机考试,而不是造一个技术炫技的玩具,JSP + MySQL 就是那把最趁手的螺丝刀。它不耀眼,但拧得紧、不打滑、不会突然崩断。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 8:07:39

插件机制设计指南:从加载原理到failed to load plugins排查

我们每天在和各种工具打交道&#xff0c;打开编辑器装插件、给浏览器加扩展、在IDE里接入静态检查工具、甚至音乐播放器都要靠插件才能听歌。但你有没有想过&#xff0c;为什么这些软件都要把功能拆成一块一块的插件&#xff1f;为什么日志里经常出现failed to load plugins之类…

作者头像 李华
网站建设 2026/10/4 8:07:35

Windows OCR工具实测:截图识别文字与免费翻译,天若OCR V4.48使用指南

每次装完新电脑&#xff0c;我第一件要办的事不是装微信&#xff0c;也不是配开发环境&#xff0c;而是先给系统安排上一款顺手的OCR工具。原因很简单&#xff0c;工作里要处理的截图、PDF扫描件、拍照材料实在太多&#xff0c;没有文字识别工具的时候&#xff0c;一份报销单都…

作者头像 李华
网站建设 2026/10/4 8:07:02

知网5.0文章AIGC率太高?2026年学姐实测10款降AI率工具红黑榜

2026年毕业季定稿最让人费神的从来不是重复率&#xff0c;是怎么都压不下去的AI内容疑似度&#xff01;前阵子我改稿熬到凌晨三点&#xff0c;查完报告还是一片飘红&#xff0c;才反应过来&#xff1a;全靠手动降AI率效率太低。 为了稳稳过审&#xff0c;我把市面上主流的降AI方…

作者头像 李华
网站建设 2026/10/4 8:05:35

【Spring AI 实战 · 阶段三·篇3】检索质量工程:五阶段混合检索管线

&#x1f4cc; 系列说明&#xff1a;一个 Java 后端视角的 Spring AI 渐进式实战教程&#xff0c;载体为开源项目「劳小司 智能法律助手」。 序章&#xff1a;技术栈全景与 AI 学习指南阶段一 流式对话内核&#xff1a;篇1 SSE 流式停止思考可见化 / 篇2 会话记忆压缩与滚动体…

作者头像 李华
网站建设 2026/10/4 8:04:13

QClaw停运后迁移WorkBuddy实战:Skill编排与1000积分使用指南

1. 从QClaw停运说起&#xff1a;一次被迫的迁移决策QClaw停运的消息出来那天&#xff0c;我的工作群里炸了锅。不是因为这个工具本身有多不可替代&#xff0c;而是因为太多人的日常工作流已经深度绑定了它——自动化脚本、定时任务、数据抓取、消息推送&#xff0c;甚至一些团队…

作者头像 李华
网站建设 2026/10/4 8:03:01

MATLAB信号处理实战:从滤波FFT到雷达仿真与深度学习

做信号处理这些年&#xff0c;我电脑里装得最勤、卸了又装的工具就是 MATLAB。别误会&#xff0c;我不是说它难用&#xff0c;恰恰相反——当你手里有一堆 ADC 采集回来的数据&#xff0c;要做滤波、频谱分析、特征提取&#xff0c;或者想快速验证一个算法思路时&#xff0c;MA…

作者头像 李华