当前位置: 首页 > news >正文

Java 原生实现代码沙箱(OJ判题系统第1期)——设计思路、实现步骤、代码实现

设计思路:


1、保存代码文件

✅ 目的:
将用户提交的源码以字符串形式写入磁盘,生成 .java 文件。

📌 原因:
Java 是静态语言,必须先编译成 .class 文件才能运行。
需要物理文件路径来调用 javac 或使用 JavaCompiler API 编译。
可以通过隔离目录结构(如 UUID 子目录)实现用户代码之间的隔离。

实现步骤:

  1. 获取当前工作目录路径

    • 使用 System.getProperty("user.dir") 获取当前运行程序的工作目录路径。例如:/home/user/project
  2. 构建全局代码存储目录的完整路径

    • 将工作目录路径与全局代码存储目录名称(假设是一个预定义的常量 GLOBAL_CODE_DIR_NAME)拼接起来,形成全局代码存储目录的完整路径。例如:/home/user/project/codeTemp
  3. 检查并创建全局代码存储目录

    • 使用工具类 FileUtil.exist(globalCodePathName) 判断全局代码存储目录是否存在。如果不存在,则调用 FileUtil.mkdir(globalCodePathName) 创建该目录。
  4. 生成用户专属目录

    • 通过 UUID.randomUUID() 生成一个唯一标识符,并将其作为当前用户的代码存放目录名,目的是为了避免多个用户提交代码时发生文件覆盖的问题。例如:/home/user/project/codeTemp/uuid-xxxxxx
  5. 构造具体的Java文件路径

    • 在用户专属目录下创建一个固定名称的 Java 文件(假设使用的是 GLOBAL_JAVA_CLASS_NAME 作为文件名),例如:/home/user/project/codeTemp/uuid-xxxxxx/Main.java
  6. 将用户代码写入到指定路径的文件中

    • 使用 FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8) 方法将用户传入的代码字符串以 UTF-8 编码格式写入到之前构造好的文件路径中。这个方法还会自动创建文件及其父目录(如果它们还不存在的话)。
  7. 返回创建好的文件对象

    • 最后,返回刚刚创建好的 File 对象,供调用者用于后续的编译或执行操作。

 

 

/*** 1. 把用户的代码保存为文件** 这个方法的主要目的是将用户传入的字符串形式的 Java 源代码保存为一个临时文件,* 并返回该文件对象。为了隔离不同用户的代码,每次都会创建一个独立的目录来存放。** @param code 用户输入的 Java 源代码字符串* @return 返回保存后的 Java 文件对象(File),可用于后续编译或执行*/
public File saveCodeToFile(String code) {// 获取当前运行程序的工作目录路径(例如:/home/user/project)String userDir = System.getProperty("user.dir");// 构建全局代码存储目录的完整路径(userDir + 文件分隔符 + 全局目录名)// 例如:/home/user/project/codeTempString globalCodePathName = userDir + File.separator + GLOBAL_CODE_DIR_NAME;// 判断这个全局代码目录是否存在// 如果不存在,则使用工具类 FileUtil 创建该目录if (!FileUtil.exist(globalCodePathName)) {FileUtil.mkdir(globalCodePathName); // 创建目录}// 生成一个唯一标识符作为当前用户的代码目录名(UUID.randomUUID())// 目的是为了避免多个用户提交代码时发生文件覆盖的问题String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID();// 构造具体的 Java 文件路径(在用户专属目录下创建一个固定名称的 Java 文件)// 例如:/home/user/project/codeTemp/uuid-xxxxxx/Main.javaString userCodePath = userCodeParentPath + File.separator + GLOBAL_JAVA_CLASS_NAME;// 使用 FileUtil 工具类将用户传入的代码字符串写入到指定路径的文件中// 编码方式为 UTF-8,确保中文等字符不会乱码// writeString 方法会自动创建文件及其父目录(如果不存在)File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);// 返回创建好的文件对象,供调用者使用(如用于后续编译、执行操作)return userCodeFile;
}

2. 编译代码,得到 class 文件

✅ 目的:

.java 源文件编译为 .class 字节码文件,供 JVM 执行。

📌 原因:
  • Java 程序不能直接执行 .java 文件,必须先经过编译。
  • 使用标准工具(如 javac 或 Java Compiler API)进行编译。
  • 如果编译失败,需要捕获错误信息并反馈给用户。

实现步骤: 

  1. 构造编译命令

    • 使用 String.format() 方法构建用于编译 Java 源代码文件的命令字符串。这里使用了 javac 命令,并通过 -encoding utf-8 参数指定源文件的字符编码格式为 UTF-8,以确保支持中文等非ASCII字符集。
    • 获取用户代码文件的绝对路径(userCodeFile.getAbsolutePath()),将其拼接到命令字符串中。
  2. 执行编译命令

    • 使用 Runtime.getRuntime().exec(compileCmd) 方法执行上述构造好的编译命令。这会启动一个子进程运行 javac 编译器来编译用户的 Java 源代码文件。
    • 返回的 Process 对象表示正在运行的编译进程,可以通过它获取编译过程中的输入输出流以及控制进程的行为。
  3. 处理编译过程和结果

    • 调用自定义工具类 ProcessUtils.runProcessAndGetMessage(compileProcess, "编译") 来处理编译进程。这个方法通常负责:
      • 读取并收集编译过程中的标准输出和错误输出信息。
      • 等待编译进程结束并获取其退出状态码。
      • 将上述信息封装到一个 ExecuteMessage 对象中返回。
  4. 检查编译是否成功

    • 根据 ExecuteMessage 对象中的退出值(exitValue)判断编译是否成功。如果退出值不为0(即 executeMessage.getExitValue() != 0),则认为编译失败,并抛出一个 RuntimeException 异常,附带消息“编译错误”。
  5. 异常处理

    • 如果在执行编译命令或处理编译过程中发生任何异常(如 IO 异常、编译命令执行失败等),则捕获这些异常并在当前实现中直接抛出一个新的 RuntimeException,将原始异常作为其原因。注意,这里的异常处理策略可以根据实际需要调整,例如可以返回一个包含错误信息的响应对象而不是直接抛出异常。
  6. 返回编译结果

    • 如果编译成功(即退出值为0),则返回封装了编译过程信息的 ExecuteMessage 对象给调用者。该对象包含了编译的标准输出、错误输出及退出状态码,供后续逻辑使用。
/*** 进程执行信息*/
@Data
public class ExecuteMessage {private Integer exitValue;private String message;private String errorMessage;private Long time;private Long memory;
}

 

/*** 2. 编译代码** 此方法用于将用户保存的 Java 源代码文件(.java)进行编译,* 生成对应的字节码文件(.class)。如果编译失败,会记录错误信息。** @param userCodeFile 用户的 Java 源代码文件对象(已保存到磁盘)* @return 返回一个 ExecuteMessage 对象,包含编译过程的标准输出、错误输出和退出码*/
public ExecuteMessage compileFile(File userCodeFile) {// 构造编译命令:javac -encoding utf-8 [源文件路径]// -encoding utf-8 确保支持中文等字符集String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());try {// 使用 Runtime.getRuntime().exec() 执行系统命令 javac 进行编译// 返回一个 Process 对象,表示正在运行的编译进程Process compileProcess = Runtime.getRuntime().exec(compileCmd);// 调用工具类 ProcessUtils.runProcessAndGetMessage() 来运行并监听编译过程// 该方法会://   - 读取标准输出流(System.out)//   - 读取错误输出流(System.err)//   - 获取进程退出码(exit code)// 最终封装成一个 ExecuteMessage 对象返回ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");// 判断编译是否成功:// - 如果 exitValue == 0,说明编译通过// - 如果 exitValue != 0,说明有语法错误或其他问题if (executeMessage.getExitValue() != 0) {// 抛出运行时异常,并提示“编译错误”// 实际项目中可以改为更具体的异常类型或封装错误信息返回throw new RuntimeException("编译错误");}// 如果编译成功,返回编译结果的信息对象return executeMessage;} catch (Exception e) {// 捕获所有异常,包括 IO 异常、执行命令失败、中断等// 可以选择记录日志或返回错误响应对象(如 getErrorResponse(e))// 当前直接抛出运行时异常// 注意:这里可以选择不抛出异常,而是返回 ExecuteMessage 错误对象// 示例:return getErrorResponse(e);throw new RuntimeException(e);}
}

3. 执行代码,得到输出结果

✅ 目的:

在受控环境下运行用户代码,并捕获其输出(包括标准输出和错误输出)。

📌 原因:
  • 用户代码可能包含无限循环、异常抛出、资源占用等风险行为。
  • 需要限制执行时间、内存使用,防止系统崩溃或被攻击。
  • 需要重定向 System.out 和 System.err 来获取输出内容。

实现步骤:

 

  1. 获取用户代码文件的父目录路径

    • 使用 userCodeFile.getParentFile().getAbsolutePath() 获取用户代码文件所在目录的绝对路径。因为编译后的 .class 文件与 .java 文件位于同一目录下,所以这个路径可以用来指定 Java 运行时的类路径(-cp 参数)。
  2. 初始化执行结果列表

    • 创建一个 ArrayList<ExecuteMessage> 类型的列表 executeMessageList 用于存储每次运行的结果信息。每个 ExecuteMessage 对象包含一次执行的标准输出、错误输出以及退出状态码。
  3. 遍历输入参数列表

    • 使用 for (String inputArgs : inputList) 循环遍历传入的 inputList,其中每个元素代表一组测试用例的输入参数。
  4. 构造运行命令

    • 使用 String.format() 方法构建用于运行 Java 程序的命令字符串。该命令包括以下部分:
      • -Xmx256m:设置 JVM 最大堆内存为 256MB,以防止程序占用过多内存。
      • -Dfile.encoding=UTF-8:设置文件编码格式为 UTF-8,确保支持多语言字符集。
      • -cp %s:指定类路径为当前用户的代码目录。
      • Main:要执行的主类名(假设是 Main.class)。
      • %s:本次循环的输入参数,作为 main 方法的参数传入。
  5. 执行命令并启动超时监控

    • 使用 Runtime.getRuntime().exec(runCmd) 执行上述构造好的命令,启动一个子进程来运行 Java 程序。
    • 启动一个新的线程,使用 Thread.sleep(TIME_OUT) 来实现超时控制。如果超过设定的时间限制,则调用 runProcess.destroy() 强制终止该进程,避免长时间占用资源或死循环等情况。
  6. 处理运行过程和结果

    • 调用 ProcessUtils.runProcessAndGetMessage(runProcess, "运行") 方法处理运行进程。此方法通常会读取并收集标准输出和错误输出信息,并等待进程结束获取其退出状态码。
    • 将收集到的信息封装成一个 ExecuteMessage 对象,并将其添加到 executeMessageList 中。
  7. 异常处理

    • 如果在执行命令或处理过程中发生任何异常(例如 IO 异常、命令执行失败等),则捕获这些异常并抛出一个新的 RuntimeException,附带原始异常作为原因。
  8. 返回执行结果列表

    • 当所有输入参数都被处理完毕后,返回包含所有执行结果的 executeMessageList 列表。
/*** 3. 执行文件,获得执行结果列表** 此方法用于执行用户编译后的 Java 字节码文件(.class),并传入多组输入参数,* 每次运行一个测试用例,并收集输出结果。** @param userCodeFile 编译生成的 .class 文件所在目录中的源代码文件(用来获取父路径)* @param inputList    用户提供的多个输入参数列表,代表多个测试用例* @return 返回一个 ExecuteMessage 列表,每个元素对应一次运行的标准输出、错误输出和退出码*/
public List<ExecuteMessage> runFile(File userCodeFile, List<String> inputList) {// 获取用户代码文件所在的父目录绝对路径(即存放 .class 文件的目录)String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();// 创建一个列表,用于存储每次执行的结果信息对象List<ExecuteMessage> executeMessageList = new ArrayList<>();// 遍历输入参数列表,依次对每一组输入执行程序for (String inputArgs : inputList) {// 构造 Java 运行命令:// -Xmx256m: 设置 JVM 最大堆内存为 256MB,防止内存溢出// -Dfile.encoding=UTF-8: 强制使用 UTF-8 编码,避免中文乱码// -cp %s: 指定类路径为当前目录(即 userCodeParentPath)// Main: 要执行的主类名(假设是 Main.class)// %s: 本次循环的输入参数,作为 main 方法的 args 参数传入String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeParentPath, inputArgs);try {// 使用 Runtime.getRuntime().exec() 执行构建好的 java 命令Process runProcess = Runtime.getRuntime().exec(runCmd);// 启动一个守护线程来监控执行时间,实现超时控制new Thread(() -> {try {// 等待预设的超时时间(TIME_OUT,单位毫秒)Thread.sleep(TIME_OUT);// 如果还未执行完成,则强制终止进程System.out.println("超时了,中断");runProcess.destroy();} catch (InterruptedException e) {throw new RuntimeException(e);}}).start();// 使用工具类 ProcessUtils 来运行并监听进程的执行过程// 该方法会读取标准输出流和错误输出流,并返回封装好的 ExecuteMessage 对象ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(runProcess, "运行");// 将单次执行结果添加到结果列表中executeMessageList.add(executeMessage);} catch (Exception e) {// 如果执行过程中出现异常(如 IO 错误、命令执行失败等),// 则抛出运行时异常,并附带原始异常作为原因throw new RuntimeException("执行错误", e);}}// 返回所有测试用例执行后的结果列表return executeMessageList;
}

4. 收集整理输出结果

✅ 目的:

将程序执行的标准输出、错误输出、退出码等信息整合后返回给调用者。

📌 原因:
  • 提供给用户清晰的运行结果反馈。
  • 包括成功输出、异常堆栈、超时提示、内存溢出警告等。
  • 用于后续判断是否通过测试用例。

实现步骤: 

  1. 初始化响应对象

    • 创建一个 ExecuteCodeResponse 对象 executeCodeResponse,用于封装最终返回给用户的响应信息。
    • 初始化一个 List<String> 类型的列表 outputList,用于存储所有成功执行的结果输出。
  2. 初始化最大执行时间

    • 定义一个 long 类型变量 maxTime 并初始化为 0,用于记录所有测试用例中最大的执行时间(毫秒),以便于后续判断是否超时或评估性能。
  3. 遍历执行消息列表

    • 使用 for (ExecuteMessage executeMessage : executeMessageList) 遍历传入的 executeMessageList,每个 executeMessage 包含一次执行的标准输出、错误输出、退出码及执行时间等信息。
  4. 检查并处理错误信息

    • 获取当前执行结果中的错误信息 errorMessage
    • 如果 errorMessage 不为空且非空白字符串(使用 StrUtil.isNotBlank(errorMessage) 检查),则表示该次执行出现了错误。
      • 将错误信息设置到 executeCodeResponse 的 message 字段中。
      • 设置 executeCodeResponse 的状态码为 3(代表代码在运行过程中出现错误)。
      • 立即跳出循环,停止进一步处理其他执行结果,因为一旦有错误发生,通常意味着整个过程失败。
  5. 收集标准输出和更新最大执行时间

    • 如果没有错误信息,则将当前执行结果的标准输出内容添加到 outputList 中。
    • 获取当前执行结果的执行时间 time(单位可能是毫秒),如果该值不为空,则更新 maxTime 为当前已知的最大值。
  6. 检查全部成功执行情况

    • 在循环结束后,比较 outputList.size() 和 executeMessageList.size()。如果两者相等,说明所有输入参数对应的执行均成功完成,此时设置 executeCodeResponse 的状态码为 1(表示全部运行成功,无任何错误)。
  7. 设置输出结果列表

    • 将收集到的所有标准输出内容 outputList 设置到 executeCodeResponse 的 outputList 字段中。
  8. 构建判题信息

    • 创建一个新的 JudgeInfo 对象 judgeInfo,用于封装判题所需的信息(如执行时间和内存占用)。
    • 将之前计算得到的最大执行时间 maxTime 设置到 judgeInfo 的 time 字段中。
    • (注:关于内存占用的获取较为复杂,通常需要借助 JVM 工具或者操作系统命令行工具,在 Java 进程中精确获取用户代码使用的内存非常困难,因此此处不做具体实现)
  9. 设置判题信息

    • 将 judgeInfo 设置到 executeCodeResponse 的 judgeInfo 字段中。
  10. 返回响应对象

    • 返回填充完毕的 executeCodeResponse 对象,供调用者使用。
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExecuteCodeResponse {private List<String> outputList;/*** 接口信息*/private String message;/*** 执行状态*/private Integer status;/*** 判题信息*/private JudgeInfo judgeInfo;
}

 

/*** 4. 获取输出结果** 此方法根据代码执行过程中的多个执行结果(ExecuteMessage 列表),* 构建最终返回给用户的响应对象 ExecuteCodeResponse。* 主要功能包括:* - 提取所有运行成功的结果* - 检查是否有错误信息,并设置对应状态码* - 收集最大执行时间等判题信息** @param executeMessageList 执行过程中收集到的多个 ExecuteMessage 对象列表* @return 返回封装好的 ExecuteCodeResponse 对象,包含输出、状态、判题信息等*/
public ExecuteCodeResponse getOutputResponse(List<ExecuteMessage> executeMessageList) {// 创建一个最终要返回的响应对象ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();// 用于存储所有成功的标准输出内容List<String> outputList = new ArrayList<>();// 用于记录所有测试用例中最大的执行时间(毫秒),便于判断是否超时long maxTime = 0;// 遍历每个 ExecuteMessage(即每次输入参数对应的执行结果)for (ExecuteMessage executeMessage : executeMessageList) {// 获取当前执行结果的错误信息(如果有)String errorMessage = executeMessage.getErrorMessage();// 如果错误信息不为空或非空白字符串,说明该次执行出错if (StrUtil.isNotBlank(errorMessage)) {// 将错误信息设置到响应对象中executeCodeResponse.setMessage(errorMessage);// 设置状态码为 3:代表代码在运行过程中出现错误(如异常、编译未通过等)executeCodeResponse.setStatus(3);// 出现错误后直接跳出循环,不再处理后续结果break;}// 如果没有错误,则将本次运行的标准输出加入结果列表outputList.add(executeMessage.getMessage());// 获取本次运行的时间(单位可能是毫秒)Long time = executeMessage.getTime();// 如果时间有效,则更新最大时间if (time != null) {maxTime = Math.max(maxTime, time);}}// 如果所有测试用例都成功运行完毕(输出数量等于执行次数)if (outputList.size() == executeMessageList.size()) {// 设置状态码为 1:表示全部运行成功,无任何错误executeCodeResponse.setStatus(1);}// 将收集到的标准输出结果设置到响应对象中executeCodeResponse.setOutputList(outputList);// 创建并填充 JudgeInfo 判题信息对象(如时间和内存)JudgeInfo judgeInfo = new JudgeInfo();judgeInfo.setTime(maxTime); // 设置最大运行时间// 内存占用获取较为复杂,通常需要借助 JVM 工具或者操作系统命令行工具(如 ps、top 等)// 在 Java 进程中精确获取用户代码使用的内存非常困难,因此此处不做具体实现
//    judgeInfo.setMemory();// 将判题信息设置到响应对象中executeCodeResponse.setJudgeInfo(judgeInfo);// 返回完整的响应对象return executeCodeResponse;
}

5. 文件清理,释放空间

✅ 目的:

删除临时生成的 .java.class 文件及目录,防止磁盘爆满。

📌 原因:
  • 沙箱频繁运行会导致大量临时文件堆积。
  • 不及时清理会影响服务器性能与稳定性。
  • 使用完即删是良好资源管理习惯。

实现步骤:

 

  1. 检查用户代码文件的父目录是否存在

    • 使用 if (userCodeFile.getParentFile() != null) 判断用户代码文件是否有父目录。如果该文件有父目录,说明它位于某个目录中,需要删除该目录及其所有内容;如果没有父目录,可能是根目录下的文件或其他特殊情况,直接认为删除成功。
  2. 获取用户代码文件所在父目录的绝对路径

    • 使用 userCodeFile.getParentFile().getAbsolutePath() 获取用户代码文件所在父目录的绝对路径。这个路径指向包含 .java 文件及其编译后生成的 .class 文件的目录。
  3. 递归删除整个目录及其内容

    • 调用 FileUtil.del(userCodeParentPath) 方法递归删除指定路径下的整个目录及其所有子目录和文件。FileUtil.del() 是一个工具方法,通常用于简化文件删除操作,并且支持递归删除。
    • 返回值 del 表示删除操作是否成功执行。
  4. 打印删除操作的结果

    • 使用 System.out.println() 打印删除操作的结果,便于调试或记录日志。如果删除成功,输出“删除成功”;如果删除失败,输出“删除失败”。
  5. 返回删除操作的结果

    • 根据删除操作的结果 del,返回相应的布尔值。如果删除成功,返回 true;如果删除失败,返回 false
  6. 处理无父目录的情况

    • 如果用户代码文件没有父目录(即 userCodeFile.getParentFile() 返回 null),则默认认为删除成功,返回 true。这种情况较为罕见,但为了确保方法的健壮性,仍然需要处理。
/*** 5. 删除文件** 此方法用于删除用户代码文件及其所在的整个目录(包括编译生成的 .class 文件等),* 以释放磁盘空间并保持环境清洁。** @param userCodeFile 用户代码文件对象(通常为 .java 文件)* @return 如果成功删除则返回 true,否则返回 false*/
public boolean deleteFile(File userCodeFile) {// 检查用户代码文件的父目录是否存在if (userCodeFile.getParentFile() != null) {// 获取用户代码文件所在父目录的绝对路径String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();// 使用 FileUtil.del() 方法递归删除整个目录及其内容// 返回值表示操作是否成功boolean del = FileUtil.del(userCodeParentPath);// 打印删除操作的结果(仅用于调试或日志记录)System.out.println("删除" + (del ? "成功" : "失败"));// 返回删除操作的结果return del;}// 如果用户代码文件没有父目录,则默认认为删除成功(这种情况很少见)return true;
}

6. 错误处理,提升程序健壮性

✅ 目的:

对所有可能出现的异常进行捕获和处理,避免沙箱自身崩溃。

📌 原因:
  • 用户代码可能存在语法错误、死循环、异常抛出等问题。
  • 外部命令执行(如 Runtime.exec())可能失败。
  • IO 操作、路径访问、权限控制等都可能引发异常。
  • 需要统一的日志记录、错误码返回机制。

实现步骤:

 

  1. 创建响应对象:初始化一个新的ExecuteCodeResponse对象。
  2. 设置输出列表:将输出列表设置为空列表,表示没有正常输出。
  3. 设置错误消息:从传入的异常对象中获取错误消息,并将其设置到响应对象中。
  4. 设置状态码:将状态码设置为2,表示发生了代码沙箱错误。
  5. 初始化判题信息:初始化一个JudgeInfo对象并设置到响应对象中。
  6. 返回响应对象:返回构建好的ExecuteCodeResponse对象。
/*** 获取错误响应的方法** @param e 异常对象,包含错误信息* @return 包含错误信息的ExecuteCodeResponse对象*/
private ExecuteCodeResponse getErrorResponse(Throwable e) {// 创建一个新的ExecuteCodeResponse对象ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();// 设置输出列表为空列表executeCodeResponse.setOutputList(new ArrayList<>());// 设置错误消息为异常对象的消息executeCodeResponse.setMessage(e.getMessage());// 设置状态码为2,表示代码沙箱错误executeCodeResponse.setStatus(2);// 初始化并设置JudgeInfo对象executeCodeResponse.setJudgeInfo(new JudgeInfo());// 返回构建好的ExecuteCodeResponse对象return executeCodeResponse;
}

至此,第一期的内容结束,不过我们可以发现一个问题,如果想要上线的话,安全么? 用户提交恶意代码,怎么办?

那针对这种情况,我们可以来提高程序安全性

这部分我放到下一期来讲!

相关文章:

  • 线段树:数据结构中的超级英雄
  • 检查当前 Docker 使用的 默认运行时(default runtime)方法
  • LeetCode-双指针-盛最多水的容器
  • 部署Superset BI(四)连接sql server数据库
  • MSF 生成不同的木马 msfvenom 框架命令
  • uniapp跨平台开发HarmonyOS NEXT应用初体验
  • 纯净IP,跨境账号稳定的底层逻辑
  • git做commit信息时的校验
  • hz2新建Keyword页面
  • Spring 必会之微服务篇(1)
  • python实现点餐系统
  • gitlab相关面试题及答案
  • 学习笔记:黑马程序员JavaWeb开发教程(2025.3.31)
  • 用Python监控金价并实现自动提醒!附完整源码
  • 软件测试——用例篇(2)
  • OpenHarmony 以太网卡热插拔事件接口无效
  • 【RLHF】 Reward Model 和 Critic Model 在 RLHF 中的作用
  • 云原生架构下的微服务通信机制演进与实践
  • 31【干货】Arcgis属性表常用查询表达式实战大全
  • 1 bit AI 框架:Part 1.1,CPU 上的快速无损 BitNet b1.58 推理
  • 总粉丝破亿!当网络大V遇见硬核科技,互联网时代如何书写上海故事?
  • 可量产9MWh超大容量储能系统亮相慕尼黑,宁德时代:大储技术迈入新时代
  • 长期对组织隐瞒真实年龄,广元市城发集团原董事韩治成被双开
  • 2025柯桥时尚周启幕:国际纺都越来越时尚
  • 代理销售保险存在误导行为,农业银行重庆市分行相关负责人被罚款0.1万元
  • 融创中国:今年前4个月销售额约112亿元