在看这期之前,建议先看前三期:
Java 原生实现代码沙箱(OJ判题系统第1期)------设计思路、实现步骤、代码实现-CSDN博客
Java 原生实现代码沙箱之Java 程序安全控制(OJ判题系统第2期)------设计思路、实现步骤、代码实现-CSDN博客
Java 原生实现代码沙箱之代码沙箱 Docker 实现(OJ判题系统第3期)------设计思路、实现步骤、代码实现-CSDN博客
判题模块和代码沙箱的关系
判题模块:调用代码沙箱,把代码和输入交给代码沙箱去执行
代码沙箱:只负责接受代码和输入,返回编译运行的结果,不负责判题(可以作为独立的项目 / 服务,提供给其他的需要执行代码的项目去使用)
代码沙箱架构开发
1.定义代码沙箱的接口,提高通用性
之后我们的项目代码只调用接口,不调用具体的实现类,这样在你使用其他的代码沙箱实现类 时,就不用去修改名称了, 便于扩展。
java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExecuteCodeResponse {
private List<String> outputList;
/**
* 接口信息
*/
private String message;
/**
* 执行状态
*/
private Integer status;
/**
* 判题信息
*/
private JudgeInfo judgeInfo;
}
字段说明:
字段名 类型 含义说明 outputList
List<String>
每个测试用例对应的标准输出结果列表。例如 ["Hello World\n", "Error: ..."]
message
String
接口级别的提示信息,如编译错误、超时等。 status
Integer
执行状态码。例如:<br>1=成功<br>2=编译失败<br>3=运行时错误<br>4=超时 judgeInfo
JudgeInfo
更详细的判题信息,如最大内存占用、最大时间消耗等。
java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExecuteCodeRequest {
private List<String> inputList;
private String code;
private String language;
}
字段说明:
字段名 类型 含义说明 inputList
List<String>
用户提供的多个输入用例列表。例如 ["1 2", "3 4"]
表示两个测试用例。code
String
用户提交的源代码字符串。比如一段 Java 程序。 language
String
用户选择的编程语言,如 "java"
、"python"
、"cpp"
等。
java
public interface CodeSandbox {
/**
* 执行代码
*
* @param executeCodeRequest
* @return
*/
ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest);
}
方法说明:
方法名 参数类型 返回类型 含义说明 executeCode
ExecuteCodeRequest
ExecuteCodeResponse
执行用户提交的代码,并返回执行结果
2.定义多种不同的代码沙箱实现
示例代码沙箱:仅为了跑通业务流程
java
// 使用 Lombok 的 @Slf4j 注解,自动生成一个日志对象 log,用于记录运行时信息(例如调试、错误等)
@Slf4j
public class ExampleCodeSandbox implements CodeSandbox {
/**
* 执行用户提交的代码,并返回执行结果。
* 这是一个示例实现类,不实际执行任何真实代码,仅模拟响应结果。
*
* @param executeCodeRequest 包含用户输入参数、代码内容、编程语言等的请求对象
* @return ExecuteCodeResponse 返回执行结果,包含输出、状态码、提示信息、判题信息等
*/
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
// 从请求对象中获取用户的输入用例列表
// inputList 是一个 List<String>,表示多个测试用例的输入,如 ["1 2", "3 4"]
List<String> inputList = executeCodeRequest.getInputList();
// 创建一个空的 ExecuteCodeResponse 对象,用于封装并返回最终的执行结果
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
// 设置响应中的 outputList 字段
// 在这个示例中,直接将输入用例原样作为输出结果返回,仅为演示使用
executeCodeResponse.setOutputList(inputList);
// 设置响应中的 message 字段,用于给调用者一个简要的执行结果说明
// 在这个示例中,固定设置为"测试执行成功"
executeCodeResponse.setMessage("测试执行成功");
// 设置响应中的 status 字段,表示本次执行的状态
// QuestionSubmitStatusEnum.SUCCEED.getValue() 表示成功的状态码,比如 1
executeCodeResponse.setStatus(QuestionSubmitStatusEnum.SUCCEED.getValue());
// 创建一个 JudgeInfo 对象,用于封装更详细的执行信息,如内存占用、执行时间等
JudgeInfo judgeInfo = new JudgeInfo();
// 设置判题信息中的 message 字段
// JudgeInfoMessageEnum.ACCEPTED.getText() 表示程序正确通过测试,例如 "Accepted"
judgeInfo.setMessage(JudgeInfoMessageEnum.ACCEPTED.getText());
// 设置判题信息中的 memory 字段,表示程序运行过程中使用的最大内存(单位:字节)
// 示例中设为 100L,仅为演示
judgeInfo.setMemory(100L);
// 设置判题信息中的 time 字段,表示程序运行的总时间(单位:毫秒或微秒,视具体定义而定)
// 示例中设为 100L,仅为演示
judgeInfo.setTime(100L);
// 将构建好的 judgeInfo 对象设置到 executeCodeResponse 中
executeCodeResponse.setJudgeInfo(judgeInfo);
// 最终返回完整的执行结果对象
return executeCodeResponse;
}
}
远程代码沙箱:实际调用接口的沙箱
java
public class RemoteCodeSandbox implements CodeSandbox {
// 定义鉴权请求头名称,用于HTTP请求中携带认证信息
private static final String AUTH_REQUEST_HEADER = "auth";
// 定义鉴权密钥,作为身份验证的一部分发送到远程服务
private static final String AUTH_REQUEST_SECRET = "secretKey";
/**
* 执行用户提交的代码,并返回执行结果。
* 这个实现类通过调用远程服务来执行代码,并接收执行结果。
*
* @param executeCodeRequest 包含用户输入参数、代码内容、编程语言等的请求对象
* @return ExecuteCodeResponse 返回执行结果,包含输出、状态码、提示信息、判题信息等
*/
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
// 打印日志,标识当前使用的是远程代码沙箱
System.out.println("远程代码沙箱");
// 定义远程服务的URL地址
String url = "http://localhost:8090/executeCode";
// 将executeCodeRequest对象转换为JSON字符串,便于通过HTTP请求传递
String json = JSONUtil.toJsonStr(executeCodeRequest);
// 发起POST请求到远程服务,并处理响应
String responseStr = HttpUtil.createPost(url)
.header(AUTH_REQUEST_HEADER, AUTH_REQUEST_SECRET) // 设置请求头中的认证信息
.body(json) // 设置请求体为JSON字符串
.execute() // 发送请求并获取响应
.body(); // 获取响应体内容(字符串形式)
// 检查响应字符串是否为空或空白
if (StringUtils.isBlank(responseStr)) {
// 如果响应为空,则抛出业务异常,表示API请求错误
throw new BusinessException(ErrorCode.API_REQUEST_ERROR,
"executeCode remoteSandbox error, message = " + responseStr);
}
// 将响应字符串转换为ExecuteCodeResponse对象,并返回
return JSONUtil.toBean(responseStr, ExecuteCodeResponse.class);
}
}
第三方代码沙箱:调用网上现成的代码沙箱
java
public class ThirdPartyCodeSandbox implements CodeSandbox {
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
System.out.println("第三方代码沙箱");
return null;
}
}
编写单元测试,验证单个代码沙箱的执行
java
// 使用 SpringBootTest 注解表示这是一个 Spring Boot 测试类
// 会启动整个 Spring Boot 应用上下文,用于集成测试
@SpringBootTest
class CodeSandboxTest {
/**
* 从 application.yml 或 application.properties 中读取配置项:
* codesandbox.type 的值,默认为 "example"
* 可以根据该配置决定使用哪个具体的 CodeSandbox 实现类
*/
@Value("${codesandbox.type:example}")
private String type;
/**
* 单元测试方法:测试 CodeSandbox 接口的 executeCode 方法是否能正常执行
* 这里直接测试了 RemoteCodeSandbox 的实现
*/
@Test
void executeCode() {
// 创建一个远程代码沙箱实例(RemoteCodeSandbox)
// 该类通过 HTTP 请求调用远程服务来执行用户的代码
CodeSandbox codeSandbox = new RemoteCodeSandbox();
// 定义一段示例代码字符串
// 注意:这里虽然是 C/C++ 风格的 main 函数,但在实际使用中应传入符合语言类型的实际代码
String code = "int main() { }";
// 设置编程语言类型,从枚举 QuestionSubmitLanguageEnum 中获取 Java 对应的值
// 例如可能是:"java"
String language = QuestionSubmitLanguageEnum.JAVA.getValue();
// 定义输入列表 inputList,模拟多个测试用例的输入参数
// 每个元素是一个测试用例的输入,如 "1 2" 表示运行程序时的标准输入内容
List<String> inputList = Arrays.asList("1 2", "3 4");
// 构建 ExecuteCodeRequest 请求对象,封装所有必要的参数
// 包括用户提交的代码、使用的编程语言、多个测试用例的输入
ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
.code(code) // 用户提交的源代码
.language(language) // 编程语言
.inputList(inputList)// 输入参数列表
.build(); // 构建请求对象
// 调用 codeSandbox.executeCode 方法,向远程代码沙箱发送执行请求
// 返回结果封装在 ExecuteCodeResponse 对象中
ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);
// 使用 JUnit 的 Assertions.assertNotNull 方法断言返回结果不为空
// 如果为 null,则测试失败,说明 executeCode 方法未正确返回结果
Assertions.assertNotNull(executeCodeResponse);
}
}
工厂模式
问题背景:多个 CodeSandbox 实现类如何统一创建?
在你的在线判题系统中,可能有多种不同类型的代码沙箱实现:
沙箱类型 功能描述 ExampleCodeSandbox
示例沙箱,模拟执行结果,适用于开发测试 RemoteCodeSandbox
远程沙箱,通过 HTTP 请求调用远程服务执行代码 ThirdPartyCodeSandbox
第三方沙箱,调用其他平台 API 执行代码 当你需要根据配置或用户输入动态选择不同的沙箱时,就面临以下问题:
❌ 如果不用工厂模式,会出现什么问题?
调用者需要知道所有实现类
- 调用方必须了解每个具体的实现类,并根据条件手动 new 出来。
- 增加了耦合度,不利于扩展。
创建逻辑分散
- 创建对象的逻辑分布在多个地方,修改起来麻烦。
- 容易出错,维护成本高。
不便于统一管理
- 比如想统一记录日志、做权限控制等,都得在每个地方写一遍。
解决方案:使用工厂模式集中管理对象创建
为了解决上述问题,我们可以使用 工厂模式(Factory Pattern)。
💡 什么是工厂模式?
工厂模式是一种创建型设计模式,用于封装对象的创建过程。它将对象的创建和使用解耦,使得客户端无需关心具体实现类,只需告诉工厂"我要什么",就能得到对应的实例。
📈 工厂模式的优势:
优势 描述 ✅ 解耦 调用者不需要知道具体类名,只需要一个标识符(如字符串) ✅ 易于扩展 新增沙箱类型时,只需修改工厂类,符合开闭原则 ✅ 统一管理 可以在工厂中加入统一逻辑(如日志、缓存、异常处理) ✅ 简化调用 调用者使用方式简单,不需要重复写 if-else 或 switch-case
使用工厂模式,根据用户传入的字符参数(沙箱类别),来生成对应的代码沙箱实现类
此处使用静态工厂模式
java
/**
* 代码沙箱工厂类(根据传入的类型字符串创建对应的代码沙箱实例)
*
* 工厂模式是一种常见的设计模式,用于统一管理对象的创建过程。
* 通过该工厂类,可以根据配置动态决定使用哪种 CodeSandbox 实现。
*/
public class CodeSandboxFactory {
/**
* 根据指定的沙箱类型创建并返回对应的 CodeSandbox 实例。
*
* @param type 沙箱类型,支持:"example"、"remote"、"thirdParty"
* 如果不匹配任何类型,默认返回 ExampleCodeSandbox
* @return CodeSandbox 返回一个实现了 CodeSandbox 接口的对象
*/
public static CodeSandbox newInstance(String type) {
// 使用 switch-case 结构判断传入的类型字符串,并返回对应的实现类实例
switch (type) {
// 如果类型是 "example",返回示例代码沙箱(模拟执行结果,不实际运行代码)
case "example":
return new ExampleCodeSandbox();
// 如果类型是 "remote",返回远程代码沙箱(调用远程服务执行代码)
case "remote":
return new RemoteCodeSandbox();
// 如果类型是 "thirdParty",返回第三方代码沙箱(可能调用外部平台 API)
case "thirdParty":
return new ThirdPartyCodeSandbox();
// 默认情况:如果传入的类型不匹配以上任意一种,返回示例代码沙箱作为兜底方案
default:
return new ExampleCodeSandbox();
}
}
}
由此,我们可以根据字符串动态生成实例,提高了通用性:
java
public static void main(String[] args) {
// 创建一个 Scanner 对象,用于从标准输入(控制台)读取用户输入内容
Scanner scanner = new Scanner(System.in);
// 进入一个无限循环,持续监听用户的输入,直到手动终止程序
while (scanner.hasNext()) {
// 从控制台读取下一个字符串作为沙箱类型(例如:"example"、"remote"、"thirdParty")
String type = scanner.next();
// 使用 CodeSandboxFactory 工厂类根据用户输入的类型创建对应的代码沙箱实例
CodeSandbox codeSandbox = CodeSandboxFactory.newInstance(type);
// 定义一段示例代码字符串(注意:这里虽然是 C/C++ 风格的 main 函数,但语言设置为 Java)
// 实际使用时应确保 code 和 language 字段匹配,否则可能执行失败
String code = "int main() { }";
// 设置编程语言类型,从枚举 QuestionSubmitLanguageEnum 中获取 Java 的值
// 例如可能是:"java"
String language = QuestionSubmitLanguageEnum.JAVA.getValue();
// 定义输入列表 inputList,模拟多个测试用例的输入参数
// 每个元素是一个测试用例的输入,如 "1 2" 表示运行程序时的标准输入内容
List<String> inputList = Arrays.asList("1 2", "3 4");
// 构建 ExecuteCodeRequest 请求对象,封装所有必要的参数
// 包括用户提交的代码、使用的编程语言、多个测试用例的输入
ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
.code(code) // 用户提交的源代码
.language(language) // 编程语言
.inputList(inputList)// 输入参数列表
.build(); // 构建请求对象
// 调用 codeSandbox.executeCode 方法,向指定的代码沙箱发送执行请求
// 返回结果封装在 ExecuteCodeResponse 对象中
ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);
// 此处没有输出或断言,只是简单调用 executeCode 方法
// 在实际调试中可以加入 System.out.println 或日志打印,观察返回结果
}
}
工厂模式适用场景总结
场景 是否适合用工厂 多个相似类的选择创建 ✅ 适合 对象创建过程复杂 ✅ 适合 需要隐藏具体类名 ✅ 适合 希望统一管理创建逻辑 ✅ 适合 提供默认实现兜底机制 ✅ 适合
最终总结一句话:
工厂模式就像一个智能制造车间,你只需要告诉它"生产哪种型号的产品",它就会自动返回合适的实例,而你完全不需要关心它是怎么造出来的。
代理模式
问题背景:代码沙箱调用前后的日志记录需求
在开发一个在线判题系统(OJ)时,我们经常需要对代码沙箱的调用行为进行监控和日志记录。比如:
- 在调用代码沙箱之前,记录用户传入的请求参数(如代码内容、输入数据等)。
- 在调用代码沙箱之后,记录返回结果(如输出内容、执行状态、资源占用等)。
- 这些日志对于后续的调试、审计、性能分析都非常有用。
❌ 如果不做统一处理,会出现什么问题?
假设我们有多个
CodeSandbox
实现类(例如:ExampleCodeSandbox
、RemoteCodeSandbox
、ThirdPartyCodeSandbox
),如果我们在每个实现类中都手动添加日志代码,就会出现以下问题:
问题 描述 ✅ 重复代码多 每个类都要写一遍相同的日志打印逻辑,造成代码冗余 ✅ 扩展性差 后续如果要增加新的功能(如统计耗时、权限校验等),需要修改所有类 ✅ 职责不单一 日志记录属于通用逻辑,不应该污染核心业务逻辑(执行代码)
解决方案:使用代理模式统一增强能力
为了解决上述问题,我们可以使用 代理模式(Proxy Pattern)。
💡 什么是代理模式?
代理模式是一种结构型设计模式,用于控制对某个对象的访问,通常用来增强其功能,而不改变其接口或调用方式。
📈 代理模式的优势:
不改变原有类的代码 :原有的
CodeSandbox
实现类不需要做任何改动。不改变调用者的行为 :调用者依然通过
codeSandbox.executeCode(...)
的方式调用,透明无感知。集中管理增强逻辑:如日志记录、权限控制、耗时统计等功能,都可以统一放在代理类中。
具体实现思路
java
// 使用 Lombok 的 @Slf4j 注解,自动生成一个日志对象 log(类型为 org.slf4j.Logger)
// 可以直接使用 log.info(...) 来打印日志信息,无需手动创建 logger 实例
@Slf4j
public class CodeSandboxProxy implements CodeSandbox {
// 被代理的真实代码沙箱对象(被包装的对象)
// 所有的 executeCode 请求最终都会委托给这个对象来处理
private final CodeSandbox codeSandbox;
/**
* 构造函数,用于创建代理对象时传入真实的代码沙箱实例
*
* @param codeSandbox 真实的代码沙箱对象,例如 ExampleCodeSandbox、RemoteCodeSandbox 等
*/
public CodeSandboxProxy(CodeSandbox codeSandbox) {
this.codeSandbox = codeSandbox;
}
/**
* 重写 CodeSandbox 接口中的 executeCode 方法
* 这是一个代理方法:在调用真实对象前后添加了额外的功能(如日志记录)
*
* @param executeCodeRequest 用户提交的执行代码请求对象,包含代码内容、输入数据、语言等
* @return ExecuteCodeResponse 执行结果响应对象,包含输出内容、状态码、判题信息等
*/
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
// 【前置增强】在执行代码沙箱前,打印请求参数日志
// 便于管理员查看用户提交了什么代码、输入是什么、使用的编程语言是什么
// 使用 log.info 记录日志,方便后续调试和问题追踪
log.info("代码沙箱请求信息:" + executeCodeRequest.toString());
// 【核心功能】将用户的请求交给真实的代码沙箱对象去执行
// 这里并没有改变原来的业务逻辑,只是在调用前后增加了日志记录
// executeCodeResponse 是真实沙箱返回的结果
ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);
// 【后置增强】在代码沙箱执行完成后,打印响应结果日志
// 包括输出内容、执行状态、判题信息等,方便管理员分析运行情况
log.info("代码沙箱响应信息:" + executeCodeResponse.toString());
// 将执行结果原样返回给调用者,保持接口的一致性
return executeCodeResponse;
}
}
代理模式适用场景
场景 是否适合用代理 简要说明 控制对象访问 ✅ 适合 通过代理控制对对象的访问权限,比如只有登录用户才能执行某些操作。 增强对象功能 ✅ 适合 在不修改原对象的前提下,为其添加额外功能,如日志记录、性能统计等。 远程调用代理 ✅ 适合 为远程服务提供本地代理,屏蔽网络通信细节,如远程代码沙箱调用。 延迟加载(虚拟代理) ✅ 适合 只有在真正需要时才创建和加载资源,提高系统性能,例如图片懒加载。 缓存结果 ✅ 适合 代理可先检查缓存是否存在结果,存在则直接返回,避免重复计算或请求。
代理模式的核心价值总结
代理模式就像给目标对象穿上一件"智能外衣",不仅让它保持原有的行为不变,还能在其前后增加各种增强逻辑(如日志记录、权限校验、性能监控),而且这一切都是透明的,不需要修改目标对象本身。
代理模式的优势总结
优势 描述 ✅ 解耦 调用者无需关心具体实现细节,只需通过代理进行操作 ✅ 易于扩展 新增功能时只需修改代理类,符合开闭原则 ✅ 统一管理 可以在代理中集中处理公共逻辑(如日志、权限、性能统计) ✅ 简化调用 调用者使用方式简单,不需要重复写相同的逻辑