OJ判题系统第6期之判题逻辑开发——设计思路、实现步骤、代码实现(策略模式)

在看这期之前,建议先看前五期:

Java 原生实现代码沙箱(OJ判题系统第1期)------设计思路、实现步骤、代码实现-CSDN博客

Java 原生实现代码沙箱之Java 程序安全控制(OJ判题系统第2期)------设计思路、实现步骤、代码实现-CSDN博客

Java 原生实现代码沙箱之代码沙箱 Docker 实现(OJ判题系统第3期)------设计思路、实现步骤、代码实现-CSDN博客

OJ判题系统第4期之判题机模块架构------设计思路、实现步骤、代码实现(工厂模式、代理模式的实践)-CSDN博客

OJ判题系统第5期之判题服务开发------设计思路、实现步骤、代码实现-CSDN博客

判题逻辑的主要指责

定义

  • 判题逻辑 是具体判断用户提交的代码是否正确的核心算法或规则集。它专注于解析沙箱返回的结果,并根据预定义的标准(如测试用例、时间限制、内存限制等)判断代码的正确性。
  • 这是一个低层次的模块,主要关注具体的判题细节。

主要职责

  1. 解析沙箱输出:从沙箱返回的结果中提取关键信息(如输出、错误信息、耗时、内存占用等)。
  2. 比对测试用例:将沙箱的输出与题目提供的标准答案进行比对,判断每个测试用例是否通过。
  3. 生成判题报告:根据比对结果生成详细的判题报告(如哪些测试用例通过了,哪些失败了,失败的原因是什么)。
  4. 性能评估:根据资源消耗情况(如时间、内存)评估代码的效率。

策略模式优化

什么是策略模式(Strategy Pattern)?

策略模式是一种行为型设计模式,它定义了一系列算法或策略,并将每一个算法封装起来,使它们可以互相替换,独立于使用它们的客户端。

简单理解:

  • 把不同的"处理方式"封装成一个个独立的类。
  • 客户端在运行时决定使用哪一种策略。
  • 这样可以让程序更灵活、更易扩展、更易维护。

问题背景:判题逻辑复杂,存在多种判断方式

在你的在线判题系统中,可能会遇到以下几种情况:

情况 描述
Java 执行较慢 Java 程序启动沙箱需要额外耗时(如 10 秒),总执行时间不能只看用户代码运行时间
Python 内存限制宽松 不同语言对内存的消耗不同,判题标准也要变化
C++ 要求输出完全一致 对输出格式要求严格,必须完全匹配才算通过
时间/内存限制动态调整 根据题目难度、语言类型等自动调整阈值

❌ 如果不用策略模式,会出现什么问题?

  • 判题逻辑中会充斥大量 if...elseswitch...case
  • 新增一个语言或规则时,要修改原有逻辑,违反开闭原则
  • 各种语言规则混在一起,可读性差,容易出错
  • 难以复用、难以测试、难以维护

解决方案:使用策略模式统一管理判题规则

🔧 设计思路

我们将"如何根据沙箱执行结果和语言特性来判定是否通过"的逻辑抽象为一个接口,然后为每种语言提供一个具体的实现类。

这样做的好处是:

  • 每个语言的判题规则彼此隔离,互不干扰
  • 可以随时新增、修改某种语言的判题规则,不影响其他逻辑
  • 在运行时可以根据提交的语言类型动态选择对应的策略

总结一句话

策略模式就像给系统装上了一个"可插拔的大脑",你可以根据不同情况(比如语言类型)自动选择最合适的判题规则,而无需改动主流程代码。

策略模式的优势总结

优势 描述
✅ 解耦 将判题逻辑与业务流程分离
✅ 易扩展 新增语言只需添加策略类,符合开闭原则
✅ 易维护 每种语言的规则独立,便于阅读和调试
✅ 动态切换 支持运行时根据条件选择不同策略
✅ 清晰结构 每个策略职责单一,符合单一职责原则

实现步骤

定义判题策略接口,让代码更加通用化:

java 复制代码
/**
 * 判题策略
 */
public interface JudgeStrategy {

    /**
     * 执行判题
     * @param judgeContext
     * @return
     */
    JudgeInfo doJudge(JudgeContext judgeContext);
}

定义判题上下文对象,用于定义在策略中传递的参数(可以理解为一种 DTO):

java 复制代码
/**
 * 上下文类(Context Class)
 *
 * JudgeContext 用于封装和传递在不同判题策略中需要用到的所有参数。
 * 它作为数据容器,确保各个判题策略能够访问到所需的信息,而不必直接依赖外部对象。
 */
@Data // Lombok 注解,自动生成 getter 和 setter 方法
public class JudgeContext {

    /**
     * 沙箱执行结果信息
     *
     * 包含了用户提交代码在沙箱中运行时的各项指标,如耗时、内存占用等。
     * 这些信息对于判断代码的正确性和性能至关重要。
     */
    private JudgeInfo judgeInfo;

    /**
     * 测试用例输入列表
     *
     * 包含了题目定义的所有测试用例的输入数据。
     * 在判题过程中,这些输入将被传入用户提交的代码,并与输出结果进行比对。
     */
    private List<String> inputList;

    /**
     * 用户代码的实际输出列表
     *
     * 当用户提交的代码在沙箱中运行时,根据不同的测试用例输入,会生成相应的输出结果。
     * 这些输出结果会被收集起来,用于后续的比对和判断。
     */
    private List<String> outputList;

    /**
     * 题目定义的标准测试用例列表
     *
     * 每个标准测试用例包含了输入和期望的输出。
     * 判题逻辑需要将用户的实际输出与这些期望输出进行比对,以确定是否通过该测试用例。
     */
    private List<JudgeCase> judgeCaseList;

    /**
     * 当前题目对象
     *
     * 包含了题目的所有相关信息,如题目描述、难度等级、附加说明等。
     * 虽然在大多数情况下,具体的判题逻辑可能不会直接使用这些信息,但它们可能会在某些特定场景下有用。
     */
    private Question question;

    /**
     * 用户提交记录对象
     *
     * 包含了用户提交的详细信息,如提交时间、编程语言、用户代码等。
     * 这些信息对于跟踪和记录用户的提交历史非常重要。
     */
    private QuestionSubmit questionSubmit;
}

实现默认判题策略

DefaultJudgeStrategy.java

java 复制代码
/**
 * 默认判题策略实现类
 *
 * 该策略用于处理通用语言(如 C++、JavaScript 等)的标准判题逻辑。
 * 包括:
 * - 判断输出数量是否匹配
 * - 判断每个测试用例的输出是否与预期一致
 * - 检查内存和时间是否超出题目限制
 */
public class DefaultJudgeStrategy implements JudgeStrategy {

    /**
     * 执行判题逻辑的核心方法
     *
     * @param judgeContext 判题上下文对象,包含所有需要的数据
     * @return 返回最终的判题结果信息(是否通过、错误类型、耗时、内存等)
     */
    @Override
    public JudgeInfo doJudge(JudgeContext judgeContext) {

从上下文中提取关键数据

java 复制代码
        // 从上下文中获取沙箱返回的执行信息(如时间、内存)
        JudgeInfo judgeInfo = judgeContext.getJudgeInfo();
        Long memory = judgeInfo.getMemory();   // 用户程序使用的内存大小(单位:字节)
        Long time = judgeInfo.getTime();       // 用户程序运行的时间(单位:毫秒)

        // 获取输入列表和输出列表
        List<String> inputList = judgeContext.getInputList();   // 测试用例的输入数据
        List<String> outputList = judgeContext.getOutputList(); // 用户程序的实际输出结果

        // 获取题目信息和测试用例列表
        Question question = judgeContext.getQuestion();                     // 当前题目对象
        List<JudgeCase> judgeCaseList = judgeContext.getJudgeCaseList();    // 题目定义的标准测试用例

        // 初始化判题结果为"接受"状态
        JudgeInfoMessageEnum judgeInfoMessageEnum = JudgeInfoMessageEnum.ACCEPTED;

        // 创建一个新的 JudgeInfo 对象用于封装最终的判题结果
        JudgeInfo judgeInfoResponse = new JudgeInfo();
        judgeInfoResponse.setMemory(memory); // 设置实际使用内存
        judgeInfoResponse.setTime(time);     // 设置实际运行时间

初步校验输出数量是否与输入一致

java 复制代码
        // 如果输出数量不等于输入数量,说明至少有一个测试用例没有正确输出
        if (outputList.size() != inputList.size()) {
            // 设置为"答案错误"
            judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
            judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
            return judgeInfoResponse; // 直接返回错误结果
        }

举例说明:

假设题目有 3 个测试用例,但用户只输出了 2 条结果,说明至少有一个用例未通过,无需继续比对。

逐条比对输出与预期是否一致

java 复制代码
        // 遍历所有测试用例,检查每一条输出是否与期望输出相等
        for (int i = 0; i < judgeCaseList.size(); i++) {
            JudgeCase judgeCase = judgeCaseList.get(i); // 获取第 i 个标准测试用例
            String expectedOutput = judgeCase.getOutput(); // 期望输出
            String actualOutput = outputList.get(i);       // 实际输出

            // 如果两者不一致,则判定为"答案错误"
            if (!expectedOutput.equals(actualOutput)) {
                judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
                judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
                return judgeInfoResponse; // 只要有一个测试用例失败,就直接返回错误
            }
        }

检查是否超过题目设定的资源限制

java 复制代码
        // 从题目对象中获取判题配置(例如内存限制、时间限制)
        String judgeConfigStr = question.getJudgeConfig(); // JSON 字符串格式的配置
        JudgeConfig judgeConfig = JSONUtil.toBean(judgeConfigStr, JudgeConfig.class);

        // 获取题目要求的最大内存和最大运行时间
        Long needMemoryLimit = judgeConfig.getMemoryLimit(); // 内存限制(单位:MB)
        Long needTimeLimit = judgeConfig.getTimeLimit();     // 时间限制(单位:毫秒)

        // 将 MB 转换为字节进行比较(注意单位一致性)
        if (memory > needMemoryLimit * 1024L * 1024L) { // 1 MB = 1024 KB = 1024*1024 bytes
            judgeInfoMessageEnum = JudgeInfoMessageEnum.MEMORY_LIMIT_EXCEEDED;
            judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
            return judgeInfoResponse;
        }

        // 检查是否超时
        if (time > needTimeLimit) {
            judgeInfoMessageEnum = JudgeInfoMessageEnum.TIME_LIMIT_EXCEEDED;
            judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
            return judgeInfoResponse;
        }

全部通过,返回成功结果

java 复制代码
        // 所有条件都满足,设置最终消息为"接受"
        judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());

        // 返回完整的判题结果
        return judgeInfoResponse;
    }
}

总结一下这个类的职责:

步骤 功能
1️⃣ 提取参数 JudgeContext 中提取所有判题所需的信息
2️⃣ 输出数量校验 判断输出数量是否与输入数量一致
3️⃣ 输出内容比对 比较每个测试用例的实际输出与期望输出是否一致
4️⃣ 资源限制判断 判断是否超出内存或时间限制
5️⃣ 返回结果 构建并返回最终的判题结果

定义 JudgeManager

为什么要定义 JudgeManager

1. 集中管理判题逻辑

  • 问题背景 : 在没有 JudgeManager 的情况下,每个地方调用判题逻辑时都需要手动判断使用哪种策略,这会导致大量的重复代码,增加维护成本。
  • 解决方案 : JudgeManager 将所有的判题逻辑集中管理,使得外部调用者只需传递 JudgeContext 对象即可完成判题操作,无需关心具体的实现细节。

2. 提高代码的可维护性和扩展性

  • 问题背景: 如果未来需要支持更多的编程语言或引入新的判题规则,直接修改现有代码容易引发潜在的风险。
  • 解决方案 : 使用 JudgeManager 和策略模式,可以轻松地添加新的策略类而不需要修改原有代码,符合开闭原则(对扩展开放,对修改封闭)。

3. 简化客户端调用

  • 问题背景: 客户端代码如果直接与各种判题策略打交道,会显得非常复杂且不易于理解。
  • 解决方案 : JudgeManager 提供了一个统一的接口,客户端只需要知道如何构建 JudgeContext 并调用 doJudge 方法,极大地简化了调用过程。

4. 增强系统的灵活性

  • 问题背景: 不同编程语言可能有不同的运行环境要求(如启动时间、内存限制等),这些差异需要在判题时特别处理。
  • 解决方案 : JudgeManager 可以根据编程语言动态选择最适合的判题策略,确保每种语言都能得到公平准确的评判。
java 复制代码
/**
 * 判题管理器
 *
 * JudgeManager 是整个判题系统的核心组件之一,负责根据用户提交的编程语言,
 * 动态选择并调用相应的判题策略(JudgeStrategy)。这有助于将复杂的判题逻辑
 * 进行模块化封装,便于后续维护和扩展。
 */
@Service // Spring 注解,标识该类为一个服务层组件
public class JudgeManager {

    /**
     * 执行判题操作
     *
     * 该方法接收一个包含所有必要信息的上下文对象(JudgeContext),
     * 根据用户提交的编程语言动态选择合适的判题策略,并返回最终的判题结果。
     *
     * @param judgeContext 包含了题目、用户提交、沙箱执行结果等信息的对象
     * @return 判题结果信息(如是否通过、错误类型、耗时、内存等)
     */
    public JudgeInfo doJudge(JudgeContext judgeContext) {
        
        // 从上下文中获取用户提交的信息
        QuestionSubmit questionSubmit = judgeContext.getQuestionSubmit();
        
        // 获取用户提交的编程语言类型
        String language = questionSubmit.getLanguage();

        // 初始化默认的判题策略
        JudgeStrategy judgeStrategy = new DefaultJudgeStrategy();

        // 根据编程语言选择不同的判题策略
        if ("java".equals(language)) {
            judgeStrategy = new JavaLanguageJudgeStrategy(); // Java 特有的判题策略
        }

        // 调用选定的策略执行判题操作
        return judgeStrategy.doJudge(judgeContext);
    }
}

执行判题:

整体流程图

bash 复制代码
用户点击提交按钮
│
├─→ 校验编程语言是否合法
│
├─→ 校验题目是否存在
│
├─→ 构造提交记录对象(包含代码、语言、用户ID、题目ID)
│
├─→ 插入数据库,设置初始状态为 WAITING
│
├─→ 获取提交记录 ID
│
├─→ 异步调用 judgeService.doJudge(questionSubmitId)
│
└─→ 返回 questionSubmitId 给前端

具体实现

方法签名与基本说明

java 复制代码
/**
 * 提交题目
 *
 * 用户提交代码进行判题的核心方法。
 * 主要流程包括:
 * 1. 参数校验(语言合法性、题目是否存在)
 * 2. 构建提交记录对象并保存到数据库
 * 3. 异步调用判题服务进行判题
 *
 * @param questionSubmitAddRequest 提交请求参数封装类
 * @param loginUser 当前登录用户信息
 * @return 返回提交记录的 ID,用于后续查询判题结果
 */
@Override
public long doQuestionSubmit(QuestionSubmitAddRequest questionSubmitAddRequest, User loginUser) {

获取并校验编程语言是否合法

java 复制代码
    // 从请求中获取用户提交的编程语言
    String language = questionSubmitAddRequest.getLanguage();

    // 使用枚举工具类判断该语言是否在支持的语言列表中
    QuestionSubmitLanguageEnum languageEnum = QuestionSubmitLanguageEnum.getEnumByValue(language);

    // 如果语言不合法,抛出异常
    if (languageEnum == null) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "编程语言错误");
    }

为什么需要这一步?

  • 防止用户输入非法或不受支持的编程语言(如 Python3、PHP 等非白名单语言)。
  • 增强系统安全性,避免后续处理出现不可控问题。

检查题目是否存在

java 复制代码
    // 获取题目 ID
    long questionId = questionSubmitAddRequest.getQuestionId();

    // 查询题目实体是否存在
    Question question = questionService.getById(questionId);
    if (question == null) {
        throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
    }

为什么需要这一步?

  • 确保用户提交的是一个真实存在的题目,防止攻击者通过伪造 ID 操作不存在的数据。
  • 同时也为后续判题逻辑提供必要的题目信息(如测试用例、限制条件等)。

构造提交记录对象,并设置默认状态

java 复制代码
    // 获取当前用户的 ID
    long userId = loginUser.getId();

    // 创建一个新的提交记录对象
    QuestionSubmit questionSubmit = new QuestionSubmit();
    questionSubmit.setUserId(userId);               // 设置用户 ID
    questionSubmit.setQuestionId(questionId);       // 设置题目 ID
    questionSubmit.setCode(questionSubmitAddRequest.getCode()); // 设置用户提交的代码内容
    questionSubmit.setLanguage(language);           // 设置编程语言

    // 设置初始状态为"等待中"
    questionSubmit.setStatus(QuestionSubmitStatusEnum.WAITING.getValue());

    // 初始 judgeInfo 字段为空 JSON,表示还未判题
    questionSubmit.setJudgeInfo("{}");

为什么设置初始状态?

  • 表示当前提交正在排队等待判题,前端或其他模块可以根据此状态判断是否已开始执行。
  • 判题完成后会异步更新状态为"成功"或"失败"。

将提交记录保存到数据库

java 复制代码
    // 尝试将提交记录插入数据库
    boolean save = this.save(questionSubmit);
    if (!save){
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "数据插入失败");
    }

为什么需要持久化?

  • 记录每一次提交的历史,便于后续查询、统计、审计。
  • 即使系统重启或发生异常,也可以根据数据库恢复状态。

获取提交记录 ID 并异步触发判题服务

java 复制代码
    // 获取刚刚插入的提交记录 ID
    Long questionSubmitId = questionSubmit.getId();

    // 异步执行判题逻辑,避免阻塞主线程
    CompletableFuture.runAsync(() -> {
        judgeService.doJudge(questionSubmitId);
    });

为什么要异步执行?

  • 判题是一个耗时操作(可能涉及启动沙箱、运行代码、比对输出等),如果同步执行会影响接口响应速度。
  • 使用 CompletableFuture 实现异步处理,提高系统吞吐量和用户体验。

返回提交记录 ID

java 复制代码
    // 返回提交记录 ID,供前端或其他服务使用
    return questionSubmitId;
}

这个 ID 的用途是什么?

  • 前端可以通过这个 ID 轮询或 WebSocket 监听判题结果。
  • 判题服务也依赖这个 ID 来查找对应的提交记录和题目信息。
相关推荐
猷咪5 分钟前
C++基础
开发语言·c++
IT·小灰灰6 分钟前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧8 分钟前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q9 分钟前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳09 分钟前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾9 分钟前
php 对接deepseek
android·开发语言·php
vx_BS8133013 分钟前
【直接可用源码免费送】计算机毕业设计精选项目03574基于Python的网上商城管理系统设计与实现:Java/PHP/Python/C#小程序、单片机、成品+文档源码支持定制
java·python·课程设计
2601_9498683613 分钟前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter
星火开发设计27 分钟前
类型别名 typedef:让复杂类型更简洁
开发语言·c++·学习·算法·函数·知识
qq_1777673739 分钟前
React Native鸿蒙跨平台数据使用监控应用技术,通过setInterval每5秒更新一次数据使用情况和套餐使用情况,模拟了真实应用中的数据监控场景
开发语言·前端·javascript·react native·react.js·ecmascript·harmonyos