【JavaEE Spring 项目】在线 OJ 系统

在线OJ系统

  • 1. 需求
  • 2. 最终页面展示
  • 3. 需求分析
  • 4. 创建 Spring 项目
  • 5. 前后端交互接口约定
  • 6. 后端功能实现
    • 6.1 编译运行模块
      • 6.1.1 进程和线程的相关知识
      • 6.1.2 Java 中的多进程编程
      • 6.1.3 进程间通信 -- 文件
      • 6.1.4 Java中的 IO 知识
      • 6.1.5 封装创建进程执行命令工具类
      • 6.1.6 实现编译运行的核心方法
    • 6.2 数据库管理模块
      • 6.2.1 题目管理
        • 数据库设计
          • 题目建表字段分析
          • 题目数据创建sql
        • OJMapper 编写
          • 增删改查接口的测试
    • 6.3 前后端交互模块
      • 6.3.1 OJ 题目数据交互
      • 6.3.2 代码提交编译运行模块
    • 6.4 统一功能处理
      • 6.4.1 统一结果返回
      • 6.4.2 统一异常处理
  • 7. 前端功能实现
    • 使用网页模板
      • 制作题目列表页
        • 通过 ajax 获取后端数据
      • 制作题目详情页
        • 从服务器上获取题目详情
        • 实现提交代码
      • 引入代码编辑器组件
        • 引入 ace.js
        • 初始化编辑器
        • 修改 makeProblemDetail 方法
        • 修改提交代码
  • 8. 拓展功能
    • 加入安全性控制
  • 9. 将项目部署到 Linux 服务器上面
    • 9.1 在Linux上执行建库建表操作
    • 9.2 多平台⽂件配置
    • 9.3 使用 Maven 打包成 jar
    • 9.4 上传Jar包到服务器, 并运⾏
  • 10. 总结

1. 需求

  • 在线的网页版的编程平台
  • 打开一个网站, 上面可以看到很多题目
  • 在线做题, 在线提交, 立即就能看到运行结果, 是否通过

2. 最终页面展示

题目列表信息页

在这里插入图片描述
做题详情页

在这里插入图片描述

3. 需求分析

一个在线OJ的核心功能(参考 leetCode):

  1. 需要能够管理题目(保存很多的题目信息: 如标题, 题目难易程度, 题目描述, 测试用例, 编写代码模板等等)
  2. 题目列表页: 能够列举所有题目的信息
  3. 题目详情页: 能够展示某个题目的详细信息, 代码编辑框, 运行结果等.
  4. 提交并运行题目: 能够提交编辑好的代码, 并知道是否编译运行通过, 运行结果是否正确, 通过了几个测试用例.

4. 创建 Spring 项目

在这里插入图片描述
在这里插入图片描述

application.yml 配置文件

spring:datasource:url: jdbc:mysql://127.0.0.1:3306/oj_spring_database?characterEncoding=utf8&useSSL=falseusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:configuration:map-underscore-to-camel-case: true #配置驼峰自动转换log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql语句mapper-locations: classpath:mapper/**Mapper.xml
# 设置日志文件的文件名
logging:file:name: logger/spring-blog.log

5. 前后端交互接口约定

  1. 获取题目列表
请求: 
get /oj/getAllProblem HTTP/1.1响应:
{code: 200,errMessage: "",data: {{id: 1,title: 两数相加,level: 简单,},{id: 2,title: 合并链表,level: 简单,},...}
}
  1. 获取题目详细信息页
请求: 
get /oj/getProblemDetail?id=1 HTTP/1.1响应:
{code: 200,errMessage: "",data: {id: 1,title: '两数相加',level: '简单',description: "给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。',templateCode: '/*** Definition for singly-linked list.* public class ListNode {*     int val;*     ListNode next;*     ListNode() {}*     ListNode(int val) { this.val = val; }*     ListNode(int val, ListNode next) { this.val = val; this.next = next; }* }*/
class Solution {public ListNode addTwoNumbers(ListNode l1, ListNode l2) {}
}",testCode: null...}
}
  1. 代码提交编译

请求: 
post /compile 
{id: 1,userCode: ''...."
}响应:
{code: 200,errMessage: "",data: {error: 0 (0 表示编译和运行都正确,0表示错误),errorMessage: "",stdout: "testcase1 OK \n testcase2 OK"}
}

后续功能待开发 …

6. 后端功能实现

6.1 编译运行模块

6.1.1 进程和线程的相关知识

  • 在Java中, 编译 .java 文件的指令是 javac, .java 文件经过编译之后生成 .class文件, 经过 java 命令就可以 .class 文件了;
  • 指令其实也是一个程序, 一个程序运行起来后就是一个进程;

进程 和 线程

  • 进程可以称为是 “任务”, 操作系统想要执行一个具体的 “动作”, 就需要创建出一个对应的进程
  • 一个程序没有运行的时候, 仅仅是一个 “可执行文件”, 一个程序跑起来了, 就变成一个进程了
  • 为了实现 “并发编程” (同时执行多个任务), 就引入了 “多进程编程”, 把一个很大的任务, 拆分成若干个很小的任务, 创建多个进程, 每个进程分别负责其中的一部分任务
  • 但是也带来一个问题: 创建/销毁进程, 比较重量(比较低效)
  • 所以就引入了线程, 每个线程都是一个独立的执行流, 一个进程包含了一个或者多个线程, 创建线程/销毁线程比创建进程/销毁进程更高效
  • 因此, Java 圈中, 大部分的并发编程都是通过多线程的方式来实现的
  • 线程相比于进程的优势就是轻量, 而进程相比于线程的优势: 进程的独立性
  • 操作系统上, 同一个时刻运行着很多个进程, 如果某个进程挂了, 不会影响到其他进程. (每个进程都有各自的地址空间)
  • 相比之下, 由于多个线程之间, 共用着同一个进程的地址空间, 某个线程挂了, 就很可能会把整个进程带走.

: 对于在线OJ的编译和运行模块的功能来说, 是使用多线程编程呢? 还是使用多进程编程呢?

: 采用多进程编程; 因为我们不知道用户的代码是怎么样的, 用户的代码中可能会存在错误, 如果是创建一个线程来编译和运行用户的代码, 其中如果出现报错, 该线程就会导致整个服务进程挂掉; 因此就需要一个新的进程来编译运行用户的代码.

6.1.2 Java 中的多进程编程

Java 的 RunTime 类

public class TestRuntime {public static void main(String[] args) throws IOException {// exec 的参数就是相当于直接终端中输入的指令, process 中存储着该指令执行的结果Process process = Runtime.getRuntime().exec("javac");// 标准输入, 标准输出, 标准错误 -- 可以从这些流中获取命令执行相关的结果InputStream inputStream = process.getInputStream();OutputStream outputStream = process.getOutputStream();InputStream errorStream = process.getErrorStream();}
}

6.1.3 进程间通信 – 文件

  • 由于各个进程直接是独自拥有一个进程地址空间的, 是相对独立的, 而独立带来的问题就是不如线程之间通信容易;
  • java命令需要知道 javac 命令编译 .java 文件后的结果, 而这两个命令是两个独立的进程, 为了这两个独立的进程之间进行通信, 则就需要"中间商", 也就是文件.
  • javac 将编译后的结果写到一个公共的文件中, java 命令在从公共的文件中读取结果;

6.1.4 Java中的 IO 知识

  • 在 Java 中, 操作文件(读写) 通过 IO 流相关的类来实现的
  • Java 标准库中, 对于 IO的操作提供了很多现成的类, 这些类放在 java.io 这个包里
  • 标准库中的这些类, 大概可以分成两大类
    • 一大类是操作字节的(以字节为单位进行读写的)
    • 一大类是操作字符的(以字符为单位进行读写的)
  • 字节是 8 个 bit 位 (表示存储空间的基本单位)
  • 字符表示一个"文字符号", 一个字符可能是由多个字节构成的.
  • 因此就需要根据文件类型来决定按照字节操作还是字符操作
    • 有的文件是二进制文件(这种就需要按照字节来操作)
    • 有的文件是文本文件(这种就需要按照字符来操作)
  • 怎么去区分一个文件是文本还是二进制呢?
    • 简单的方法, 就是使用记事本打开, 看看是不是乱码, 如果是乱码, 就是二进制文件; 如果不是乱码, 就是文本文件
    • 这是因为记事本是默认按照文本的方式来打开解析文件的
  • 针对字节为单位进行读写的类, 统称为 “字节流”
    • 字节流: InputStream, FileInputStream, OutputStream, FileOutputSteam
  • 针对字符为单位进行读写的类, 统称为 “字符流”
    • 字符流: Reader, FileReader, Writer, FileWriter

封装文件相关读写操作为一个类

package com.example.ojspring.util;import java.io.*;/*** Created with IntelliJ IDEA.* Description:封装文件读写相关的方法** @author: zxj* @date: 2024-02-23* @time: 18:35:26*/
public class FileUtils {/*** @description: 从指定的文件目录中读取文件内容到 String* @param: [fromFilePath 需要读取的文件目录]* @return: 返回一个字符串, 记录文件里面的内容**/public static String readFile(String fromFilePath) {try (Reader reader = new FileReader(fromFilePath)) {StringBuilder tmp = new StringBuilder();while (true) {int ch = reader.read();if (ch == -1) break;tmp.append((char)ch);}return tmp.toString();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {throw new RuntimeException(e);}return null;}/*** @description: 将content写入toFilePath* @param: [toFilePath 写入文件所在的目录, content 需要写的内容]**/public static void writeFile(String toFilePath,String content) {try (Writer writer = new FileWriter(toFilePath)){writer.write(content);} catch (IOException e) {e.printStackTrace();}}
}

6.1.5 封装创建进程执行命令工具类

package com.example.ojspring.util;import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;/*** Created with IntelliJ IDEA.* Description:** @author: zxj* @date: 2024-02-23* @time: 20:20:16*/
public class CommandUtils {/*** @description: 执行 cmd 命令, 将信息存储到对应的文件中* @param: [cmd 执行指令, stdoutFilePath 存储标准输出内容, stderrFilePath 存储标准错误的内容]* @return:**/public static void run(String cmd,String stdoutFilePath,String stderrFilePath) {try {Process process = Runtime.getRuntime().exec(cmd);if (stdoutFilePath != null) {InputStream inputStream = process.getInputStream();try (OutputStream outputStream = new FileOutputStream(stdoutFilePath)){while (true) {int ch = inputStream.read();if (ch == -1) break;outputStream.write(ch);}} finally {inputStream.close();}}if (stderrFilePath != null) {InputStream inputStream = process.getErrorStream();try (OutputStream outputStream = new FileOutputStream(stderrFilePath)){while (true) {int ch = inputStream.read();if (ch == -1) break;outputStream.write(ch);}} finally {inputStream.close();}}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {run("javac","./stdout.txt","./stderr.txt");}
}

6.1.6 实现编译运行的核心方法

  • Java 中编译要求文件名和类名相同, 参考 leetCode中的OJ题目, 我们可以规定类名统一为Solution
  • 创建 CompileTask 类, 里面提供核心方法 compileAndRun 方法, 创建Question 类作为 CompileTask 的输入类, Answer 类作为返回结果的实体类

【Question 类】

/*** Created with IntelliJ IDEA.* Description:向编译运行提供的实体类** @author: zxj* @date: 2024-02-23* @time: 20:44:57*/
@Data
public class Question {// 需要编译运行的代码private String code;// ...
}

【Answer 类】


/*** Created with IntelliJ IDEA.* Description:** @author: zxj* @date: 2024-02-23* @time: 20:44:50*/
@Data
public class Answer {// 错误码: 0 表示编译运行都成功, 1 表示编译失败, 2 表示运行失败 ...private Integer errorCode;// 错误信息private String errorMessage;// 记录成功时的标准输出信息private String stdoutMessage;
}

【CompileTask】


/*** Created with IntelliJ IDEA.* Description:** @author: zxj* @date: 2024-02-23* @time: 20:31:52*/
@Data
@Slf4j
public class CompileTask {// 约定相关的文件名称// 工作目录private String wordDir;// 类名private String className;// .java 源文件private String codeFilename;// 标准输出 -- 记录的是测试用例的输出结果private String stdoutFilePath;// 标准错误, 运行是抛异常的记录private String stderrFilePath;// 编译时出现的错误private String compileErrFillPath;public CompileTask() {// 使用 UUID, 防止同时多个进程同时编译运行的时候, 出现进程安全的问题, 也就是为每一次执行编译运行时的进程提供自己的工作目录wordDir = "./tmp/" + UUID.randomUUID().toString() + "/";className = "Solution";codeFilename = wordDir + className + ".java";stdoutFilePath = wordDir + "stdout.txt";stderrFilePath = wordDir + "stderr.txt";compileErrFillPath = wordDir + "compile_err.txt";}/*** @description: 核心方法**/public Answer compileAndRun(Question question) {// 0. 判断工作目录是否存在, 不存在就创建File file = new File(wordDir);if (!file.exists()) {// 不存在, 创建file.mkdirs();}Answer answer = new Answer();// 1. 编译String code = question.getCode();// 1.1 将 code 写入 .java 文件中FileUtils.writeFile(codeFilename, code);// 1.2 构造编译指令 -d 选项表示将生成的.class文件放在哪一个目录下String compileCmd = String.format("javac -encoding utf8 %s -d %s",codeFilename, wordDir);log.info("编译命令: {}", compileCmd);// 1.3 创建新进程执行javac编译命令CommandUtils.run(compileCmd, null, compileErrFillPath);// 1.4 判断编译是否出现错误, 即 判断 compileErrFillPath 所对应的文件中是否有内容String compileErrMessage = FileUtils.readFile(compileErrFillPath);if (StringUtils.hasLength(compileErrMessage)) {answer.setErrorCode(1);answer.setErrorMessage(compileErrMessage);return answer;}// 走到这里说明编译成功// 2. 运行// 2.1. 构造运行指令,     -classpath <目录和 zip/jar 文件的类搜索路径>String runCmd = String.format("java -classpath %s %s",wordDir, className);log.info("运行指令: ", runCmd);// 2.2. 创建新进程执行 java 运行命令CommandUtils.run(runCmd, stdoutFilePath, stderrFilePath);// 2.4 判断运行是否出现错误, 即 判断 stderrFilePath 所对应的文件中是否有内容String stderrMessage = FileUtils.readFile(stderrFilePath);if (StringUtils.hasLength(stderrMessage)) {answer.setErrorCode(2);answer.setErrorMessage(stderrMessage);return answer;}// 走到这里, 说明编译和运行都正确// 3. 返回结果answer.setErrorCode(0);answer.setStdoutMessage(FileUtils.readFile(stdoutFilePath));return answer;}}

6.2 数据库管理模块

6.2.1 题目管理

数据库设计
题目建表字段分析

在这里插入图片描述

  • 题目标题
  • 题目难度等级
  • 题目描述
  • 代码模板
  • 测试用例
题目数据创建sql

建表 sql

create database if not exists oj_spring_database charset utf8mb4;use oj_spring_database;SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for oj_table
-- ----------------------------
DROP TABLE IF EXISTS `oj_table`;
CREATE TABLE `oj_table`
(`id`           int(11)       NOT NULL AUTO_INCREMENT,`title`        varchar(64)   not null,`level`        varchar(32)   not null,`description`  varchar(4096) not null,`templateCode` varchar(4096) not null,`testCode`     varchar(4096) not null,`delete_flag`  tinyint(4) DEFAULT '0',`create_time`  datetime   DEFAULT CURRENT_TIMESTAMP,`update_time`  datetime   DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (`id`)
) ENGINE = InnoDB COMMENT ='题目表';

题目对应的 Java 对象

package com.example.ojspring.model.info;import lombok.Data;
import org.springframework.stereotype.Component;import java.util.Date;/*** Created with IntelliJ IDEA.* Description:题目对应的信息实体类** @author: zxj* @date: 2024-02-20* @time: 20:43:56*/
@Data
@Component
public class OJInfo {private Integer id;private String title;private String level;private String description;private String templateCode;private String testCode;private Integer deleteFlag;private Date createTime;private Date updateTime;
}
OJMapper 编写

题目相关的增删改查操作 OJMapper

package com.example.ojspring.mapper;import com.example.ojspring.model.info.OJInfo;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;import java.util.List;/*** Created with IntelliJ IDEA.* Description:实现增删改查操作** @author: zxj* @date: 2024-02-20* @time: 20:48:17*/
@Mapper
public interface OJMapper {/*** @description: 查询所有的题目**/@Select("select id,title,level from oj_table where delete_flag = 0")List<OJInfo> selectAllOJ();/*** @description: 依据 ID 查询题目**/@Select("select id,title,level,description,template_code,test_code from oj_table where delete_flag = 0 and id = #{id}")OJInfo selectOJById(Integer id);/*** @description: 插入题目信息**/@Insert("insert into oj_table (title, level, description, template_code, test_code) values (#{title},#{level},#{description},#{templateCode},#{testCode})")Integer insert(OJInfo ojInfo);/*** @description: 逻辑删除题目**/@Update("update oj_table set delete_flag = 1 where id = #{id}")Integer delete(Integer id);
}
增删改查接口的测试
  • 增添题目接口
@Testvoid insert() {OJInfo ojInfo = new OJInfo();ojInfo.setTitle("两数之和");ojInfo.setLevel("简单");ojInfo.setDescription("给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。\n" +"\n" +"你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。\n" +"\n" +"你可以按任意顺序返回答案。\n" +"\n" +" \n" +"\n" +"示例 1:\n" +"\n" +"输入:nums = [2,7,11,15], target = 9\n" +"输出:[0,1]\n" +"解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。\n" +"示例 2:\n" +"\n" +"输入:nums = [3,2,4], target = 6\n" +"输出:[1,2]\n" +"示例 3:\n" +"\n" +"输入:nums = [3,3], target = 6\n" +"输出:[0,1]\n" +" \n" +"\n" +"提示:\n" +"\n" +"2 <= nums.length <= 104\n" +"-109 <= nums[i] <= 109\n" +"-109 <= target <= 109\n" +"只会存在一个有效答案\n" +" \n" +"\n" +"进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗?");ojInfo.setTemplateCode("class Solution {\n" +"    public int[] twoSum(int[] nums, int target) {\n" +"\n" +"    }\n" +"}");ojInfo.setTestCode("public static void main(String[] args) {\n" +"        Solution solution = new Solution();\n" +"        // case1\n" +"        int[] nums1 = {2,7,11,15};\n" +"        int target1 = 9;\n" +"        int[] result1 = solution.twoSum(nums1,target1);\n" +"        if (result1 != null && result1.length == 2 && result1[0] == 0 && result1[1] == 1) {\n" +"            System.out.println(\"testcase1 ok\");\n" +"        } else {\n" +"            System.out.println(\"testcase1 ok fail\");\n" +"        }\n" +"\n" +"\n" +"        // case2\n" +"        int[] nums2 = {3,2,4};\n" +"        int target2 = 6;\n" +"        int[] result2 = solution.twoSum(nums1,target1);\n" +"        if (result2 != null && result2.length == 2 && result2[0] == 1 && result2[1] == 2) {\n" +"            System.out.println(\"testcase2 ok\");\n" +"        } else {\n" +"            System.out.println(\"testcase2 ok fail\");\n" +"        }\n" +"\n" +"\n" +"        // case3\n" +"        int[] nums3 = {3,3};\n" +"        int target3 = 6;\n" +"        int[] result3 = solution.twoSum(nums1,target1);\n" +"        if (result3 != null && result3.length == 2 && result3[0] == 0 && result3[1] == 1) {\n" +"            System.out.println(\"testcase3 ok\");\n" +"        } else {\n" +"            System.out.println(\"testcase3 ok fail\");\n" +"        }\n" +"    }");ojMapper.insert(ojInfo);}

测试用例的解决方法:

  • 题目标题, 题目难度, 题目描述, 代码模板都可以在 力扣上获取, 但是测试用例无法拿到;
  • 直接手搓一两个测试用例 如下
public static void main(String[] args) {Solution solution = new Solution();// case1int[] nums1 = {2,7,11,15};int target1 = 9;int[] result1 = solution.twoSum(nums1,target1);if (result1 != null && result1.length == 2 && result1[0] == 0 && result1[1] == 1) {System.out.println("testcase1 ok");} else {System.out.println("testcase1 ok fail");}// case2int[] nums2 = {3,2,4};int target2 = 6;int[] result2 = solution.twoSum(nums1,target1);if (result2 != null && result2.length == 2 && result2[0] == 1 && result2[1] == 2) {System.out.println("testcase2 ok");} else {System.out.println("testcase2 ok fail");}// case3int[] nums3 = {3,3};int target3 = 6;int[] result3 = solution.twoSum(nums1,target1);if (result3 != null && result3.length == 2 && result3[0] == 0 && result3[1] == 1) {System.out.println("testcase3 ok");} else {System.out.println("testcase3 ok fail");}}
  • 查询
    @Testvoid selectAllOJ() {System.out.println(ojMapper.selectAllOJ());}@Testvoid selectOJBy() {System.out.println(ojMapper.selectOJById(1));}

6.3 前后端交互模块

6.3.1 OJ 题目数据交互

OJController 类

package cn.edu.zxj.ojspring.controller;import cn.edu.zxj.ojspring.model.Result;
import cn.edu.zxj.ojspring.model.info.OJInfo;
import cn.edu.zxj.ojspring.service.OJService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;/*** Created with IntelliJ IDEA.* Description:** @author: zxj* @date: 2024-02-21* @time: 17:02:26*/
@RestController
@RequestMapping("/oj")
@Slf4j
public class OJController {@Autowiredprivate OJService ojService;@RequestMapping("/getProblem")public List<OJInfo> getProblem() {log.info("接收到获取所有题目信息请求...");return ojService.getProblem();}@RequestMapping("/getProblemDetail")public Result getProblemDetail(Integer id) {log.info("接收到获取题目{} 详细信息请求...", id);// 参数校验if (id == null || id < 1) {return Result.fail("参数传入错误~");}OJInfo ojInfo = ojService.getProblemDetail(id);if (ojInfo == null) {return Result.fail("内部出现错误, 请联系管理员~");}return Result.success(ojInfo);}}

OJService 类

package cn.edu.zxj.ojspring.service;import cn.edu.zxj.ojspring.mapper.OJMapper;
import cn.edu.zxj.ojspring.model.info.OJInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;/*** Created with IntelliJ IDEA.* Description:** @author: zxj* @date: 2024-02-21* @time: 17:04:17*/
@Service
@Slf4j
public class OJService {@Autowiredprivate OJMapper ojMapper;public List<OJInfo> getProblem() {try {return ojMapper.selectAllOJ();} catch (Exception e) {log.error("数据库查询题目信息出错, e: {}", e);}return null;}public OJInfo getProblemDetail(Integer id) {try {OJInfo ojInfo = ojMapper.selectOJBy(id);ojInfo.setTestCode("");return ojInfo;} catch (Exception e) {log.error("数据库查询题目信息出错, e: {}", e);}return null;}
}

6.3.2 代码提交编译运行模块

CompileRequest 类

package cn.edu.zxj.ojspring.model.compile;import lombok.Data;/*** Created with IntelliJ IDEA.* Description:** @author: zxj* @date: 2024-02-21* @time: 18:10:09*/
@Data
public class CompileRequest {private Integer id;private String code;
}
package cn.edu.zxj.ojspring.model.compile;import lombok.Data;/*** Created with IntelliJ IDEA.* Description:** @author: zxj* @date: 2024-02-21* @time: 18:10:26*/
@Data
public class CompileResponse {// 约定 error 为 0 表示编译运行 ok, error 为 1 表示编译出错, error 为 2 表示运行异常(用户提交的代码异常了), 3 表示其他错误public Integer error;// 错误信息public String reason;// 测试用例通过情况public String stdout;
}

CompileController 类

package cn.edu.zxj.ojspring.controller;import cn.edu.zxj.ojspring.model.Result;
import cn.edu.zxj.ojspring.model.compile.CompileRequest;
import cn.edu.zxj.ojspring.model.compile.CompileResponse;
import cn.edu.zxj.ojspring.service.CompileService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** Created with IntelliJ IDEA.* Description:编译管理** @author: zxj* @date: 2024-02-21* @time: 17:46:41*/
@RestController
@Slf4j
public class CompileController {@Autowiredprivate CompileService compileService;@RequestMapping("/compile")public Result compile(@RequestBody CompileRequest compileRequest) {log.info("接收到用户提交代码的请求, compileRequest: {}", compileRequest);CompileResponse compileResponse = compileService.compileAndRun(compileRequest);if (compileResponse == null) {return Result.fail("内部出现错误, 请联系管理员~");}return Result.success(compileResponse);}
}

CompileService 类

package cn.edu.zxj.ojspring.service;import cn.edu.zxj.ojspring.controller.CompileController;
import cn.edu.zxj.ojspring.mapper.OJMapper;
import cn.edu.zxj.ojspring.model.compile.*;
import cn.edu.zxj.ojspring.model.info.OJInfo;
import cn.edu.zxj.ojspring.util.FileUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;/*** Created with IntelliJ IDEA.* Description:** @author: zxj* @date: 2024-02-21* @time: 18:04:35*/
@Service
@Slf4j
public class CompileService {@Autowiredprivate OJMapper ojMapper;public CompileResponse compileAndRun(CompileRequest compileRequest) {// 1. 查询数据, 获取测试用例的代码OJInfo ojInfo = ojMapper.selectOJBy(compileRequest.getId());if (ojInfo == null) {log.warn("查询数据库无结果, 题目 id: {}", compileRequest.getId());return null;}// 测试用例的代码 -- 里面包含 main 方法 形式如下/*public static void main(String[] args) {Solution solution = new Solution();// testcase1if (solution.addDigits(38) == 2) {System.out.println("Test OK");} else {System.out.println("Test failed");}// testcase2if (solution.addDigits(111) == 3) {System.out.println("Test OK");} else {System.out.println("Test failed");}}*/String testCode = ojInfo.getTestCode();log.info("testCode: {}", testCode);// 2. 获取用户的代码// 用户的代码/*class Solution {public double findMedianSortedArrays(int[] nums1, int[] nums2) {}}*/String requestCode = compileRequest.getCode();log.info("requestCode: {}", requestCode);// 3. 合并代码String finalCode = mergeCode(testCode, requestCode);log.info("最终的代码: finalCode: {}", finalCode);// 4. 构造 compile.Task 来实现编译运行逻辑Task task = new Task();Question question = new Question();question.setCode(finalCode);Answer answer = task.compileAndRun(question);if (answer == null) {return null;}// 5. 依据 answer 构造 CompileResponseCompileResponse compileResponse = new CompileResponse();compileResponse.setError(answer.getError());compileResponse.setStdout(answer.getStdoutMessage());compileResponse.setReason(answer.getErrorMessage());return compileResponse;}private static String mergeCode(String testCode, String requestCode) {StringBuilder tmp = new StringBuilder();int pos = requestCode.lastIndexOf('}');if (pos == -1) {return null;}tmp.append(requestCode, 0, pos);tmp.append(testCode);tmp.append("\n}");return tmp.toString();}// public static void main(String[] args) {//     String testCode = "          public static void main(String[] args) {\n" +//             "          \tSolution solution = new Solution();\n" +//             "              // testcase1\n" +//             "              if (solution.addDigits(38) == 2) {\n" +//             "              \tSystem.out.println(\"Test OK\");\n" +//             "              } else {\n" +//             "                  System.out.println(\"Test failed\");\n" +//             "              }\n" +//             "              // testcase2\n" +//             "              if (solution.addDigits(111) == 3) {\n" +//             "              \tSystem.out.println(\"Test OK\");\n" +//             "              } else {\n" +//             "                  System.out.println(\"Test failed\");\n" +//             "              }\n" +//             "          }";//     String code = "          class Solution {\n" +//             "              public double findMedianSortedArrays(int[] nums1, int[] nums2) {\n" +//             "         \n" +//             "              }\n" +//             "          }";//     System.out.println(mergeCode(testCode,code));//     FileUtils.writeContentToFile("./tmp/Solution.java",mergeCode(testCode,code));// }}

处理编译运行的逻辑:

  1. 用户传来CompileRequest实体类, 里面字段有 对应题目的Id, 还要用户编写的代码;
  2. 通过 id 查询数据库中对应的题目信息, 从题目信息中提取对应的测试代码;
  3. 从 CompileRequest 实体类中提取用户代码
  4. 使用 mergeCode 方法, 将测试方法拼接到用户代码最后的 } 之前
  5. 接着将 finalCode 构造成一个 Question 类, 交给 CompileTask 中的compileAndRun方法进行处理得到 answer结果
  6. 利用answer中的字段填充 CompileResponse中的字段进行返回
  7. 这里不把 answer 作为结果返回给前端, 是为了符合一个类只用于一个功能的原则

6.4 统一功能处理

6.4.1 统一结果返回

Result 类

package cn.edu.zxj.ojspring.model;import lombok.Data;/*** Created with IntelliJ IDEA.* Description:** @author: zxj* @date: 2024-02-21* @time: 17:09:50*/
@Data
public class Result {// 业务处理逻辑代码, 200 表示成功, -1 表示出现错误private Integer code;// 错误信息private String errMessage;// 返回的数据private Object data;public static Result success(Object data) {Result result = new Result();result.setCode(200);result.setErrMessage("");result.setData(data);return result;}public static Result fail(Object data,String errMessage) {Result result = new Result();result.setCode(-1);result.setErrMessage(errMessage);result.setData(data);return result;}public static Result fail(String errMessage) {Result result = new Result();result.setCode(-1);result.setErrMessage(errMessage);return result;}
}

ResponseAdvice 类 – 启用统一结果返回功能

package cn.edu.zxj.ojspring.config;import cn.edu.zxj.ojspring.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;/*** Created with IntelliJ IDEA.* Description:** @author: zxj* @date: 2024-02-21* @time: 17:08:41*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@SneakyThrows@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {if (body instanceof Result) {return body;}if (body instanceof String) {ObjectMapper objectMapper = new ObjectMapper();return objectMapper.writeValueAsString(Result.success(body));}return Result.success(body);}
}

6.4.2 统一异常处理

package cn.edu.zxj.ojspring.config;import cn.edu.zxj.ojspring.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;/*** Created with IntelliJ IDEA.* Description:** @author: zxj* @date: 2024-02-21* @time: 17:29:42*/
@ControllerAdvice
@Slf4j
@ResponseBody
public class ErrorAdvice {@ExceptionHandlerpublic Result exceptionAdvice(Exception e) {log.error("发生错误, e: {}",e);return Result.fail("内部发生错误, 请联系管理员");}
}

7. 前端功能实现

一共需要两个页面:

  1. 题目列表页: 展示当前有哪些题目
  2. 题目详情页: 展示当前题目的细节, 包括提供一个代码编辑框, 让同学们编写代码.

使用网页模板

直接在百度上搜索 “免费网页模板”, 能找到很多免费模板网站. 可以直接基于现成的漂亮的页面进行修改.

tips: 做减法比做加法更容易.

将网页模板解压缩, 拷贝到项目的 static 目录中.

制作题目列表页

根据网页模板进行裁剪, 保留自己需要的部分.

主要是保留表格, 来作为展示题目列表的组件.

核心代码:

<div class="row mb-5" id="tables"><div class="col-sm-12"><div class="mt-3 mb-5"><h3>题目列表</h3><table class="table table-striped"><thead><tr><th>编号</th><th>标题</th><th>难度</th></tr></thead><tbody id="problemTable"><!-- <tr><td>1</td><td><a href="#">两数之和</a></td><td>简单</td></tr> --></tbody></table></div></div>
</div>
通过 ajax 获取后端数据

通过 ajax 的方式和后端交互, 获取到数据

在 methods 中创建 getProblems 方法

注意 url 的路径要用相对路径.

function getProblemList() {$.ajax({type: "get",url: "/oj/getProblem",success: function (result) {if (result != null && result.code == 200 && result.data != null) {makeProblemTable(result.data);}}});}function makeProblemTable(problemList) {let problemTable = document.querySelector("#problemTable");for (let problem of problemList) {let tr = document.createElement("tr");// 序号let tdId = document.createElement("td");tdId.innerHTML = problem.id;tr.appendChild(tdId);// 题目let tdTitle = document.createElement("td");let aTitle = document.createElement("a");aTitle.innerHTML = problem.title;aTitle.href = "/oj/getProblemDetail?id=" + problem.id;aTitle.target = "_blank";tdTitle.appendChild(aTitle);tr.appendChild(tdTitle);// 难度let tdLevel = document.createElement("td");tdLevel.innerHTML = problem.level;tr.appendChild(tdLevel);problemTable.appendChild(tr);}}getProblemList();

制作题目详情页

先把题目列表页拷贝一份, 修改名字为 problemDetail.html

调整页面内容. 去掉表格了.

  • 使用一个 jumbotron 表示题目详情
  • 使用一个 textarea 表示代码编辑框
  • 使用 button 表示提交按钮.
  • 再使用一个 jumbotron 表示题目运行结果.
<div class="container"><div class="row mt-4"><div class="col-sm-12 pb-4"><div class="jumbotron jumbotron-fluid"><div class="container" id="problemDesc"><!-- <h1>Container fluid size jumbotron</h1>
<p>Think BIG with a Bootstrap Jumbotron!</p> --></div></div></div></div><div class="row mt-4"><div class="col-sm-12 pb-4"><div class="form-group"><label for="codeEditor">代码编辑框</label>               <textarea class="form-control" id="codeEditor" style="width: 100%; height: 400px;"></textarea></div></div></div><button type="button" class="btn btn-primary" id="submitButton">提交</button><div class="row mt-4"><div class="col-sm-12 pb-4"><div class="jumbotron jumbotron-fluid"><div class="container"><pre id="problemResult"></pre><!-- <h1>Container fluid size jumbotron</h1>
<p>Think BIG with a Bootstrap Jumbotron!</p> --></div></div></div></div>
</div>

注意

  • 页面的基本结构为 .container -> .row -> .col -> 组件元素
  • 在这个页面模板中, 一行被分成了 12 份. .col-sm-12 表示这一列的宽度占据了 12 份(相当于 100%), 如果是 .col-sm.6 则表示占据 6 份(相当于 50%)
  • mt-4 表示 margin-top, pb-4 表示 padding-bottom
  • 使用 pre 标签, 可以使填充的内容保留换行.
从服务器上获取题目详情

在跳转到题目详情页中, 首先会把题目列表页的题目编号带过来.

题目详情页获取到编号, 通过 ajax 来获取题目详情.

function getProblemDetail() {$.ajax({type: "get",url: "/oj/getProblemDetail" + location.search,success: function (result) {if (result != null && result.code == 200 && result.data != null) {makeProblemDetail(result.data);}}});}function makeProblemDetail(problem) {let problemDetail = document.querySelector("#problemDetail");let firstRow = problem.id + "." + problem.title + '-' + problem.level;let h3 = document.createElement("h3");h3.innerHTML = firstRow;problemDetail.appendChild(h3);let pDescription = document.createElement("p");let preDescription = document.createElement("pre");preDescription.innerHTML = problem.description;pDescription.appendChild(preDescription);problemDetail.appendChild(pDescription);let codeEditor = document.querySelector("#codeEditor");codeEditor.innerHTML = problem.templateCode;let commitButton = document.querySelector("#commitButton");commitButton.onclick = function () {$.ajax({type: "post",url: "/compile",data: JSON.stringify({'id': problem.id,'code': codeEditor.value}),contentType: 'application/json; charset=utf-8',success: function (result) {if (result != null && result.code == 200 && result.data != null) {makeResult(result.data);}}});}}function makeResult(compileResponse) {let result = document.querySelector("#result");if (compileResponse.error == 0) {result.innerHTML = compileResponse.stdout;} else {result.innerHTML = compileResponse.reason;}}getProblemDetail();
实现提交代码

在刚才的 makeProblemDetail 函数中, 新增一个逻辑来实现提交代码.

在这里插入图片描述

引入代码编辑器组件

引入 ace.js
<script src="https://cdn.bootcss.com/ace/1.2.9/ace.js"></script>
<script src="https://cdn.bootcss.com/ace/1.2.9/ext-language_tools.js"></script>
初始化编辑器
function initAce() {// 参数 editor 就对应到刚才在 html 里加的那个 div 的 idlet editor = ace.edit("editor");editor.setOptions({enableBasicAutocompletion: true,enableSnippets: true,enableLiveAutocompletion: true});editor.setTheme("ace/theme/twilight");editor.session.setMode("ace/mode/java");editor.resize();document.getElementById('editor').style.fontSize = '20px';return editor;
}let editor = initAce();

并且将页面编辑框外面套一层 div, id 设为 editor, 并且一定要设置 min-height 属性.

<div id="editor" style="min-height:400px"><textarea style="width: 100%; height: 200px"></textarea>
</div>
修改 makeProblemDetail 方法

把显示模板代码的逻辑改为

// let codeEditor = document.querySelector("#codeEditor");
// codeEditor.innerHTML = problem.templateCode;
editor.setValue(this.problem.templateCode);
修改提交代码

把请求中的获取编辑器代码的逻辑进行修改.

submitButton.onclick = function () {// 点击这个按钮, 就要进行提交. (把编辑框的内容给提交到服务器上)let body = {id: problem.id,// code: codeEditor.value,code: editor.getValue(),}// ..... 其他代码略
}

8. 拓展功能

加入安全性控制

为了避免用户提交的代码包含恶意代码, 此处通过黑名单的方式, 对提交代码进行扫描限制. 如果发现用户提交代码中包含了黑名单中的关键词, 则直接报错.

在 Task 类中新增逻辑

public Answer compileAndRun(Question question) {Answer answer = new Answer();// 0. 准备好用来存放临时文件的目录File workDir = new File(WORK_DIR);if (!workDir.exists()) {// 创建多级目录.workDir.mkdirs();}// [新增代码] 进行安全性判定if (!checkCodeSafe(question.getCode())) {System.out.println("用户提交了不安全的代码!");answer.setError(3);answer.setReason("您提交的代码可能会危害到服务器, 禁止运行!");return answer;}// .... 其他代码略
}

checkCodeSafe 方法实现

private boolean checkCodeSafe(String code) {List<String> blackList = new ArrayList<>();// 防止提交的代码运行恶意程序blackList.add("Runtime");blackList.add("exec");// 禁止提交的代码读写文件blackList.add("java.io");// 禁止提交的代码访问网络blackList.add("java.net");for (String target : blackList) {int pos = code.indexOf(target);if (pos >= 0) {// 找到任意的恶意代码特征, 返回 false 表示不安全return false;}}return true;
}

9. 将项目部署到 Linux 服务器上面

9.1 在Linux上执行建库建表操作

执行以下 sql 语句

create database if not exists oj_spring_database charset utf8mb4;use oj_spring_database;
/*
Navicat MySQL Data TransferSource Server         : localhost_3306
Source Server Version : 80017
Source Host           : localhost:3306
Source Database       : oj_spring_databaseTarget Server Type    : MYSQL
Target Server Version : 80017
File Encoding         : 65001Date: 2024-02-22 15:20:07
*/SET FOREIGN_KEY_CHECKS=0;-- ----------------------------
-- Table structure for oj_table
-- ----------------------------
DROP TABLE IF EXISTS `oj_table`;
CREATE TABLE `oj_table` (`id` int(11) NOT NULL AUTO_INCREMENT,`title` varchar(50) NOT NULL COMMENT '文章标题',`level` varchar(50) NOT NULL COMMENT '题目难度',`description` varchar(4096) NOT NULL COMMENT '题目描述',`template_code` varchar(4096) NOT NULL COMMENT '代码初始化',`test_code` varchar(4096) NOT NULL COMMENT '测试代码',`delete_flag` tinyint(4) DEFAULT '0',`create_time` datetime DEFAULT CURRENT_TIMESTAMP,`update_time` datetime DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='题目表';-- ----------------------------
-- Records of oj_table
-- ----------------------------
INSERT INTO `oj_table` VALUES ('8', '寻找两个正序数组的中位数', '中等', '给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。\r\n\r\n算法的时间复杂度应该为 O(log (m+n)) 。\r\n\r\n \r\n\r\n示例 1:\r\n\r\n输入:nums1 = [1,3], nums2 = [2]\r\n输出:2.00000\r\n解释:合并数组 = [1,2,3] ,中位数 2\r\n示例 2:\r\n\r\n输入:nums1 = [1,2], nums2 = [3,4]\r\n输出:2.50000\r\n解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5\r\n \r\n\r\n \r\n\r\n提示:\r\n\r\nnums1.length == m\r\nnums2.length == n\r\n0 <= m <= 1000\r\n0 <= n <= 1000\r\n1 <= m + n <= 2000\r\n-106 <= nums1[i], nums2[i] <= 106', 'class Solution {\r\n    public double findMedianSortedArrays(int[] nums1, int[] nums2) {\r\n\r\n    }\r\n}', 'public static void main(String[] args) {\r\n        Solution solution = new Solution();\r\n\r\n\r\n        // testcase1\r\n        if (solution.findMedianSortedArrays(new int[]{1, 3}, new int[]{2}) == 2.00000) {\r\n            System.out.println(\"Test1 OK\");\r\n        } else {\r\n            System.out.println(\"Test1 failed\");\r\n        }\r\n        // testcase2\r\n        if (solution.findMedianSortedArrays(new int[]{1, 3}, new int[]{2, 4}) == 2.50000) {\r\n            System.out.println(\"Test2 OK\");\r\n        } else {\r\n            System.out.println(\"Test2 failed\");\r\n        }\r\n    }', '0', '2024-02-21 20:22:57', '2024-02-21 20:22:57');

9.2 多平台⽂件配置

针对不同平台创建不同的配置⽂件, 要求名字为application-XXX.yml或者application-XXX.properties

application-dev.yml

在这里插入图片描述

application-prod.yml

在这里插入图片描述

在主配置⽂件 application.yml 中指定配置⽂件, 并删除数据库相关配置

在这里插入图片描述

9.3 使用 Maven 打包成 jar

  1. 如果Test代码中有与环境配置相关的操作(⽐如数据库相关的操作), 打包会失败, 点击下图①处的图标, 可以跳过测试
  2. 点击clean->package

在这里插入图片描述

9.4 上传Jar包到服务器, 并运⾏

  1. 上传Jar包
    直接拖动打好的jar包到xshell窗⼝即可完成⽂件的上传
  2. 运⾏程序

nohup java -jar blog-spring-0.0.1-SNAPSHOT.jar &

nohup : 后台运⾏程序. ⽤于在系统后台不挂断地运⾏命令,退出终端不会影响程序的运⾏

在这里插入图片描述

10. 总结

  1. 项目的基本需求
    1. 题目列表页
    2. 题目详情页
    3. ``
    4. 代码编辑框
    5. 提交给服务器编译运行
    6. 展示结果
  2. 利用了多进程编程, 基于多进程编程(Runtime) 封装了一个 CommandUtils 类, 就可以创建进程执行一个具体的任务, 同时把输出结果记录到指定的文件中;
  3. 创建了一个 Task 类, 调用 CommandUtils 封装了一个 完整的 “编译-运行” 过程, 后面又给 Task 类扩充了一个基于黑名单的安全代码校验
  4. 设计了数据库, 封装了数据库操作, OJInfo, OJMapper
  5. 设计了前后端交互的接口
    1. 获取题目列表
    2. 获取题目详情
    3. 编译运行
  6. 基于 Spring 实现了这几个接口
  7. 引入了代码模板, 基于代码模板进行了修改, 创建除了两个页面
    1. 题目列表页 index.html
    2. 题目详情页 problemDetail.html
  8. 通过 js 代码, 实现了前端调用 HTTP API 的过程引入
  9. 引入 ace.js 让代码编辑框变得更加友好

代码获取

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/702657.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

项目:shell实现多级菜单脚本编写

目录 1. 提示 2. 演示效果 2.1. 一级菜单 2.2. 二级菜单 2.3. 执行操作 3. 参考代码 1. 提示 本脚本主要实现多级菜单效果&#xff0c;并没有安装LAMP、LNMP环境&#xff0c;如果要用在实际生成环境中部署LNMP、LAMP环境&#xff0c;只需要简单修改一下就可以了。 2. 演…

C#高级--设计模式(七个原则)

一、单一职责原则 原理&#xff1a; 单一职责原则即&#xff1a;对于一个类而言&#xff0c;应该仅有一个引起他变化的原因。换言之&#xff0c;一个类只负责一个功能领域中的相应职责。 单一职责原则是实现高内聚、低耦合的指导方针&#xff0c;它是最简单但又最难运用的原则…

keepalived+HAProxy+MySQL双主实验

keepalivedHAProxyMySQL双主实验 环境准备 node1(HAProxy1):192.168.184.10 node2(HAProxy2):192.168.184.20 node3(MySQL1):192.168.184.30 node4(MySQL2):192.168.184.40 虚拟IP vip&#xff1a;192.168.184.100MySQL部署 在node3执行以下脚本&#xff1a; #!/bin/bash sy…

【Linux】部署单机项目(自动化启动)

目录 一.jdk安装 二.tomcat安装 三.MySQL安装 四.部署项目 一.jdk安装 1.上传jdk安装包 jdk-8u151-linux-x64.tar.gz 进入opt目录&#xff0c;将安装包拖进去 2.解压安装包 防止后面单个系列解压操作&#xff0c;我这边就直接将所有的要用的全部给解压&#xff0c;如下图注…

【数据集】世界水评估方案指标:灌溉面积/灌溉用水等

世界水评估方案指标 概述(Overview)数据下载(Data Download)案例1:F. Irrigated lands案例2:G. Irrigated water use参考World Water Development Report II-Indicators for World Water Assessment Programme 概述(Overview) 在关于全球环境变化和可持续发展的辩论…

(详细使用指南)Linux下交叉编译带ffmpeg的opencv并移植到RK3588等ARM端

一 问题背景 瑞芯微RK3588等嵌入式板作为边缘端设备为算法模型的部署提供了便利&#xff0c;目前很多分类或好检测模型针对边缘端做了优化或量化&#xff0c;使得在边缘端也能达到实时稳定的识别和检测效果。 但嵌入式设备普遍的flash emmc不大&#xff0c;一般在32G左…

Jitsi Meet 大型视频会议调优方案

jitsi meet 大型视频会议调优方案 在举办一些大型会议的时候,比如100个人会议,为了节约宽带和节省资源,我们并不会选择传输全部的音视频资源。 举个例子,比如100个人线下会议,如果大家都说话的情况下,大家要么听不清,要么听得是声音最大的那几个人。 视频会议也可以借…

Mysql运维篇(五) 部署MHA--主机环境配置

一路走来&#xff0c;所有遇到的人&#xff0c;帮助过我的、伤害过我的都是朋友&#xff0c;没有一个是敌人。如有侵权&#xff0c;请留言&#xff0c;我及时删除&#xff01; 大佬博文 https://www.cnblogs.com/gomysql/p/3675429.html MySQL 高可用&#xff08;MHA&#x…

SpringMVC 学习(五)之域对象

目录 1 域对象介绍 2 向 request 域对象共享数据 2.1 通过 ServletAPI (HttpServletRequest) 向 request 域对象共享数据 2.2 通过 ModelAndView 向 request 域对象共享数据 2.3 通过 Model 向 request 域对象共享数据 2.4 通过 map 向 request 域对象共享数据 2.5 通过…

【前端素材】推荐优质后台管理系统Jampack平台模板(附源码)

一、需求分析 后台管理系统&#xff08;或称作管理后台、管理系统、后台管理平台&#xff09;是一种专门用于管理网站、应用程序或系统后台运营的软件系统。它通常由一系列功能模块组成&#xff0c;为管理员提供了管理、监控和控制网站或应用程序的各个方面的工具和界面。以下…

macOS系统下载IDEA的操作流程

第一步 进入官网 Download IntelliJ IDEA – The Leading Java and Kotlin IDE 第二步 根据mac的芯片选择版本下载 芯片的查看位置是【设置】-【通用】-【关于本机】-第二个&#xff0c;我的是Apple芯片&#xff0c;选Apple Silicon -- 第三步 右上角下载处打开安装包&…

Seata 入门知识

目录 概述 工作流程 工作模式 AT模式 TCC模式 概述 Seata 是一款开源的分布式事务解决方案&#xff0c;致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式&#xff0c;为用户打造一站式的分布式解决方案。 AT模式是阿里首推…

CUDA C++ 编程指南

目录 1. Introduction1.1. The Benefits of Using GPUs1.2. CUDA: A General-Purpose Parallel Computing1.3. A Scalable Programming Model CUDA C Programming Guide 1. Introduction 1.1. The Benefits of Using GPUs 在相似的价格和功耗范围内&#xff0c;图形处理单…

9个最受欢迎的开源自动化测试框架盘点!

自动化测试框架可以帮助测试人员评估多个Web和移动应用程序的功能&#xff0c;安全性&#xff0c;可用性和可访问性。尽管团队可以自己构建复杂的自动化测试框架&#xff0c;但是当他们可以使用现有的开源工具&#xff0c;库和测试框架获得相同甚至更好的结果时&#xff0c;通常…

Github开源贡献者的狂欢——教你如何免费领取价值$200的Starknet空投

前言&#xff1a; 2024 又迎来了四年一度的 BTC 减半时刻&#xff0c;币圈仿佛一下又热闹了起来&#xff0c;这几天有一个新的基于 ETH 的项目诞生了&#xff1a;StarkNet&#xff0c;代号 STRK&#xff0c;凡是在前 5000 个开源项目贡献过至少 3 个 commit 的程序猿都会被空投…

Linux系统网络服务部分拓展练习

1&#xff09;网关服务器&#xff1a;ens36&#xff1a;12.0.0.254/24&#xff0c;ens33&#xff1a;192.168.241.254/24&#xff1b;Server1&#xff1a;192.168.241.0/24&#xff1b;PC1和server2&#xff1a;自动获取IP&#xff1b;交换机无需配置。要求能够使用Xshell等远程…

MySQL - 事务日志

目录 1. redo日志 1.1 为什么需要REDO日志 1.2 REDO日志的好处、特点 1. 好处 2. 特点 1.3 redo的组成 1.4 redo的整体流程 1.5 redo log的刷盘策略 1.6 不同刷盘策略演示 1. 流程图 ​编辑2. 举例 1.7 写入redo log buffer 过程 1.8 redo log file 1. 相关参数…

matlab倒立摆小车LQR控制动画

1、内容简介 略 54-可以交流、咨询、答疑 2、内容说明 略 摆杆长度为 L&#xff0c;质量为 m 的单级倒立摆(摆杆的质心在杆的中心处)&#xff0c;小车的质量为 M。在水平方向施加控制力 u&#xff0c;相对参考系产生位移为 y。为了简化问题并且保其实质不变&#xff0c;忽…

【SelectIO】bitslice原语学习记录

基本概念 在Ultrascale (plus)系列上的FPGA中&#xff0c;Xilinx引入了bitslice硬核&#xff0c;它取代了7系列上的IDELAYCTRL/IODELAY/IOSERDES/IODDR系列硬核&#xff0c;用于为HP&#xff08;High Performance&#xff09;类型Bank上的IO接口提供串并转化、信号延时、三态控…

【k8s资源调度-HPA(自动扩缩容)】

1、HPA可以做什么&#xff1f; 通过观察pod的cpu、内存使用率或自定义metrics指标进行自动的扩容或缩容pod的数量。通常用于Deployment&#xff0c;不适用于无法扩/缩容的对象&#xff0c;如DaemonSet。控制管理器每隔30s(可以通过-horizontal-pod-autoscaler–sync-period修改…