判题机的开发(代码沙箱、三种模式、工厂模式、策略模式优化、代理模式)

发布于:2024-12-19 ⋅ 阅读:(13) ⋅ 点赞:(0)

判题机模块预开发

  1. 梳理判题模块和代码沙箱的关系
    判题模块:调用代码沙箱,把代码和输入交给代码沙箱去执行。
    代码沙箱:只负责接收代码和输入,返回编译运行的结果,不负责判题。(可以作为独立的项目/服务,提供给其他的需要执行代码的项目去使用)
    这两个模块完全解耦。
    思考:为什么代码沙箱要接受和输出一组运行用例?
    每道题有多组用例,如果每条用例都单独调用一次代码沙箱,会调用多个接口、需要多次网络传输、程序要多次编译、记录程序的执行状态(重复代码不重复编译)

代码沙箱开发

  1. 定义代码沙箱的接口,提高通用性(之后我们的项目代码只调用接口,不调用具体实现类,这样在调用其他代码沙箱实现类,就不用去修改名称了,便于扩展)
    扩展思路:代码可以增加一个查看代码沙箱状态的接口
  2. 定义多种不同代码沙箱实现:
    示例代码沙箱、远程代码沙箱、第三方代码沙箱

LomBok Builder注解:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
  1. 编写单元测试,验证单个代码沙箱的执行
@Test
    void executeCode() {
        CodeSandbox codeSandbox=new ExampleCodeSandbox();
        String userCode="int main(){}";
        String codeLanguage= QuestionSubmitLanguageEnum.JAVA.getValue();
        List<String>inputList= Arrays.asList("1 2","2 3");
        ExecuteCodeRequest executeCodeRequest= ExecuteCodeRequest.builder()
                .userCode(userCode)
                .codeLanguage(codeLanguage)
                .inputList(inputList)
                .build();
        ExecuteCodeResponse executeCodeResponse=codeSandbox.executeCode(executeCodeRequest);
        Assertions.assertNotNull(executeCodeResponse);
    }

存在问题:new某个沙箱代码写死了,若要改用其他沙箱,需要改动很多处代码
4. 使用工厂模式,根据用户传入的字符串参数来生成对应代码沙箱实现类

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    while (scanner.hasNext()) {
        String type = scanner.next();
        CodeSandbox codeSandbox = CodeSandboxFactory.newInstance(type);
        String userCode = "int main(){}";
        String codeLanguage = QuestionSubmitLanguageEnum.JAVA.getValue();
        List<String> inputList = Arrays.asList("1 2", "2 3");
        ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
                .userCode(userCode)
                .codeLanguage(codeLanguage)
                .inputList(inputList)
                .build();
        ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);
    }
}

只需要根据字符串来判断然后生成沙箱,无需再手动创建。
5. 参数配置化,把项目中一些可选项交给用户去自定义选项或字符串,写到配置文件中。这样只需改变配置文件,而无需看代码内容,就可以更方便自由的自定义使用项目更多功能。

#代码沙箱配置
codesandbox:
  type: example
-------------------------
@Value("${codesandbox.type}")
private String type;
  1. 代码沙箱能力增强:比如在调用代码沙箱前,输出请求参数;在代码沙箱调用后,输出响应结果日志,便于管理员分析
    ------>使用代理模式,提供一个Proxy,来增强代码沙箱的能力
    原本需要用户自己多次调用日志,使用代理后,调用者只需要调用代理类,代理类调用沙箱类(代理类可以完成一些额外的功能)。优点:不仅不需要改变原本沙箱,对调用者来说,调用方式几乎没有改变,无需在每个调用沙箱的代码上方再去调用日志
@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);
/**
 * 示例代码沙箱(单纯跑通业务流程)
 */
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.setTime(100L);
        judgeInfo.setMemory(100L);
        judgeInfo.setMessage(JudgeInfoMessageEnum.ACCEPTED.getText());
        executeCodeResponse.setJudgeInfo(judgeInfo);
        return executeCodeResponse;
    }
}

判题服务完整业务流程实现

判题服务业务流程

  1. 获取题目id,获取对应题目提交信息(代码,编程语言)
  2. 如果提交状态不为等待中就不用重复执行
  3. 更改题目提交状态“判题中”,防止重复执行,也能让用户看到判题状态
  4. 调用沙箱,获取执行结果
  5. 根据执行结果,设置题目判题状态和信息
    判断逻辑:1、先判断沙箱执行的结果输出数量是否和预期数量相等;2、判断每一项输出和预期输出是否相等;3、判断题目的限制是否符合要求;4、还可能有其他异常情况
@Service
public class JudegeServiceImpl implements JudegeService {
    @Value("${codesandbox.type}")
    private String type;
    @Resource
    private QuestionService questionService;
    @Resource
    private QuestionSubmitService questionSubmitService;
    /**
     * 1. 获取题目id,获取对应题目提交信息(代码,编程语言)
     * 2. 如果提交状态不为等待中就不用重复执行
     * 3. 更改题目提交状态“判题中”,防止重复执行,也能让用户看到判题状态
     * 4. 调用沙箱,获取执行结果
     * 5. 根据执行结果,设置题目判题状态和信息
     * @param questionSubmitId
     * @return
     */
    @Override
    public QuestionSubmitVO doJudege(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. 如果提交状态不为等待中就不用重复执行
        if(!questionSubmit.getCodeStatus().equals(QuestionSubmitStatusEnum.WAITING.getValue())){
            throw new BusinessException(ErrorCode.OPERATION_ERROR,"正在判题中!");
        }
        //3. 更改题目提交状态“判题中”,防止重复执行,也能让用户看到判题状态
        QuestionSubmit questionSubmitUpdate=new QuestionSubmit();
        questionSubmitUpdate.setId(questionSubmitId);
        questionSubmitUpdate.setCodeStatus(QuestionSubmitStatusEnum.RUNNING.getValue());
        boolean update=questionSubmitService.updateById(questionSubmitUpdate);
        if(!update){
            throw new BusinessException(ErrorCode.SYSTEM_ERROR,"题目状态更新错误!");
        }
//        4. 调用沙箱,获取执行结果
        CodeSandbox codeSandbox= CodeSandboxFactory.newInstance(type);
        codeSandbox=new CodeSandboxProxy(codeSandbox);
        String codeLanguage = questionSubmit.getCodeLanguage();
        String userCode = questionSubmit.getUserCode();
        //获取输入用例
        String judegeCaseStr=question.getJudgeCase();
        List<JudgeCase>judgeCaseList=JSONUtil.toList(judegeCaseStr, JudgeCase.class);
        List<String>inputList=judgeCaseList.stream().map(JudgeCase::getInput).collect(Collectors.toList());
        ExecuteCodeRequest executeCodeRequest= ExecuteCodeRequest.builder()
                .userCode(userCode)
                .codeLanguage(codeLanguage)
                .inputList(inputList)
                .build();
        ExecuteCodeResponse executeCodeResponse=codeSandbox.executeCode(executeCodeRequest);

//        5. 根据执行结果,设置题目判题状态和信息
        JudgeInfoMessageEnum judgeInfoMessageEnum=JudgeInfoMessageEnum.WAITING;
        List<String> outputList = executeCodeResponse.getOutputList();
//        依次判断每一项输出和预期是否相等
        if(outputList.size()!=inputList.size()){
            judgeInfoMessageEnum=JudgeInfoMessageEnum.WRONG_ANSWER;
            return null;
        }
        for (int i=0;i<judgeCaseList.size();i++){
            if(!judgeCaseList.get(i).getOutput().equals(outputList.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 needMemoryLimit = judgeConfig.getMemoryLimit();
        Long needTimeLimit= judgeConfig.getTimeLimit();
        if(memory>needMemoryLimit){
            judgeInfoMessageEnum=JudgeInfoMessageEnum.MEMORY_LIMIT_EXCEEDED;
            return null;
        }
        if(time>needTimeLimit){
            judgeInfoMessageEnum=JudgeInfoMessageEnum.TIME_LIMIT_EXCEEDED;
            return null;
        }
        return null;
    }
}

策略模式

思考:我们的代码沙箱本身执行时间,对于不同的编程语言是不同的。所以我们可以采用策略模式。针对不同的情况,定义独立的策略,而不是把所有的判题逻辑全部混在一起。
首先编写默认判题模块,如果所有的选择判题策略都写在判题服务代码中,代码会过于复杂,产生很多if-else,建议单独编写判断策略的方法。–>定义JudgeManager,尽量简化对判题功能的调用,

/**
 * 判题管理,简化调用
 *
 * @Author Adellle
 * @Date 2024/12/18 19:44
 * @Version 1.0
 */
 @Service
public class JudgeManager {
    JudgeInfo doJudge(JudgeContext judgeContext) {
        QuestionSubmit questionSubmit = judgeContext.getQuestionSubmit();
        String codeLanguage = questionSubmit.getCodeLanguage();
        JudgeStrategy judgeStrategy = new DefaultJudgeStrategy();
        if ("java".equals(codeLanguage)) {
            judgeStrategy = new JavaLanguageDefaultJudgeStrategy();
        }
        return judgeStrategy.doJudge(judgeContext);
    }
}

代码逻辑梳理

 @Resource
    @Lazy
    private JudgeService judgeService;

    /**
     * 提交题目
     *
     * @param questionSubmitAddRequest
     * @param loginUser
     * @return
     */
    @Override
    public long doQuestionSubmit(QuestionSubmitAddRequest questionSubmitAddRequest, User loginUser) {
        // 校验编程语言是否合法(校验合法性)
        String language = questionSubmitAddRequest.getCodeLanguage();
        QuestionSubmitLanguageEnum languageEnum = QuestionSubmitLanguageEnum.getEnumByValue(language);
        if (languageEnum == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "编程语言错误");
        }
        Long questionId = questionSubmitAddRequest.getQuestionId();
        // 判断实体是否存在,根据类别获取实体
        Question question = questionService.getById(questionId);
        if (question == null) {
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
        }
        // 是否已提交题目
        long userId = loginUser.getId();
        // 每个用户串行提交题目
        QuestionSubmit questionSubmit = new QuestionSubmit();
        questionSubmit.setUserId(userId);
        questionSubmit.setQuestionId(questionId);
        questionSubmit.setUserCode(questionSubmitAddRequest.getUserCode());
        questionSubmit.setCodeLanguage(questionSubmitAddRequest.getCodeLanguage());
        // 设置初始状态
        questionSubmit.setCodeStatus(QuestionSubmitStatusEnum.WAITING.getValue());
        questionSubmit.setJudgeInfo("{}");
        boolean save = this.save(questionSubmit);
        if (!save) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "插入数据失败!");
        }
        long questionSubmitId=questionSubmit.getId();
        //  (执行判题服务(异步执行))
        //作用:调用 judgeService.doJudege 方法来执行判题操作,使用CompletableFuture.runAsync() 来异步执行判题任务。
        CompletableFuture.runAsync(() -> {
            judgeService.doJudege(questionSubmitId);
        });
        return questionSubmitId;
    }

注:CompletableFuture.runAsync 会在独立的线程中异步执行判题操作,不会阻塞主线程。需要确保 judgeService.doJudege(questionSubmitId) 方法能够正确处理判题逻辑,并且判题是一个耗时操作,因此采用了异步方式。这里使用异步执行来提高系统性能,避免主线程被阻塞,确保用户能够快速得到提交的反馈。

开始判题

public interface JudgeService {
    /**
     * 判题服务
     *
     * @param questionSubmitId
     * @return
     */
    QuestionSubmit doJudege(long questionSubmitId);
}
@Service
public class JudgeServiceImpl implements JudgeService {
    @Value("${codesandbox.type}")
    private String type;
    @Resource
    private QuestionService questionService;
    @Resource
    private QuestionSubmitService questionSubmitService;
    @Resource
    private JudgeManager judgeManager;

    /**
     * 1. 获取题目id,获取对应题目提交信息(代码,编程语言)
     * 2. 如果提交状态不为等待中就不用重复执行
     * 3. 更改题目提交状态“判题中”,防止重复执行,也能让用户看到判题状态
     * 4. 调用沙箱,获取执行结果
     * 5. 根据执行结果,设置题目判题状态和信息
     *
     * @param questionSubmitId
     * @return
     */
    @Override
    public QuestionSubmit doJudege(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. 如果提交状态不为等待中就不用重复执行
        if (!questionSubmit.getCodeStatus().equals(QuestionSubmitStatusEnum.WAITING.getValue())) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "正在判题中!");
        }
        //3. 更改题目提交状态“判题中”,防止重复执行,也能让用户看到判题状态
        QuestionSubmit questionSubmitUpdate = new QuestionSubmit();
        questionSubmitUpdate.setId(questionSubmitId);
        questionSubmitUpdate.setCodeStatus(QuestionSubmitStatusEnum.RUNNING.getValue());
        boolean update = questionSubmitService.updateById(questionSubmitUpdate);
        if (!update) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "题目状态更新错误!");
        }
//        4. 调用沙箱,获取执行结果
        CodeSandbox codeSandbox = CodeSandboxFactory.newInstance(type);
        codeSandbox = new CodeSandboxProxy(codeSandbox);
        String codeLanguage = questionSubmit.getCodeLanguage();
        String userCode = questionSubmit.getUserCode();
        //获取输入用例
        String judegeCaseStr = question.getJudgeCase();
        List<JudgeCase> judgeCaseList = JSONUtil.toList(judegeCaseStr, JudgeCase.class);
        List<String> inputList = judgeCaseList.stream().map(JudgeCase::getInput).collect(Collectors.toList());
        ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
                .userCode(userCode)
                .codeLanguage(codeLanguage)
                .inputList(inputList)
                .build();
        ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);
        List<String> outputList = executeCodeResponse.getOutputList();
//        5. 根据执行结果,设置题目判题状态和信息
        JudgeContext judgeContext = new JudgeContext();
        judgeContext.setJudgeInfo(executeCodeResponse.getJudgeInfo());
        judgeContext.setInputList(inputList);
        judgeContext.setOutputList(outputList);
        judgeContext.setQuestion(question);
        judgeContext.setJudgeCaseList(judgeCaseList);
        judgeContext.setQuestionSubmit(questionSubmit);
        JudgeInfo judgeInfo = judgeManager.doJudge(judgeContext);
        //6. 修改数据库中的判题结果
        questionSubmitUpdate = new QuestionSubmit();
        questionSubmitUpdate.setId(questionSubmitId);
        questionSubmitUpdate.setCodeStatus(QuestionSubmitStatusEnum.SUCCEED.getValue());
        questionSubmitUpdate.setJudgeInfo(JSONUtil.toJsonStr(judgeInfo));
        update = questionSubmitService.updateById(questionSubmitUpdate);
        if (!update) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "题目状态更新错误!");
        }
        QuestionSubmit res = questionSubmitService.getById(questionId);
        return res;
    }
}

  • 使用策略模式,不把所有的if-else都放在判题服务中,减少对判题功能的调用
@Service
public class JudgeManager {
    JudgeInfo doJudge(JudgeContext judgeContext) {
        QuestionSubmit questionSubmit = judgeContext.getQuestionSubmit();
        String codeLanguage = questionSubmit.getCodeLanguage();
        JudgeStrategy judgeStrategy = new DefaultJudgeStrategy();
        if ("java".equals(codeLanguage)) {
            judgeStrategy = new JavaLanguageDefaultJudgeStrategy();
        }
        return judgeStrategy.doJudge(judgeContext);
    }
}
public class DefaultJudgeStrategy implements JudgeStrategy {

    @Override
    public JudgeInfo doJudge(JudgeContext judgeContext) {
        JudgeInfo judgeInfo = judgeContext.getJudgeInfo();
        Long time = judgeInfo.getTime();
        Long memory = judgeInfo.getMemory();
        JudgeInfo judgeInfoResponse=new JudgeInfo();
        judgeInfoResponse.setTime(time);
        judgeInfoResponse.setMemory(memory);
        List<String> inputList = judgeContext.getInputList();
        List<String> outputList = judgeContext.getOutputList();
        Question question = judgeContext.getQuestion();
        List<JudgeCase> judgeCaseList = judgeContext.getJudgeCaseList();
        JudgeInfoMessageEnum judgeInfoMessageEnum=JudgeInfoMessageEnum.ACCEPTED;
//        依次判断每一项输出和预期是否相等
        if(outputList.size()!=inputList.size()){
            judgeInfoMessageEnum=JudgeInfoMessageEnum.WRONG_ANSWER;
            judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
            return judgeInfoResponse;
        }
        for (int i=0;i<judgeCaseList.size();i++){
            if(!judgeCaseList.get(i).getOutput().equals(outputList.get(i))){
                judgeInfoMessageEnum=JudgeInfoMessageEnum.WRONG_ANSWER;
                judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
                return judgeInfoResponse;
            }
        }
        //判断题目限制
        String judgeConfigStr = question.getJudgeConfig();
        JudgeConfig judgeConfig = JSONUtil.toBean(judgeConfigStr, JudgeConfig.class);
        Long needMemoryLimit = judgeConfig.getMemoryLimit();
        Long needTimeLimit= judgeConfig.getTimeLimit();
        if(memory>needMemoryLimit){
            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;
        }
        judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
        return judgeInfoResponse;
        
    }
}
public class JavaLanguageDefaultJudgeStrategy implements JudgeStrategy {

    @Override
    public JudgeInfo doJudge(JudgeContext judgeContext) {
        JudgeInfo judgeInfo = judgeContext.getJudgeInfo();
        Long time = judgeInfo.getTime();
        Long memory = judgeInfo.getMemory();
        JudgeInfo judgeInfoResponse = new JudgeInfo();
        judgeInfoResponse.setTime(time);
        judgeInfoResponse.setMemory(memory);
        List<String> inputList = judgeContext.getInputList();
        List<String> outputList = judgeContext.getOutputList();
        Question question = judgeContext.getQuestion();
        List<JudgeCase> judgeCaseList = judgeContext.getJudgeCaseList();
        JudgeInfoMessageEnum judgeInfoMessageEnum = JudgeInfoMessageEnum.ACCEPTED;
//        依次判断每一项输出和预期是否相等
        if (outputList.size() != inputList.size()) {
            judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
            judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
            return judgeInfoResponse;
        }
        for (int i = 0; i < judgeCaseList.size(); i++) {
            if (!judgeCaseList.get(i).getOutput().equals(outputList.get(i))) {
                judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
                judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
                return judgeInfoResponse;
            }
        }
        //判断题目限制
        String judgeConfigStr = question.getJudgeConfig();
        JudgeConfig judgeConfig = JSONUtil.toBean(judgeConfigStr, JudgeConfig.class);
        Long needMemoryLimit = judgeConfig.getMemoryLimit();
        Long needTimeLimit = judgeConfig.getTimeLimit();
        if (memory > needMemoryLimit) {
            judgeInfoMessageEnum = JudgeInfoMessageEnum.MEMORY_LIMIT_EXCEEDED;
            judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
            return judgeInfoResponse;
        }
        //Java程序本身需要额外执行10s
        long JAVA_PROGRAM_TIME_COST = 10000L;
        if ((time - JAVA_PROGRAM_TIME_COST) > needTimeLimit) {
            judgeInfoMessageEnum = JudgeInfoMessageEnum.TIME_LIMIT_EXCEEDED;
            judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
            return judgeInfoResponse;
        }
        judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
        return judgeInfoResponse;
    }
}