在線評測系統(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; @Autowiredprivate 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 {@Autowiredprivate LanguageProperties languageProperties;@Overrideprotected CompileResult compileCodeByDocker(String containerId, String userCodePath) {// 解釋型語言,編譯步驟為空實現,直接返回成功return CompileResult.success();}@Overrideprotected 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 實例。*/@Autowiredpublic 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. 結論
在復雜的系統設計中,直接的思考過程實現往往會導致僵化的、難以維護的系統。這時候不妨先對系統中每個類的職責先進行分析清楚,然后借助相關設計模式的思路,將業務邏輯進行解耦合,達到可拓展,可維護的系統架構。