OJ在线评测系统 后端 用策略模式优化判题机架构
判题机架构优化(策略模式)
思考
我们的判题策略可能会有很多种 比如 我们的代码沙箱本身执行程序需要消耗时间
这个时间可能不同的编程语言是不同的 比如沙箱执行Java要额外花费2秒
我们可以采用策略模式 针对不同的情况 定义不同独立的策略
而不是把所有情况全部放在一个if else里面
定义一个策略接口
我們先写一个方法
这个方法传入的是一个上下文对象context
package com.dduo.dduoj.judge.strategy;
import com.dduo.dduoj.model.dto.questionsubmit.JudgeInfo;
/*
* 判题策略
* */
public interface JudgeStrategy {
/*
* 执行判题
* @param judgeContext
* @return
* */
JudgeInfo doJudge(JudgeContext judgeContext);
}
创建这个上下文对象
我们传入的数据 有判题状态 输入用例 输出用例
用于定义在策略中传递的参数
package com.dduo.dduoj.judge.strategy;
import com.dduo.dduoj.model.dto.questionsubmit.JudgeInfo;
import com.dduo.dduoj.model.entity.Question;
import lombok.Data;
import java.util.List;
/*
* 上下文 用于定义在策略中传递的参数
* */
@Data
public class JudgeContext {
private JudgeInfo judgeInfo;
private List<String> inputList;
private List<String> outputList;
private Question question;
}
接下来我们要把刚刚写的判题逻辑部分的代码搬到接口的实现类里面去
这边创建一个策略模式接口的实现类
默认实现类
类名为DefaultJudgeStrategy
我们先从上下文对象中获得信息
再进行判断
返回值
package com.dduo.dduoj.judge.strategy;
import cn.hutool.json.JSONUtil;
import com.dduo.dduoj.model.dto.question.JudgeCase;
import com.dduo.dduoj.model.dto.question.JudgeConfig;
import com.dduo.dduoj.model.dto.questionsubmit.JudgeInfo;
import com.dduo.dduoj.model.entity.Question;
import com.dduo.dduoj.model.enums.JudgeInfoMessageEnum;
import javax.swing.*;
import java.util.List;
public class DefaultJudgeStrategyImpl implements JudgeStrategy {
/**
* 执行判题
* @param judgeContext
* @return
*/
@Override
public JudgeInfo doJudge(JudgeContext judgeContext) {
// 从上下文对象获取信息
JudgeInfo judgeInfo = judgeContext.getJudgeInfo();
List<String> inputList = judgeContext.getInputList();
List<String> outputList = judgeContext.getOutputList();
Question question = judgeContext.getQuestion();
List<JudgeCase> judgeCaseList = judgeContext.getJudgeCaselist();
// 从判题信息中获取信息
Long memory = judgeInfo.getMemoryLimit();
Long time = judgeInfo.getTime();
JudgeInfo judgeInfoResponse = new JudgeInfo();
JudgeInfoMessageEnum judgeInfoMessageEnum = JudgeInfoMessageEnum.Accepted;
judgeInfoResponse.setMemoryLimit(memory);
judgeInfoResponse.setTime(time);
// 先判断沙箱执行的结果输出数量是否和预期输出数量相等
if (outputList.size() != inputList.size()) {
judgeInfoMessageEnum = JudgeInfoMessageEnum.Wrong_Answer;
judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
return judgeInfoResponse;
}
// 依次判断每一项输出和预期输出是否相等
for (int i = 0; i < judgeCaseList.size(); i++) {
JudgeCase judgeCase = judgeCaseList.get(i);
if (!judgeCase.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.Memory_Limit_Exceeded;
judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
return judgeInfoResponse;
}
judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
return judgeInfoResponse;
}
}
我们把所有判题的内容 放到一个单独的类里面 这个类叫做默认策略
接下来我们就可以在实现类里面用默认策略去解决
先根据沙箱的执行结果设置题目的判题状态和信息
再把上下文对象放到策略模式里面去
package com.dduo.dduoj.judge;
import cn.hutool.json.JSONUtil;
import com.dduo.dduoj.common.ErrorCode;
import com.dduo.dduoj.exception.BusinessException;
import com.dduo.dduoj.judge.codesandbox.CodeSandbox;
import com.dduo.dduoj.judge.codesandbox.CodeSandboxFactory;
import com.dduo.dduoj.judge.codesandbox.CodeSandboxProxy;
import com.dduo.dduoj.judge.codesandbox.model.ExecuteCodeRequest;
import com.dduo.dduoj.judge.codesandbox.model.ExecuteCodeResponse;
import com.dduo.dduoj.judge.strategy.DefaultJudgeStrategy;
import com.dduo.dduoj.judge.strategy.JudgeContext;
import com.dduo.dduoj.judge.strategy.JudgeStrategy;
import com.dduo.dduoj.model.dto.question.JudgeCase;
import com.dduo.dduoj.model.dto.questionsubmit.JudgeInfo;
import com.dduo.dduoj.model.entity.Question;
import com.dduo.dduoj.model.entity.QuestionSubmit;
import com.dduo.dduoj.model.enums.QuestionSubmitStatusEnum;
import com.dduo.dduoj.service.QuestionService;
import com.dduo.dduoj.service.QuestionSubmitService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class JudgeServiceImpl implements JudgeService {
// 题目服务
@Resource
private QuestionService questionService;
// 题目提交服务
@Resource
private QuestionSubmitService questionSubmitService;
@Value("${codesandbox.type:example}")
private String value;
@Override
public QuestionSubmit doJudge(Long questionSubmitId) {
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, "题目不存在");
}
// 题目存在
// 开始判题
// 更改题目的状态 status
// 如果不为等待状态
if (questionSubmit.getStatus().equals(QuestionSubmitStatusEnum.WAITING.getValue())) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "题目正在判题中");
}
// 重新设置
QuestionSubmit questionSubmitUpdate = new QuestionSubmit();
questionSubmitUpdate.setId(questionSubmitId);
questionSubmitUpdate.setStatus(QuestionSubmitStatusEnum.RUNNING.getValue());
boolean judge = questionSubmitService.updateById(questionSubmitUpdate);
if (!judge) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "题目状态更新错误");
}
// 接下来放代码沙箱
CodeSandbox codeSandbox = CodeSandboxFactory.NewInstance(value);
codeSandbox = new CodeSandboxProxy(codeSandbox);
// 拿出数据
String code = questionSubmit.getCode();
String language = questionSubmit.getLanguage();
// 获取输入用例
String judgeCaseStr = question.getJudgeCase();
List<JudgeCase> judgeCaselist = JSONUtil.toList(judgeCaseStr, JudgeCase.class);
List<String> inputList = judgeCaselist.stream().map(JudgeCase::getInput).collect(Collectors.toList());
ExecuteCodeRequest executeRequest = ExecuteCodeRequest.builder().code(code).language(language).inputList(inputList).build();
ExecuteCodeResponse executeCodeResponse=codeSandbox.executeCode(executeRequest);
List<String> outputList = executeCodeResponse.getOutputList();
// 根据沙箱的执行结果 设置题目的判题状态和信息
JudgeContext judgeContext = new JudgeContext();
judgeContext.setJudgeInfo(executeCodeResponse.getJudgeInfo());
judgeContext.setInputList(inputList);
judgeContext.setOutputList(outputList);
judgeContext.setQuestion(question);
judgeContext.setJudgeCaselist(judgeCaselist);
// 用默认策略去解决
JudgeStrategy judgeStrategy=new DefaultJudgeStrategy();
JudgeInfo judgeInfo = judgeStrategy.doJudge(judgeContext);
// 修改数据库中的判题结果
questionSubmitUpdate = new QuestionSubmit();
questionSubmitUpdate.setId(questionSubmitId);
questionSubmitUpdate.setStatus(QuestionSubmitStatusEnum.SUCCESS.getValue());
questionSubmitUpdate.setJudgeInfo(JSONUtil.toJsonStr(judgeInfo));
// 看数据库
boolean update = questionSubmitService.updateById(questionSubmitUpdate);
if (!update) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "题目状态更新错误");
}
// 返回结果
QuestionSubmit questionSubmitResult = questionSubmitService.getById(questionId);
return questionSubmitResult;
}
}
刚才只是定义了默认策略
我们还要定义其他策略
如Java程序的执行的策略
接下来我们就要去想一想如何去切换策略
但是如果用简单的判断 如果有复杂的情况会很麻烦 而且 写的if else语句会变的很多
建议单独编写一个判断策略的方法 或者是类
工厂 map缓存
定义JudgeManager 目的是尽量简化对判题功能的调用
让调用方最简便
去操作
补全代码
实际上就是对我们选择合适的策略这个过程
或者是进行其他的判断的时候
进行了一个判断
package com.dduo.dduoj.judge;
import com.dduo.dduoj.judge.strategy.DefaultJudgeStrategy;
import com.dduo.dduoj.judge.strategy.JavaLanguageJudgeStrategy;
import com.dduo.dduoj.judge.strategy.JudgeContext;
import com.dduo.dduoj.judge.strategy.JudgeStrategy;
import com.dduo.dduoj.model.dto.questionsubmit.JudgeInfo;
import com.dduo.dduoj.model.entity.QuestionSubmit;
/*
* 判题管理(简化调用)
* */
public class JudgeManger {
/*
* 执行判题
* @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);
}
}
这样就行
在之前的题目提交实现类 核心逻辑里面
那么我们的核心逻辑就是这样的
@Override
public long doQuestionSubmit(QuestionSubmitAddRequest questionSubmitAddRequest, User loginUser) {
// 校验编程语言是否合法
String language = questionSubmitAddRequest.getLanguage();
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.setCode(questionSubmitAddRequest.getCode());
questionSubmit.setLanguage(language);
// 设置初始状态
questionSubmit.setStatus(QuestionSubmitStatusEnum.WAITING.getValue());
questionSubmit.setJudgeInfo("{}");
boolean save = this.save(questionSubmit);
if (!save){
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "数据插入失败");
}
// todo 执行判题服务
Long questionSubmitId = questionSubmit.getId();
// 执行判题服务
CompletableFuture.runAsync(() -> {
judgeService.doJudge(questionSubmitId);
});
return questionSubmitId;
}