导员:你这么牛,那你来讲讲你项目的核心流程-判题模块吧

耗时一个月开发的OJ在线判题系统,文末有项目地址,目前还在更新代码~
今天我们来开发OJ系统后端核心流程之一的判题模块

判题机模块与代码沙箱的关系

判题模块:调用代码沙箱,把代码和输入交给代码沙箱去执行
代码沙箱:只负责接收代码和输入,返回编译运行的结果,不负责判题(可以作为独立的项目/服务,提供给其他的需要执行代码的项目去使用)
这两个模块完全解耦:在这里插入图片描述

注意:每次代码沙箱要接受和输出一组运行用例

这是一种很常见的性能优化方法(批处理)

因为如果是每个用例单独调用一次代码沙箱,会调用多次接口,需要多次网络传输,程序要多次编译、记录程序的执行状态

代码沙箱架构开发

1)定义代码沙箱的接口,提高通用性

public interface CodeSandbox {
    /**
     * 执行代码
     * @param excodeCodeRequest
     * @return
     */
    ExecutecodeResponse executeCode(ExecutecodeCodeRequest excodeCodeRequest);
}

之后我们的项目代码只调用接口,不调用具体的实现类,这样在你使用其他的代码沙箱实现类时,就不用去修改名称了,便于扩展

代码沙箱的求接口中,timeLimit可加可不加,可自行扩展,即时中断程序

扩展思路:增加一个查看代码沙箱状态的接口
2)定义多种不同的代码沙箱实现
示例代码沙箱:仅为了跑通业务流程
远程代码沙箱:实际调用接口的沙箱
第三方代码沙箱:调用网上现成的代码沙箱:https://github.com/criyle/go-judge
3)编写单元测试:验证单个代码沙箱的执行

    @Test
    void executeCode(){
        CodeSandbox codesandbox = new ExampleCodeSandbox();
        String code = "int main(){}";
        String language = QuestionSubmitLanguageEnum.JAVA.getValue();
        List<String> inputList = Arrays.asList("1 2","3 4");
        ExecutecodeCodeRequest executecodeCodeRequest = ExecutecodeCodeRequest.builder().code(code).language(language).inputList(inputList).build();

        ExecutecodeResponse executecodeResponse = codesandbox.executeCode(executecodeCodeRequest);
        Assertions.assertNotNull(executecodeResponse);
    }

但现在的问题是,我们把new某个沙箱的代码写死了,如果后面项目要改用其他沙箱,可能要改很多地方的代码。

4)使用工厂模式,根据用户传入的字符串参数(沙箱类别),来生成对应的代码沙箱实现类,此处使用静态工厂模式,实现比较简单,符合我们的需求。

/**
 * 代码沙箱创建工厂:根据指定的字符串参数,创建指定的代码沙箱实例
 */
public class CodeSandboxFactory {
    /**
     * 创建代码沙箱实例
     * @param type 沙箱类型
     * @return 返回的是接口,而不是具体的实现类
     */
    public static CodeSandbox newInstance(String type){
        switch (type){
            case "example":
                return new ExampleCodeSandbox();
            case "remote":
                return new RemoteCodeSandbox();
            case "Third":
                return new ThirdPartyCodeSandbox();
            default:
                return new ExampleCodeSandbox();
        }
    }
    
}

测试类可改成下面

    @Test
    void executeCode(){
        String type = "remote";
        CodeSandbox codesandbox = CodeSandboxFactory.newInstance(type);
        String code = "int main(){}";
        String language = QuestionSubmitLanguageEnum.JAVA.getValue();
        List<String> inputList = Arrays.asList("1 2","3 4");
        ExecutecodeCodeRequest executecodeCodeRequest = ExecutecodeCodeRequest.builder().code(code).language(language).inputList(inputList).build();

        ExecutecodeResponse executecodeResponse = codesandbox.executeCode(executecodeCodeRequest);
        Assertions.assertNotNull(executecodeResponse);
    }

5)参数配置化,把项目中的一些可以交给用户去自定义的选项或字符串,写到配置文件中,这样开发者只需要改配置文件,而不需要去看你的项目代码,就能够自定义使用你项目的更多功能

application.yml配置文件中指定变量:

# 代码沙箱配置
codesandbox:
  type: example

在Spring的Bean中通过@Value 注解读取 :

@Value("${codesandbox.type:example}")
private String type;

6)代理模式优化
比如:我们需要在调用沙箱代码前,输出请求参数日志,在调用沙箱代码后,输出响应结果日志,便于管理员去分析
难道每个代码沙箱类都写一遍log.info?难道每次调用代码沙箱前后都执行log?
使用代理模式,提供一个Proxy,来增强代码沙箱的能力(代理模式的作用就是增强能力)
原本:需要用户自己去调用多次
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用代理后:不仅不用改变原本的代码沙箱实现类,而且对调用者来说,调用方式几乎没有改变,也不需要再每个调用沙箱的地方去写统计代码

在这里插入图片描述

代理模式的实现原理:
1、实现被代理的接口
2、通过构造函数接受一个被代理的接口实现类
3、通过被代理的接口实现类,在调用前后增加对应的操作
CodeSandboxProxy 示例代码:

@Slf4j
public class CodeSandboxProxy implements CodeSandbox {

    private final CodeSandbox codeSandbox;


    public CodeSandboxProxy(CodeSandbox codeSandbox) {
        this.codeSandbox = codeSandbox;
    }

    @Override
    public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
        log.info("代码沙箱请求信息:" + executeCodeRequest.toString());
        ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);
        log.info("代码沙箱响应信息:" + executeCodeResponse.toString());
        return executeCodeResponse;
    }
}

使用方式:

CodeSandbox codeSandbox = CodeSandboxFactory.newInstance(type);
codeSandbox = new CodeSandboxProxy(codeSandbox);

7)实现示例的代码沙箱

/**
 * 示例代码沙箱(仅为了跑通业务流程)
 */
@Slf4j
public class ExampleCodeSandbox implements CodeSandbox {
    @Override
    public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
        List<String> inputList = executeCodeRequest.getInputList();
        ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
        executeCodeResponse.setOutputList(inputList);
        executeCodeResponse.setMessage("测试执行成功");
        executeCodeResponse.setStatus(QuestionSubmitStatusEnum.SUCCEED.getValue());
        JudgeInfo judgeInfo = new JudgeInfo();
        judgeInfo.setMessage(JudgeInfoMessageEnum.ACCEPTED.getText());
        judgeInfo.setMemory(100L);
        judgeInfo.setTime();
        executeCodeResponse.setJudgeInfo(judgeInfo);
        return executeCodeResponse;
    }
}

判题服务开发

定义单独的judgeService 类,而不是把所有判题相关的代码写到questionSubmitService 里,有利于后续的模块抽离,微服务改造

判题服务业务流程

1)传入题目的提交id,获取到对应的题目,提交信息(包含代码,编程语言等)
2)如果题目提交状态不为等待中,就不用重复执行了,只执行等待中的题目
3)更改题目提交状态为“判题中”,防止重复执行,也能让用户即时看到状态
4)调用沙箱,获取到执行结果
5)根据沙箱的执行结果,设置题目的判题状态和信息

判断逻辑

1、先判断沙箱执行的结果输出数量是否和预期输出数量相等
2、依次判断每一项输出和预期输出结果是否相等
3、判题题目的限制是否符合要求
4、可能还有其他的异常情况

坐标:yoj-backend\src\main\java\com\yupi\yoj\judge\JudgeServiceImpl.java

@Service
public class JudgeServiceImpl implements JudgeService {
    
    @Value("${codeSandbox.type:example}")
    private String type;

    @Resource
    private QuestionSubmitService questionSubmitService;

    @Resource
    private QuestionService questionService;

    @Override
    public QuestionSubmitVO doJudge(long questionSubmitId) {
//        1)传入题目的提交id,获取到对应的题目,提交信息(包含代码,编程语言等)
        QuestionSubmit questionSubmit = questionSubmitService.getById(questionSubmitId);
        if(questionSubmit == null){
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR,"题目提交信息不存在");
        }
        Long questionId = questionSubmit.getQuestionId();
        Question question = questionService.getById(questionId);
        if(question == null){
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR,"题目不存在");
        }
//        2)如果题目提交状态不为等待中,就不用重复执行了,只执行等待中的题目
        Integer status = questionSubmit.getStatus();
        if(!Objects.equals(status, QuestionSubmitStatusEnum.WAITING.getValue())){
            throw new BusinessException(ErrorCode.OPERATION_ERROR,"题目正在判題中");
        }
//        3)更改题目提交状态为“判题中”,防止重复执行,也能让用户即时看到状态
        QuestionSubmit questionSubmitUpdate = new QuestionSubmit();
        questionSubmitUpdate.setId(questionSubmit.getId());
        questionSubmitUpdate.setStatus(QuestionSubmitStatusEnum.RUNNING.getValue());
        boolean save = questionSubmitService.updateById(questionSubmitUpdate);
        if(!save){
            throw new BusinessException(ErrorCode.SYSTEM_ERROR,"题目状态更新失败");
        }
//        4)调用沙箱,获取到执行结果
        CodeSandbox codesandbox = CodeSandboxFactory.newInstance(type);
        codesandbox = new CodeSandboxProxy(codesandbox);
        String code = questionSubmit.getCode();
        String language = questionSubmit.getLanguage();
        //输入用例转列表
        String judgeCaseStr = question.getJudgeCase();
        List<JudgeCase> judgeCaseList = JSONUtil.toList(judgeCaseStr, JudgeCase.class);
        //把判题用例中的输入用例过滤出来,喂给需要的inputList,得到了输入列表
        List<String> inputList = judgeCaseList.stream().map(JudgeCase::getInput).collect(Collectors.toList());
        ExecutecodeCodeRequest executecodeCodeRequest = ExecutecodeCodeRequest.builder().code(code).language(language).inputList(inputList).build();
//        5)根据沙箱的执行结果,设置题目的判题状态和信息
//        1、先判断沙箱执行的结果输出数量是否和预期输出数量相等
//        2、依次判断每一项输出和预期输出结果是否相等
//        3、判题题目的限制是否符合要求
//        4、可能还有其他的异常情况
        ExecutecodeResponse executecodeResponse = codesandbox.executeCode(executecodeCodeRequest);
        List<String> outputList = executecodeResponse.getOutputList();
        JudgeInfoMessageEnum judgeInfoMessageEnum = JudgeInfoMessageEnum.WAITING;
        if(inputList.size() != outputList.size()){
            judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
            return null;
        }
        for(int i = 0; i < outputList.size();i ++){
            if(!Objects.equals(outputList.get(i), inputList.get(i))){
                judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
                return null;
            }
        }
        //判断题目限制是否符合要求
        JudgeInfo judgeInfo = executecodeResponse.getJudgeInfo();
        Long time = judgeInfo.getTime();
        Long memory = judgeInfo.getMemory();
        String judgeConfigStr = question.getJudgeConfig();
        JudgeConfig judgeConfig = JSONUtil.toBean(judgeConfigStr, JudgeConfig.class);
        Long needtimeLimit = judgeConfig.getTimeLimit();
        Long needmemoryLimit = judgeConfig.getMemoryLimit();
        if(time > needtimeLimit){
            judgeInfoMessageEnum = JudgeInfoMessageEnum.TIME_LIMIT_EXCEEDED;
            return null;
        }
        if(memory > needmemoryLimit){
            judgeInfoMessageEnum = JudgeInfoMessageEnum.MEMORY_LIMIT_EXCEEDED;
            return null;
        }
        return null;
    }
}

策略模式优化

我们的判题策略可能会有很多种,比如:我们的代码沙箱本身执行程序需要消耗时间,这个时间可能不同的编程语言是不同的,比如沙箱执行java要额外话10s

我们可以采用策略模式,针对不同的情况,定义独立的策略,便于修改策略和维护。而不是把所有的判题逻辑,if …else…代码全部混在一起写

实现步骤
1)定义判题策略接口,让代码更加通用化

public interface JudgeStrategy {

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

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

@Data
public class JudgeContext {

    private JudgeInfo judgeInfo;

    private List<String> inputList;

    private List<String> outputList;

    private List<JudgeCase> judgeCaseList;

    private Question question;

    private QuestionSubmit questionSubmit;

}

3)实现默认判题策略,先把judgeService中的代码搬运过来

public class DefaultJudgeStrategy implements JudgeStrategy {
    @Override
    public JudgeInfo doJudge(JudgeContext judgeContext) {
        //        1、先判断沙箱执行的结果输出数量是否和预期输出数量相等
//        2、依次判断每一项输出和预期输出结果是否相等
//        3、判题题目的限制是否符合要求
//        4、可能还有其他的异常情况
        JudgeInfo judgeInfo = judgeContext.getJudgeInfo();
        Long time = judgeInfo.getTime();
        Long memory = judgeInfo.getMemory();
        List<String> inputList = judgeContext.getInputList();
        List<String> outputList = judgeContext.getOutputList();
        List<JudgeCase> judgeCaseList = judgeContext.getJudgeCaseList();
        Question question = judgeContext.getQuestion();
        QuestionSubmit questionSubmit = judgeContext.getQuestionSubmit();
        JudgeInfoMessageEnum judgeInfoMessageEnum = JudgeInfoMessageEnum.ACCEPTED;
        JudgeInfo judgeInfoResponse = new JudgeInfo();
        judgeInfoResponse.setTime(time);
        judgeInfoResponse.setMemory(memory);
        if(inputList.size() != outputList.size()){
            judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
            judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
            return judgeInfoResponse;
        }
        for(int i = 0; i < outputList.size();i ++){
            if(!Objects.equals(outputList.get(i), inputList.get(i))){
                judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
                judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
                return judgeInfoResponse;
            }
        }
        //判断题目限制是否符合要求
        String judgeConfigStr = question.getJudgeConfig();
        JudgeConfig judgeConfig = JSONUtil.toBean(judgeConfigStr, JudgeConfig.class);
        Long needtimeLimit = judgeConfig.getTimeLimit();
        Long needmemoryLimit = judgeConfig.getMemoryLimit();
        if(time > needtimeLimit){
            judgeInfoMessageEnum = JudgeInfoMessageEnum.TIME_LIMIT_EXCEEDED;
            judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
            return judgeInfoResponse;
        }
        if(memory > needmemoryLimit){
            judgeInfoMessageEnum = JudgeInfoMessageEnum.MEMORY_LIMIT_EXCEEDED;
            judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
            return judgeInfoResponse;
        }
        judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
        return judgeInfoResponse;
    }
}

此时的yoj-backend\src\main\java\com\yupi\yoj\judge\JudgeServiceImpl.java修改的部分

//        5)根据沙箱的执行结果,设置题目的判题状态和信息
        ExecutecodeResponse executecodeResponse = codesandbox.executeCode(executecodeCodeRequest);
        List<String> outputList = executecodeResponse.getOutputList();
        JudgeStrategy judgeStrategy = new DefaultJudgeStrategy();
        JudgeContext judgeContext = new JudgeContext();
        judgeContext.setJudgeInfo(executecodeResponse.getJudgeInfo());
        judgeContext.setInputList(inputList);
        judgeContext.setOutputList(outputList);
        judgeContext.setJudgeCaseList(judgeCaseList);
        judgeContext.setQuestion(question);
        judgeContext.setQuestionSubmit(questionSubmit);
        JudgeInfo judgeInfo = judgeStrategy.doJudge(judgeContext);
        //6)修改数据库中的判题结果
        questionSubmitUpdate = new QuestionSubmit();
        questionSubmitUpdate.setId(questionSubmit.getId());
        questionSubmitUpdate.setJudgeInfo(JSONUtil.toJsonStr(judgeInfo));
        questionSubmitUpdate.setStatus(QuestionSubmitStatusEnum.SUCCEED.getValue());
        save = questionSubmitService.updateById(questionSubmitUpdate);
        if(!save){
            throw new BusinessException(ErrorCode.SYSTEM_ERROR,"题目状态更新错误");
        }
        QuestionSubmit questionSubmitResult = questionSubmitService.getById(questionId);
        return questionSubmitResult;

4)再新增一种判题策略,通过if…else…的方式选择哪种策略

坐标:yoj-backend\src\main\java\com\yupi\yoj\judge\strategy\JavaLanguageJudgeStrategy.java

public class JavaLanguageJudgeStrategy implements JudgeStrategy {
    @Override
    public JudgeInfo doJudge(JudgeContext judgeContext) {
        //        1、先判断沙箱执行的结果输出数量是否和预期输出数量相等
//        2、依次判断每一项输出和预期输出结果是否相等
//        3、判题题目的限制是否符合要求
//        4、可能还有其他的异常情况
        JudgeInfo judgeInfo = judgeContext.getJudgeInfo();
        Long time = judgeInfo.getTime();
        Long memory = judgeInfo.getMemory();
        List<String> inputList = judgeContext.getInputList();
        List<String> outputList = judgeContext.getOutputList();
        List<JudgeCase> judgeCaseList = judgeContext.getJudgeCaseList();
        Question question = judgeContext.getQuestion();
        QuestionSubmit questionSubmit = judgeContext.getQuestionSubmit();
        JudgeInfoMessageEnum judgeInfoMessageEnum = JudgeInfoMessageEnum.ACCEPTED;
        JudgeInfo judgeInfoResponse = new JudgeInfo();
        judgeInfoResponse.setTime(time);
        judgeInfoResponse.setMemory(memory);
        if(inputList.size() != outputList.size()){
            judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
            judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
            return judgeInfoResponse;
        }
        for(int i = 0; i < outputList.size();i ++){
            if(!Objects.equals(outputList.get(i), inputList.get(i))){
                judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
                judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
                return judgeInfoResponse;
            }
        }
        //判断题目限制是否符合要求
        String judgeConfigStr = question.getJudgeConfig();
        JudgeConfig judgeConfig = JSONUtil.toBean(judgeConfigStr, JudgeConfig.class);
        Long needtimeLimit = judgeConfig.getTimeLimit();
        Long needmemoryLimit = judgeConfig.getMemoryLimit();
        //假设JAVA程序本身需要额外执行10s
        long JAVA_TIME_COST = 1000L;
        if(time - JAVA_TIME_COST> needtimeLimit){
            judgeInfoMessageEnum = JudgeInfoMessageEnum.TIME_LIMIT_EXCEEDED;
            judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
            return judgeInfoResponse;
        }
        if(memory - JAVA_TIME_COST > needmemoryLimit){
            judgeInfoMessageEnum = JudgeInfoMessageEnum.MEMORY_LIMIT_EXCEEDED;
            judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
            return judgeInfoResponse;
        }
        judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
        return judgeInfoResponse;
    }
}
JudgeStrategy judgeStrategy = new DefaultJudgeStrategy();
if (language.equals("java")) {
    judgeStrategy = new JavaLanguageJudgeStrategy();
}
JudgeInfo judgeInfo = judgeStrategy.doJudge(judgeContext);

但是,如果选择某种判题策略过程比较复杂,如果都写在调用判题服务的代码中,代码会越来越复杂,会有大量的if…else…,所以建议单独编写一个判断策略的类

5)定义JudgeManager,目的是尽量简化对判题功能的调用,让调用方写最少的代码,调用最简单。对于判题策略的选取,也是在JudgeManager里处理的
坐标:com.yupi.yoj.judge.JudgeManager

/**
 * 判题管理(简化调用)
 */
@Service
public class JudgeManager {

    /**
     * 执行判题
     *
     * @param judgeContext
     * @return
     */
    JudgeInfo doJudge(JudgeContext judgeContext) {
        QuestionSubmit questionSubmit = judgeContext.getQuestionSubmit();
        String language = questionSubmit.getLanguage();
        JudgeStrategy judgeStrategy = new DefaultJudgeStrategy();
        if ("java".equals(language)) {
            judgeStrategy = new JavaLanguageJudgeStrategy();
        }
        return judgeStrategy.doJudge(judgeContext);
    }

}

6)在JudgeServiceImpl中,将judgeStrategy.doJudege() 改为 judgeManager.doJudge()

7)坐标:com.yupi.yoj.service.impl.QuestionSubmitServiceImpl
在题目提交实现类中,在返回结果前,执行判题服务

小知识-Lombox Builder 注解

以前我们是使用new 对象后,再逐行执行set方法的方式来给对象进行赋值的。
还有另外一种可能更方便的方式builder。
1)实体类加上@Builder等注解

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExecuteCodeRequest {

    private List<String> inputList;

    private String code;

    private String language;
}

2)可以使用链式的方式更方便地给对象赋值

ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
    .code(code)
    .language(language)
    .inputList(inputList)
    .build();

小知识-Mybatis-plus updateById方法

根据id进行更新,但是参数却是为实体类,根据传入参数的类的id进行更新,不会全部更新,只会更新设置了值的属性

项目地址

(求求大佬们赏个star~)

前端:https://github.com/IMZHEYA/yoj-frontend
后端:https://github.com/IMZHEYA/yoj-backend
代码沙箱:https://github.com/IMZHEYA/yoj-code-sandbox

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-11 21:04:02       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-11 21:04:02       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-11 21:04:02       58 阅读
  4. Python语言-面向对象

    2024-07-11 21:04:02       69 阅读

热门阅读

  1. Qt 的Q_PROPERTY关键字

    2024-07-11 21:04:02       20 阅读
  2. C++ 入门08:运算符重载

    2024-07-11 21:04:02       24 阅读
  3. [AI 大模型] 百度 文心一言

    2024-07-11 21:04:02       21 阅读
  4. 架构面试-场景题-单点登录(SSO)怎么实现的

    2024-07-11 21:04:02       24 阅读
  5. 深入理解Spring Boot中的事件驱动架构

    2024-07-11 21:04:02       21 阅读
  6. DDD架构面试问题

    2024-07-11 21:04:02       21 阅读
  7. 解析 pdfminer pdfparser.py

    2024-07-11 21:04:02       23 阅读
  8. 解决vue3子组件onMounted中获取不到props的值

    2024-07-11 21:04:02       17 阅读
  9. 关系代数中的八种基本运算

    2024-07-11 21:04:02       22 阅读