判题机模块预开发
- 梳理判题模块和代码沙箱的关系
判题模块:调用代码沙箱,把代码和输入交给代码沙箱去执行。
代码沙箱:只负责接收代码和输入,返回编译运行的结果,不负责判题。(可以作为独立的项目/服务,提供给其他的需要执行代码的项目去使用)
这两个模块完全解耦。
思考:为什么代码沙箱要接受和输出一组运行用例?
每道题有多组用例,如果每条用例都单独调用一次代码沙箱,会调用多个接口、需要多次网络传输、程序要多次编译、记录程序的执行状态(重复代码不重复编译)
代码沙箱开发
- 定义代码沙箱的接口,提高通用性(之后我们的项目代码只调用接口,不调用具体实现类,这样在调用其他代码沙箱实现类,就不用去修改名称了,便于扩展)
扩展思路:代码可以增加一个查看代码沙箱状态的接口 - 定义多种不同代码沙箱实现:
示例代码沙箱、远程代码沙箱、第三方代码沙箱
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;
}
}