【OJ项目】深入剖析 JudgeServiceImpl 类:题目的判题逻辑详解
《深入剖析 JudgeServiceImpl 类:题目的判题逻辑详解》
一、引言
在编程竞赛或者在线编程平台中,判题服务是核心功能之一。它负责对用户提交的代码进行编译、执行,并根据预设的测试用例判断代码的正确性。今天我们就来详细剖析一个名为 JudgeServiceImpl
的 Java 服务类,它实现了题目的判题逻辑。
二、代码整体概述
JudgeServiceImpl
类实现了 JudgeService
接口,主要用于处理题目的判题流程。整个判题过程可以分为以下几个主要步骤:
- 获取题目和提交信息:根据题目提交 ID 获取对应的题目和提交信息。
- 检查提交状态:确保题目提交状态为等待中,避免重复判题。
- 更新状态为判题中:将题目提交状态更新为“判题中”,防止重复执行。
- 调用代码沙箱:执行用户提交的代码,获取执行结果。
- 设置判题状态和信息:根据沙箱执行结果设置题目的判题状态和信息。
- 更新数据库判题结果:将最终的判题结果更新到数据库中。
三、代码详细解析
3.1 类的定义和依赖注入
@Service
public class JudgeServiceImpl implements JudgeService {
@Resource
private QuestionFeignClient questionFeignClient;
@Resource
private JudgeManager judgeManager;
@Value("${codesandbox.type:example}")
private String type;
// ... 其他代码 ...
}
@Service
注解表明这是一个 Spring 服务类。QuestionFeignClient
用于远程调用获取题目和提交信息,以及更新提交状态。JudgeManager
负责具体的判题逻辑。@Value
注解用于获取配置文件中代码沙箱的类型,默认值为example
。
3.2 获取题目和提交信息
@Override
public QuestionSubmit doJudge(long questionSubmitId) {
// 1)传入题目的提交 id,获取到对应的题目、提交信息(包含代码、编程语言等)
QuestionSubmit questionSubmit = questionFeignClient.getQuestionSubmitById(questionSubmitId);
if (questionSubmit == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "提交信息不存在");
}
Long questionId = questionSubmit.getQuestionId();
Question question = questionFeignClient.getQuestionById(questionId);
if (question == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "题目不存在");
}
// ... 其他代码 ...
}
- 通过
questionFeignClient
根据questionSubmitId
获取题目提交信息。 - 如果提交信息不存在,抛出
BusinessException
异常。 - 从提交信息中获取
questionId
,再通过questionFeignClient
获取对应的题目信息。 - 如果题目信息不存在,同样抛出异常。
3.3 检查提交状态
// 2)如果题目提交状态不为等待中,就不用重复执行了
if (!questionSubmit.getStatus().equals(QuestionSubmitStatusEnum.WAITING.getValue())) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "题目正在判题中");
}
- 检查题目提交状态是否为等待中,如果不是则抛出异常,避免重复判题。
3.4 更新状态为判题中
// 3)更改判题(题目提交)的状态为 “判题中”,防止重复执行
QuestionSubmit questionSubmitUpdate = new QuestionSubmit();
questionSubmitUpdate.setId(questionSubmitId);
questionSubmitUpdate.setStatus(QuestionSubmitStatusEnum.RUNNING.getValue());
boolean update = questionFeignClient.updateQuestionSubmitById(questionSubmitUpdate);
if (!update) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "题目状态更新错误");
}
- 创建一个
QuestionSubmit
对象,设置其 ID 和状态为“判题中”。 - 调用
questionFeignClient
的updateQuestionSubmitById
方法更新状态。 - 如果更新失败,抛出异常。
3.5 调用代码沙箱
// 4)调用沙箱,获取到执行结果
CodeSandbox codeSandbox = CodeSandboxFactory.newInstance(type);
codeSandbox = new CodeSandboxProxy(codeSandbox);
String language = questionSubmit.getLanguage();
String code = questionSubmit.getCode();
// 获取输入用例
String judgeCaseStr = question.getJudgeCase();
List<JudgeCase> judgeCaseList = JSONUtil.toList(judgeCaseStr, JudgeCase.class);
List<String> inputList = judgeCaseList.stream().map(JudgeCase::getInput).collect(Collectors.toList());
ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
.code(code)
.language(language)
.inputList(inputList)
.build();
ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);
List<String> outputList = executeCodeResponse.getOutputList();
- 使用
CodeSandboxFactory
创建代码沙箱实例,并使用代理包装。 - 从提交信息中获取代码和编程语言。
- 从题目信息中获取输入用例,并转换为列表。
- 创建
ExecuteCodeRequest
对象,包含代码、编程语言和输入用例列表。 - 调用代码沙箱的
executeCode
方法执行代码,获取执行结果。
3.6 设置判题状态和信息
// 5)根据沙箱的执行结果,设置题目的判题状态和信息
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 = judgeManager.doJudge(judgeContext);
- 创建
JudgeContext
对象,将执行结果、输入用例、题目信息等设置到上下文中。 - 调用
JudgeManager
的doJudge
方法进行具体的判题逻辑,得到判题信息。
3.7 更新数据库判题结果
// 6)修改数据库中的判题结果
questionSubmitUpdate = new QuestionSubmit();
questionSubmitUpdate.setId(questionSubmitId);
questionSubmitUpdate.setStatus(QuestionSubmitStatusEnum.SUCCEED.getValue());
questionSubmitUpdate.setJudgeInfo(JSONUtil.toJsonStr(judgeInfo));
update = questionFeignClient.updateQuestionSubmitById(questionSubmitUpdate);
if (!update) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "题目状态更新错误");
}
QuestionSubmit questionSubmitResult = questionFeignClient.getQuestionSubmitById(questionSubmitId);
return questionSubmitResult;
- 创建一个新的
QuestionSubmit
对象,设置其 ID、状态为“成功”,并将判题信息转换为 JSON 字符串。 - 调用
questionFeignClient
的updateQuestionSubmitById
方法更新数据库中的判题结果。 - 如果更新失败,抛出异常。
- 最后再次获取更新后的题目提交信息并返回。
四、代码优化建议
4.1 异常处理优化
在获取题目和提交信息时,如果信息不存在,直接抛出 BusinessException
,可能会导致上层调用者难以处理。可以在抛出异常前记录日志,方便后续排查问题。同时,可以考虑提供更详细的错误信息。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
public class JudgeServiceImpl implements JudgeService {
private static final Logger logger = LoggerFactory.getLogger(JudgeServiceImpl.class);
// ... 其他代码 ...
@Override
public QuestionSubmit doJudge(long questionSubmitId) {
// 1)传入题目的提交 id,获取到对应的题目、提交信息(包含代码、编程语言等)
QuestionSubmit questionSubmit = questionFeignClient.getQuestionSubmitById(questionSubmitId);
if (questionSubmit == null) {
logger.error("提交信息不存在,提交 ID: {}", questionSubmitId);
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "提交信息不存在,提交 ID: " + questionSubmitId);
}
Long questionId = questionSubmit.getQuestionId();
Question question = questionFeignClient.getQuestionById(questionId);
if (question == null) {
logger.error("题目不存在,题目 ID: {}", questionId);
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "题目不存在,题目 ID: " + questionId);
}
// ... 其他代码 ...
}
}
4.2 重复更新操作优化
在更新题目提交状态和判题结果时,都调用了 questionFeignClient.updateQuestionSubmitById
方法,代码存在重复。可以将更新操作封装成一个单独的方法,提高代码的复用性。
private boolean updateQuestionSubmit(QuestionSubmit questionSubmitUpdate) {
return questionFeignClient.updateQuestionSubmitById(questionSubmitUpdate);
}
@Override
public QuestionSubmit doJudge(long questionSubmitId) {
// ... 其他代码 ...
// 3)更改判题(题目提交)的状态为 “判题中”,防止重复执行
QuestionSubmit questionSubmitUpdate = new QuestionSubmit();
questionSubmitUpdate.setId(questionSubmitId);
questionSubmitUpdate.setStatus(QuestionSubmitStatusEnum.RUNNING.getValue());
if (!updateQuestionSubmit(questionSubmitUpdate)) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "题目状态更新错误");
}
// ... 其他代码 ...
// 6)修改数据库中的判题结果
questionSubmitUpdate = new QuestionSubmit();
questionSubmitUpdate.setId(questionSubmitId);
questionSubmitUpdate.setStatus(QuestionSubmitStatusEnum.SUCCEED.getValue());
questionSubmitUpdate.setJudgeInfo(JSONUtil.toJsonStr(judgeInfo));
if (!updateQuestionSubmit(questionSubmitUpdate)) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "题目状态更新错误");
}
// ... 其他代码 ...
}
4.3 错误处理优化
在调用代码沙箱执行代码时,如果出现异常,没有进行处理。可以添加异常处理逻辑,将题目提交状态更新为“失败”。
try {
ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);
List<String> outputList = executeCodeResponse.getOutputList();
// ... 其他代码 ...
} catch (Exception e) {
logger.error("代码沙箱执行代码出错,提交 ID: {}", questionSubmitId, e);
QuestionSubmit questionSubmitUpdate = new QuestionSubmit();
questionSubmitUpdate.setId(questionSubmitId);
questionSubmitUpdate.setStatus(QuestionSubmitStatusEnum.FAILED.getValue());
if (!updateQuestionSubmit(questionSubmitUpdate)) {
logger.error("题目状态更新为失败出错,提交 ID: {}", questionSubmitId);
}
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "代码沙箱执行代码出错");
}
4.4 最后获取结果的 ID 错误修正
questionFeignClient.getQuestionSubmitById(questionId);
这里传入的是 questionId
,应该传入 questionSubmitId
。
QuestionSubmit questionSubmitResult = questionFeignClient.getQuestionSubmitById(questionSubmitId);
return questionSubmitResult;
五、总结
通过对 JudgeServiceImpl
类的详细剖析,我们了解了题目的判题逻辑的具体实现。同时,通过优化建议,我们可以提高代码的健壮性、可维护性和可读性,避免一些潜在的错误。在实际开发中,我们可以根据具体需求对代码进行进一步的扩展和优化。