在线评测系统(Online Judge, OJ)的核心是判题引擎,其关键挑战在于如何高效、安全且可扩展地支持多种编程语言。在博主的项目练习过程中,借鉴了相关设计模式实现一种架构设计方案,即通过组合运用模板方法、策略、工厂等设计模式,将判题流程中与语言相关的逻辑进行深度解耦,从而构建一个符合“开闭原则”、易于维护和扩展的现代化判题引擎。
1. 问题域分析:判题引擎的复杂性
一个典型的判题流程包含以下阶段:
- 环境准备:创建隔离的执行环境(如 Docker 容器)。
- 代码编译:将源代码编译成可执行文件(非解释型语言)。
- 代码执行:在受限的环境中运行代码,并监控资源消耗。
- 结果收集:获取程序的输出、错误、执行时间及内存消耗。
- 环境清理:销毁执行环境,回收资源。
该流程的复杂性源于不同编程语言在“编译”和“执行”阶段的显著差异:
- 编译型语言 (C++, Java): 需要特定的编译器和编译指令,生成中间产物(可执行文件或字节码)。
- 解释型语言 (Python, JavaScript): 无需编译,直接通过解释器执行。
- 运行参数: 不同语言的运行时(Runtime)在内存限制、安全策略等方面有不同的配置方式。
若采用过程式编程,通过大量的 if-else
或 switch-case
语句来处理不同语言,将导致代码结构僵化、维护成本高昂,每新增一门语言都可能引发对核心代码的大规模修改,违背了软件设计的 “开闭原则“ (Open-Closed Principle)。
2. 架构设计:设计模式的组合应用
为了应对上述挑战,我们需要采用一系列设计模式来重构判题引擎,将流程中的“不变”与“可变”部分分离。(对于一些生产级别的安全配置本文进行忽略,着重于设计模式的使用)对于实现多语言的容器池参考:[[j借助线程池的思想,构建一个高性能、配置驱动的Docker容器池]])
2.1. 模板方法模式:定义流程骨架
模板方法模式是整个架构的基石。我们定义一个抽象基类 AbstractJudgeTemplate
,它封装了判题流程的固定算法骨架。
@Component
public abstract class AbstractJudgeTemplate {
@Autowired
protected MultiLanguageDockerSandBoxPool sandBoxPool;
// 模板方法,定义了判题的完整流程骨架
public final SandBoxExecuteResult judge(String userCode, List<String> inputList,Long timeLimit) {
// 1. 准备环境
String containerId = prepareEnvironment();
// 2. 创建用户代码文件
String userCodePath = createUserCodePath(containerId);
File userCodeFile = createUserCodeFile(userCode, userCodePath);
try {
// 3. 编译代码
CompileResult compileResult = compileCodeByDocker(containerId, userCodePath); // 传递所需参数
if (!compileResult.isCompiled()) {
// 如果编译失败,也需要清理文件和容器
return SandBoxExecuteResult.fail(CodeRunStatus.COMPILE_FAILED, compileResult.getExeMessage());
}
// 4. 运行代码
return executeCodeByDocker(containerId, inputList,timeLimit); // 传递所需参数
} catch (SecurityException e) {
log.error("代码安全检查失败: {}", e.getMessage());
return SandBoxExecuteResult.fail(CodeRunStatus.SECURITY_ERROR, "代码包含不安全内容");
} catch (ContainerNotAvailableException e) {
log.error("容器资源不足: {}", e.getMessage());
judgeMetrics.recordContainerError(getLanguageType());
return SandBoxExecuteResult.fail(CodeRunStatus.SYSTEM_ERROR, "系统资源不足,请稍后重试");
} catch (Exception e) {
log.error("判题过程发生异常", e);
judgeMetrics.recordSystemError(getLanguageType());
return SandBoxExecuteResult.fail(CodeRunStatus.SYSTEM_ERROR, "系统内部错误");
} finally {
// 5. 清理环境
deleteUserCodeFile(userCodeFile);
cleanupEnvironment(containerId);
}
}
private void deleteUserCodeFile(File userCodeFile) {
if (userCodeFile != null && userCodeFile.exists()) {
FileUtil.del(userCodeFile);
}
}
/**
* 创建用户代码文件
*/
private File createUserCodeFile(String userCode, String userCodePath) {
if (FileUtil.exist(userCodePath)) {
FileUtil.del(userCodePath);
}
return FileUtil.writeString(userCode, userCodePath, Constants.UTF8);
}
private void cleanupEnvironment(String containerId) {
// 只有在 containerId 有效时才归还 ,还可以进行其他校验
if (containerId != null) {
sandBoxPool.returnContainer(containerId);
}
}
//安全检查的相关方法省略...
// --- 抽象方法 (钩子),由子类实现 ---
/**
* 编译代码,不同语言实现不同
*/
protected abstract CompileResult compileCodeByDocker(String containerId, String userCodePath);
/**
* 运行代码,不同语言的运行命令和参数不同
*/
protected abstract SandBoxExecuteResult executeCodeByDocker(String containerId, List<String> inputList,Long timeLimit);
/**
* 准备环境
* @return 容器id
*/
protected abstract String prepareEnvironment();
protected abstract String createUserCodePath(String containerId);
}
通过这种方式,AbstractJudgeTemplate
定义了“做什么”(判题流程),而将“怎么做”(具体语言的编译和运行)的责任下放给了子类。
2.2. 策略模式:封装语言特定逻辑
每个具体的语言实现都可以看作是一种独立的“策略”。我们为每种支持的语言创建一个继承自 AbstractJudgeTemplate
的具体类。
Java 策略实现:
@Service("java")
public class JavaJudgeStrategy extends AbstractJudgeTemplate {
//与docker操作的对象,也可以进行二次封装进行对上层提高封装后的api
@Autowired
private DockerClient dockerClient;
@Autowired
private LanguageProperties languageProperties;
@Override
protected CompileResult compileCodeByDocker(String containerId, String userCodePath) {
// 从配置中获取编译命令
String compileCmd = languageProperties.getJava().getCompileCmd();
// 使用该命令在容器中执行编译...
log.info("Executing compile command: {}", compileCmd);
// ... 省略与沙箱交互的底层代码
return CompileResult.success();
}
@Override
protected SandBoxExecuteResult executeCodeByDocker(String containerId, List<String> inputList,Long timeLimit) {
// 从配置中获取运行命令
String executeCmd = languageProperties.getJava().getExecuteCmd();
List<String> outputList = new ArrayList<>();
for (String input : context.getInputList()) {
// 拼接输入参数并执行...
log.info("Executing run command: {} with input: {}", executeCmd, input);
// ... 省略与沙箱交互的底层代码
}
//封装结果
return getSanBoxResult(inputList, outList, maxMemory, maxUseTime);
}
@Override
protected String prepareEnvironment() {
return sandBoxPool.getContainer(ProgramType.JAVA);
}
@Override
protected String createUserCodePath(String containerId) {
String codeDir = sandBoxPool.getHostCodeDir(containerId);
return codeDir + File.separator +
//从配置中读取也可以
JudgeConstants.USER_CODE_JAVA_CLASS_NAME;
}
//其他方法这里忽略
}
Python 策略实现:
@Service("python3")
public class PythonJudgeStrategy extends AbstractJudgeTemplate {
@Autowired
private LanguageProperties languageProperties;
@Override
protected CompileResult compileCodeByDocker(String containerId, String userCodePath) {
// 解释型语言,编译步骤为空实现,直接返回成功
return CompileResult.success();
}
@Override
protected SandBoxExecuteResult executeCodeByDocker(String containerId, List<String> inputList,Long timeLimit) {
// env.executeCommand(RUN_CMD)
// ... 返回运行结果
}
//其他重写方法
}
现在,每种语言的判题逻辑被隔离在独立的策略类中,实现了高度的内聚和解耦。
2.3. 工厂模式:动态选择策略
有了各种策略,我们需要一个机制来根据客户端请求(例如,任务中指定的语言)动态地选择并实例化正确的策略。工厂模式是解决此问题的理想选择。
结合 Spring 框架的依赖注入(DI),可以实现一个高效的策略工厂。
@Component
public class JudgeStrategyFactory {
private final Map<String, AbstractJudgeTemplate> strategyMap;
/**
* 利用 Spring 的构造函数注入,自动将所有 AbstractJudgeTemplate 类型的 Bean 注入。
* Key 为 Bean 的名称 (e.g., "java", "python3"),Value 为 Bean 实例。
*/
@Autowired
public JudgeStrategyFactory(Map<String, AbstractJudgeTemplate> strategyMap) {
this.strategyMap = strategyMap;
}
public AbstractJudgeTemplate getStrategy(String language) {
AbstractJudgeTemplate strategy = strategyMap.get(language);
if (strategy == null) {
//可以自定义抛出业务异常
throw new ServiceException(ResultCode.FAILED_NOT_SUPPORT_PROGRAM);
}
return strategy;
}
}
客户端调用:
@Service
@Slf4j
public class JudgeServiceImpl implements IJudgeService {
@Autowired
private JudgeStrategyFactory judgeStrategyFactory;
@Autowired
private UserSubmitMapper userSubmitMapper;
@Override
public UserQuestionResultVO doJudgeJavaCode(JudgeSubmitDTO judgeSubmitDTO) {
//获取判题策略对象
AbstractJudgeTemplate strategy = judgeStrategyFactory.getStrategy(judgeSubmitDTO.getProgramType().getDesc());
//调用容器池进行判题
SandBoxExecuteResult sandBoxExecuteResult =
strategy.judge(judgeSubmitDTO.getUserCode(), judgeSubmitDTO.getInputList(),judgeSubmitDTO.getTimeLimit());
UserQuestionResultVO userQuestionResultVO = new UserQuestionResultVO();
//返回判题结果
//成功
//...相关判断
//失败
//...相关判断
//存储用户代码数据到数据库
log.info("判题逻辑结束,判题结果为: {} ", userQuestionResultVO.getPass());
return userQuestionResultVO;
}
}
3. 架构优势与可扩展性
通过上述设计模式的组合应用,我们构建了一个结构清晰、易于扩展的判题引擎:
- 高内聚,低耦合: 每种语言的实现细节被封装在各自的策略类中,与主流程和其他语言实现完全解耦。
- 符合开闭原则:
- 对修改关闭: 核心判题流程
AbstractJudgeTemplate
和调度器JudgeDispatcherService
无需任何修改。 - 对扩展开放: 若要新增对 Go 语言的支持,只需完成两步:
- 创建一个
GoJudgeStrategy
类,继承AbstractJudgeTemplate
并实现其compile
和run
方法。 - 为该类添加
@Component("go")
注解。
系统即可自动集成新的语言支持,无需改动任何已有代码。
- 创建一个
- 对修改关闭: 核心判题流程
- 职责单一: 每个类(模板、策略、工厂)的职责都非常明确,提升了代码的可读性和可维护性。
4. 结论
在复杂的系统设计中,直接的思考过程实现往往会导致僵化的、难以维护的系统。这时候不妨先对系统中每个类的职责先进行分析清楚,然后借助相关设计模式的思路,将业务逻辑进行解耦合,达到可拓展,可维护的系统架构。