在看這期之前,建議先看前五期:
Java 原生實現代碼沙箱(OJ判題系統第1期)——設計思路、實現步驟、代碼實現-CSDN博客
Java 原生實現代碼沙箱之Java 程序安全控制(OJ判題系統第2期)——設計思路、實現步驟、代碼實現-CSDN博客
Java 原生實現代碼沙箱之代碼沙箱 Docker 實現(OJ判題系統第3期)——設計思路、實現步驟、代碼實現-CSDN博客
OJ判題系統第4期之判題機模塊架構——設計思路、實現步驟、代碼實現(工廠模式、代理模式的實踐)-CSDN博客?
OJ判題系統第5期之判題服務開發——設計思路、實現步驟、代碼實現-CSDN博客?
判題邏輯的主要指責
定義
- 判題邏輯?是具體判斷用戶提交的代碼是否正確的核心算法或規則集。它專注于解析沙箱返回的結果,并根據預定義的標準(如測試用例、時間限制、內存限制等)判斷代碼的正確性。
- 這是一個低層次的模塊,主要關注具體的判題細節。
主要職責
- 解析沙箱輸出:從沙箱返回的結果中提取關鍵信息(如輸出、錯誤信息、耗時、內存占用等)。
- 比對測試用例:將沙箱的輸出與題目提供的標準答案進行比對,判斷每個測試用例是否通過。
- 生成判題報告:根據比對結果生成詳細的判題報告(如哪些測試用例通過了,哪些失敗了,失敗的原因是什么)。
- 性能評估:根據資源消耗情況(如時間、內存)評估代碼的效率。
策略模式優化
什么是策略模式(Strategy Pattern)?
策略模式是一種行為型設計模式,它定義了一系列算法或策略,并將每一個算法封裝起來,使它們可以互相替換,獨立于使用它們的客戶端。
簡單理解:
- 把不同的“處理方式”封裝成一個個獨立的類。
- 客戶端在運行時決定使用哪一種策略。
- 這樣可以讓程序更靈活、更易擴展、更易維護。
問題背景:判題邏輯復雜,存在多種判斷方式?
在你的在線判題系統中,可能會遇到以下幾種情況:
情況 描述 Java 執行較慢 Java 程序啟動沙箱需要額外耗時(如 10 秒),總執行時間不能只看用戶代碼運行時間 Python 內存限制寬松 不同語言對內存的消耗不同,判題標準也要變化 C++ 要求輸出完全一致 對輸出格式要求嚴格,必須完全匹配才算通過 時間/內存限制動態調整 根據題目難度、語言類型等自動調整閾值 ? 如果不用策略模式,會出現什么問題?
- 判題邏輯中會充斥大量?
if...else
?或?switch...case
- 新增一個語言或規則時,要修改原有邏輯,違反開閉原則
- 各種語言規則混在一起,可讀性差,容易出錯
- 難以復用、難以測試、難以維護
解決方案:使用策略模式統一管理判題規則?
🔧 設計思路
我們將“如何根據沙箱執行結果和語言特性來判定是否通過”的邏輯抽象為一個接口,然后為每種語言提供一個具體的實現類。
這樣做的好處是:
- 每個語言的判題規則彼此隔離,互不干擾
- 可以隨時新增、修改某種語言的判題規則,不影響其他邏輯
- 在運行時可以根據提交的語言類型動態選擇對應的策略
總結一句話?
策略模式就像給系統裝上了一個“可插拔的大腦”,你可以根據不同情況(比如語言類型)自動選擇最合適的判題規則,而無需改動主流程代碼。
策略模式的優勢總結?
優勢 描述 ? 解耦 將判題邏輯與業務流程分離 ? 易擴展 新增語言只需添加策略類,符合開閉原則 ? 易維護 每種語言的規則獨立,便于閱讀和調試 ? 動態切換 支持運行時根據條件選擇不同策略 ? 清晰結構 每個策略職責單一,符合單一職責原則
?實現步驟
定義判題策略接口,讓代碼更加通用化:
/*** 判題策略*/
public interface JudgeStrategy {/*** 執行判題* @param judgeContext* @return*/JudgeInfo doJudge(JudgeContext judgeContext);
}
定義判題上下文對象,用于定義在策略中傳遞的參數(可以理解為一種 DTO):
/*** 上下文類(Context Class)** JudgeContext 用于封裝和傳遞在不同判題策略中需要用到的所有參數。* 它作為數據容器,確保各個判題策略能夠訪問到所需的信息,而不必直接依賴外部對象。*/
@Data // Lombok 注解,自動生成 getter 和 setter 方法
public class JudgeContext {/*** 沙箱執行結果信息** 包含了用戶提交代碼在沙箱中運行時的各項指標,如耗時、內存占用等。* 這些信息對于判斷代碼的正確性和性能至關重要。*/private JudgeInfo judgeInfo;/*** 測試用例輸入列表** 包含了題目定義的所有測試用例的輸入數據。* 在判題過程中,這些輸入將被傳入用戶提交的代碼,并與輸出結果進行比對。*/private List<String> inputList;/*** 用戶代碼的實際輸出列表** 當用戶提交的代碼在沙箱中運行時,根據不同的測試用例輸入,會生成相應的輸出結果。* 這些輸出結果會被收集起來,用于后續的比對和判斷。*/private List<String> outputList;/*** 題目定義的標準測試用例列表** 每個標準測試用例包含了輸入和期望的輸出。* 判題邏輯需要將用戶的實際輸出與這些期望輸出進行比對,以確定是否通過該測試用例。*/private List<JudgeCase> judgeCaseList;/*** 當前題目對象** 包含了題目的所有相關信息,如題目描述、難度等級、附加說明等。* 雖然在大多數情況下,具體的判題邏輯可能不會直接使用這些信息,但它們可能會在某些特定場景下有用。*/private Question question;/*** 用戶提交記錄對象** 包含了用戶提交的詳細信息,如提交時間、編程語言、用戶代碼等。* 這些信息對于跟蹤和記錄用戶的提交歷史非常重要。*/private QuestionSubmit questionSubmit;
}
實現默認判題策略
DefaultJudgeStrategy.java
/*** 默認判題策略實現類** 該策略用于處理通用語言(如 C++、JavaScript 等)的標準判題邏輯。* 包括:* - 判斷輸出數量是否匹配* - 判斷每個測試用例的輸出是否與預期一致* - 檢查內存和時間是否超出題目限制*/
public class DefaultJudgeStrategy implements JudgeStrategy {/*** 執行判題邏輯的核心方法** @param judgeContext 判題上下文對象,包含所有需要的數據* @return 返回最終的判題結果信息(是否通過、錯誤類型、耗時、內存等)*/@Overridepublic JudgeInfo doJudge(JudgeContext judgeContext) {
從上下文中提取關鍵數據
// 從上下文中獲取沙箱返回的執行信息(如時間、內存)JudgeInfo judgeInfo = judgeContext.getJudgeInfo();Long memory = judgeInfo.getMemory(); // 用戶程序使用的內存大小(單位:字節)Long time = judgeInfo.getTime(); // 用戶程序運行的時間(單位:毫秒)// 獲取輸入列表和輸出列表List<String> inputList = judgeContext.getInputList(); // 測試用例的輸入數據List<String> outputList = judgeContext.getOutputList(); // 用戶程序的實際輸出結果// 獲取題目信息和測試用例列表Question question = judgeContext.getQuestion(); // 當前題目對象List<JudgeCase> judgeCaseList = judgeContext.getJudgeCaseList(); // 題目定義的標準測試用例// 初始化判題結果為“接受”狀態JudgeInfoMessageEnum judgeInfoMessageEnum = JudgeInfoMessageEnum.ACCEPTED;// 創建一個新的 JudgeInfo 對象用于封裝最終的判題結果JudgeInfo judgeInfoResponse = new JudgeInfo();judgeInfoResponse.setMemory(memory); // 設置實際使用內存judgeInfoResponse.setTime(time); // 設置實際運行時間
初步校驗輸出數量是否與輸入一致
// 如果輸出數量不等于輸入數量,說明至少有一個測試用例沒有正確輸出if (outputList.size() != inputList.size()) {// 設置為“答案錯誤”judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());return judgeInfoResponse; // 直接返回錯誤結果}
舉例說明:
假設題目有 3 個測試用例,但用戶只輸出了 2 條結果,說明至少有一個用例未通過,無需繼續比對。
?逐條比對輸出與預期是否一致
// 遍歷所有測試用例,檢查每一條輸出是否與期望輸出相等for (int i = 0; i < judgeCaseList.size(); i++) {JudgeCase judgeCase = judgeCaseList.get(i); // 獲取第 i 個標準測試用例String expectedOutput = judgeCase.getOutput(); // 期望輸出String actualOutput = outputList.get(i); // 實際輸出// 如果兩者不一致,則判定為“答案錯誤”if (!expectedOutput.equals(actualOutput)) {judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());return judgeInfoResponse; // 只要有一個測試用例失敗,就直接返回錯誤}}
檢查是否超過題目設定的資源限制
// 從題目對象中獲取判題配置(例如內存限制、時間限制)String judgeConfigStr = question.getJudgeConfig(); // JSON 字符串格式的配置JudgeConfig judgeConfig = JSONUtil.toBean(judgeConfigStr, JudgeConfig.class);// 獲取題目要求的最大內存和最大運行時間Long needMemoryLimit = judgeConfig.getMemoryLimit(); // 內存限制(單位:MB)Long needTimeLimit = judgeConfig.getTimeLimit(); // 時間限制(單位:毫秒)// 將 MB 轉換為字節進行比較(注意單位一致性)if (memory > needMemoryLimit * 1024L * 1024L) { // 1 MB = 1024 KB = 1024*1024 bytesjudgeInfoMessageEnum = 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;}
}
總結一下這個類的職責:
步驟 功能 1?? 提取參數 從? JudgeContext
?中提取所有判題所需的信息2?? 輸出數量校驗 判斷輸出數量是否與輸入數量一致 3?? 輸出內容比對 比較每個測試用例的實際輸出與期望輸出是否一致 4?? 資源限制判斷 判斷是否超出內存或時間限制 5?? 返回結果 構建并返回最終的判題結果
定義 JudgeManager
為什么要定義?JudgeManager
1.?集中管理判題邏輯
- 問題背景: 在沒有?
JudgeManager
?的情況下,每個地方調用判題邏輯時都需要手動判斷使用哪種策略,這會導致大量的重復代碼,增加維護成本。- 解決方案:?
JudgeManager
?將所有的判題邏輯集中管理,使得外部調用者只需傳遞?JudgeContext
?對象即可完成判題操作,無需關心具體的實現細節。2.?提高代碼的可維護性和擴展性
- 問題背景: 如果未來需要支持更多的編程語言或引入新的判題規則,直接修改現有代碼容易引發潛在的風險。
- 解決方案: 使用?
JudgeManager
?和策略模式,可以輕松地添加新的策略類而不需要修改原有代碼,符合開閉原則(對擴展開放,對修改封閉)。3.?簡化客戶端調用
- 問題背景: 客戶端代碼如果直接與各種判題策略打交道,會顯得非常復雜且不易于理解。
- 解決方案:?
JudgeManager
?提供了一個統一的接口,客戶端只需要知道如何構建?JudgeContext
?并調用?doJudge
?方法,極大地簡化了調用過程。4.?增強系統的靈活性
- 問題背景: 不同編程語言可能有不同的運行環境要求(如啟動時間、內存限制等),這些差異需要在判題時特別處理。
- 解決方案:?
JudgeManager
?可以根據編程語言動態選擇最適合的判題策略,確保每種語言都能得到公平準確的評判。
/*** 判題管理器** JudgeManager 是整個判題系統的核心組件之一,負責根據用戶提交的編程語言,* 動態選擇并調用相應的判題策略(JudgeStrategy)。這有助于將復雜的判題邏輯* 進行模塊化封裝,便于后續維護和擴展。*/
@Service // Spring 注解,標識該類為一個服務層組件
public class JudgeManager {/*** 執行判題操作** 該方法接收一個包含所有必要信息的上下文對象(JudgeContext),* 根據用戶提交的編程語言動態選擇合適的判題策略,并返回最終的判題結果。** @param judgeContext 包含了題目、用戶提交、沙箱執行結果等信息的對象* @return 判題結果信息(如是否通過、錯誤類型、耗時、內存等)*/public JudgeInfo doJudge(JudgeContext judgeContext) {// 從上下文中獲取用戶提交的信息QuestionSubmit questionSubmit = judgeContext.getQuestionSubmit();// 獲取用戶提交的編程語言類型String language = questionSubmit.getLanguage();// 初始化默認的判題策略JudgeStrategy judgeStrategy = new DefaultJudgeStrategy();// 根據編程語言選擇不同的判題策略if ("java".equals(language)) {judgeStrategy = new JavaLanguageJudgeStrategy(); // Java 特有的判題策略}// 調用選定的策略執行判題操作return judgeStrategy.doJudge(judgeContext);}
}
執行判題:
整體流程圖
用戶點擊提交按鈕
│
├─→ 校驗編程語言是否合法
│
├─→ 校驗題目是否存在
│
├─→ 構造提交記錄對象(包含代碼、語言、用戶ID、題目ID)
│
├─→ 插入數據庫,設置初始狀態為 WAITING
│
├─→ 獲取提交記錄 ID
│
├─→ 異步調用 judgeService.doJudge(questionSubmitId)
│
└─→ 返回 questionSubmitId 給前端
具體實現
方法簽名與基本說明
/*** 提交題目** 用戶提交代碼進行判題的核心方法。* 主要流程包括:* 1. 參數校驗(語言合法性、題目是否存在)* 2. 構建提交記錄對象并保存到數據庫* 3. 異步調用判題服務進行判題** @param questionSubmitAddRequest 提交請求參數封裝類* @param loginUser 當前登錄用戶信息* @return 返回提交記錄的 ID,用于后續查詢判題結果*/
@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, "編程語言錯誤");}
為什么需要這一步?
- 防止用戶輸入非法或不受支持的編程語言(如 Python3、PHP 等非白名單語言)。
- 增強系統安全性,避免后續處理出現不可控問題。
檢查題目是否存在
// 獲取題目 IDlong questionId = questionSubmitAddRequest.getQuestionId();// 查詢題目實體是否存在Question question = questionService.getById(questionId);if (question == null) {throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);}
為什么需要這一步?
- 確保用戶提交的是一個真實存在的題目,防止攻擊者通過偽造 ID 操作不存在的數據。
- 同時也為后續判題邏輯提供必要的題目信息(如測試用例、限制條件等)。
?構造提交記錄對象,并設置默認狀態
// 獲取當前用戶的 IDlong userId = loginUser.getId();// 創建一個新的提交記錄對象QuestionSubmit questionSubmit = new QuestionSubmit();questionSubmit.setUserId(userId); // 設置用戶 IDquestionSubmit.setQuestionId(questionId); // 設置題目 IDquestionSubmit.setCode(questionSubmitAddRequest.getCode()); // 設置用戶提交的代碼內容questionSubmit.setLanguage(language); // 設置編程語言// 設置初始狀態為“等待中”questionSubmit.setStatus(QuestionSubmitStatusEnum.WAITING.getValue());// 初始 judgeInfo 字段為空 JSON,表示還未判題questionSubmit.setJudgeInfo("{}");
為什么設置初始狀態?
- 表示當前提交正在排隊等待判題,前端或其他模塊可以根據此狀態判斷是否已開始執行。
- 判題完成后會異步更新狀態為“成功”或“失敗”。
將提交記錄保存到數據庫?
// 嘗試將提交記錄插入數據庫boolean save = this.save(questionSubmit);if (!save){throw new BusinessException(ErrorCode.SYSTEM_ERROR, "數據插入失敗");}
為什么需要持久化?
- 記錄每一次提交的歷史,便于后續查詢、統計、審計。
- 即使系統重啟或發生異常,也可以根據數據庫恢復狀態。
獲取提交記錄 ID 并異步觸發判題服務
// 獲取剛剛插入的提交記錄 IDLong questionSubmitId = questionSubmit.getId();// 異步執行判題邏輯,避免阻塞主線程CompletableFuture.runAsync(() -> {judgeService.doJudge(questionSubmitId);});
為什么要異步執行?
- 判題是一個耗時操作(可能涉及啟動沙箱、運行代碼、比對輸出等),如果同步執行會影響接口響應速度。
- 使用?
CompletableFuture
?實現異步處理,提高系統吞吐量和用戶體驗。
?返回提交記錄 ID
// 返回提交記錄 ID,供前端或其他服務使用return questionSubmitId;
}
這個 ID 的用途是什么?
- 前端可以通過這個 ID 輪詢或 WebSocket 監聽判題結果。
- 判題服務也依賴這個 ID 來查找對應的提交記錄和題目信息。