判题机模块预开发
- 梳理判题模块和代码沙箱的关系
 判题模块:调用代码沙箱,把代码和输入交给代码沙箱去执行。
 代码沙箱:只负责接收代码和输入,返回编译运行的结果,不负责判题。(可以作为独立的项目/服务,提供给其他的需要执行代码的项目去使用)
 这两个模块完全解耦。
 思考:为什么代码沙箱要接受和输出一组运行用例?
 每道题有多组用例,如果每条用例都单独调用一次代码沙箱,会调用多个接口、需要多次网络传输、程序要多次编译、记录程序的执行状态(重复代码不重复编译)
代码沙箱开发
- 定义代码沙箱的接口,提高通用性(之后我们的项目代码只调用接口,不调用具体实现类,这样在调用其他代码沙箱实现类,就不用去修改名称了,便于扩展)
 扩展思路:代码可以增加一个查看代码沙箱状态的接口
- 定义多种不同代码沙箱实现:
 示例代码沙箱、远程代码沙箱、第三方代码沙箱
LomBok Builder注解:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor- 
编写单元测试,验证单个代码沙箱的执行 @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某个沙箱代码写死了,若要改用其他沙箱,需要改动很多处代码
- 
使用工厂模式,根据用户传入的字符串参数来生成对应代码沙箱实现类 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);
 }
 }
只需要根据字符串来判断然后生成沙箱,无需再手动创建。
- 
参数配置化,把项目中一些可选项交给用户去自定义选项或字符串,写到配置文件中。这样只需改变配置文件,而无需看代码内容,就可以更方便自由的自定义使用项目更多功能。 #代码沙箱配置
 codesandbox:
 type: example@Value("${codesandbox.type}") 
 private String type;
- 
代码沙箱能力增强:比如在调用代码沙箱前,输出请求参数;在代码沙箱调用后,输出响应结果日志,便于管理员分析 
 ------>使用代理模式,提供一个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;
 }
 }
 
- 示例代码沙箱(单纯跑通业务流程)
判题服务完整业务流程实现
判题服务业务流程
- 
获取题目id,获取对应题目提交信息(代码,编程语言) 
- 
如果提交状态不为等待中就不用重复执行 
- 
更改题目提交状态"判题中",防止重复执行,也能让用户看到判题状态 
- 
调用沙箱,获取执行结果 
- 
根据执行结果,设置题目判题状态和信息 
 判断逻辑: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;
 }
 }