【在线oj网站项目】在线oj做题网站是如何用代码写出来的

news2024/11/20 12:32:51

简单读懂一个在线oj网站项目

  • 逻辑
    • 1. 从数据库中读取题目信息
      • 1.1 创建题目数据库(sql)
      • 1.2 创建题目 类(dao包下的problem)
      • 1.3 实现数据库连接类 (common包下的 DBUtil)
        • 1. 通过datasource.getConnection();连接数据库
        • 2. 再次之前需要知道
          • 1)数据库的地址
          • 2)用户名
          • 3)密码
        • 3. 由于可能多线程调用连接数据库类,所以用多线程的懒汉模式实现datasource赋值
        • 4. 实现getconnection函数
        • 5. 关闭资源函数(connection,statement,resultSet)
      • 1.4 实现对数据库的题目 进行增删查改(dao包下的 problemDAO)
        • 增删查改的逻辑:
        • 1. 参数有不同的参数
        • 2. 建立connect
        • 3. 预备statement存储操作命令
        • 4. 连接connect
        • 5. 处理命令
        • 6. 执行命令
        • 7. 判断执行的命令是否成功
        • 8. 最后关闭资源
    • 2. 对用户提交的代码进行编译和运行(利用多进程)
      • 2.1 创建一个接收用户提交代码的 类(complie包下面的 Question)
        • 1. Question包含String code信息
      • 2.2 进行编译和运行的 类(compile包下 CommandUtil)
        • 1. run方法实现 执行cmd指令 并把结果输出到指定文件,并返回状态码
        • 2. `Process process = Runtime.getRuntime().exec(cmd);`创建进程
        • 3. 通过process.getInputStream();读取进程信息,FileOutputStream输出进程信息
        • 4. process.getErrorStream();FileOutputStream输出错误信息
        • 5. int exitCode = process.waitFor(); 返回子进程的状态码
      • 2.3 实现编辑运行代码的 具体逻辑 (compile包下的 Task)
        • 1. 工作目录,编译的类名,代码文件名,标准输出,错误输出
        • 2. 实现编译运行函数(传入参数Question)
          • 2.1 创建目录
          • 2.2 判断代码合理性
          • 2.3 把用户代码 写入要编译运行的 文件中
          • 2.4 创建编译命令,进行编译,判断是否正确,否则返回错误值
          • 2.5 创建运行指令,进行运行,判断是否正确
      • 2.4 由于编译运行需要返回答案,所以创建Answer类
        • 1. error错误值(0正常,1编译错误,2运行错误)
        • 2. reason(错误原因)
        • 3. stdout(标准输出)
        • 4. stderr(错误输出)
    • 3. 前后端交互
      • 1. 客户端向服务器请求所有题目 或者 单个题目
        • 前端
            • 获取所有题目
            • 获取一个题目
        • 后端
          • 1. objectMapper把要返回的题目列表的String 转成body类型
          • 2. 返回值的状态值设定和类型设定
          • 3. 创建数据库的题目操作类,进行查询
      • 2. 后端读取前端提交的代码,进行编译运行,返回结果
        • 前端提交代码
        • 后端处理
          • 1. (首先清楚前端传过来的是什么——编译的代码和题目序号)把前端传来的body读取成string,然后按着数据类型转成相应的类
          • 2. 创建数据库操作函数,查询要编译的题目
          • 3. 处理代码,进行编译和运行
    • 4. 前端页面

逻辑

首先一个在线oj网站,我们想一下需要什么

  1. 题目列表页(展现出网站上包含的所有题目)
  2. 题目详情页(展现一个题目的具体详情,可以做题了)
  3. 代码编辑框(编辑代码)
  4. 提交给服务器编译运行(编译运行代码)
  5. 展示结果(展示运行代码的结果)

我们就按着上述逻辑写网站

由低而上写
代码最底层

  1. 从数据库中读取题目信息
  2. 对用户提交的代码进行编译和运行
  3. 前后端交互
  4. 前端模块页面的实现

接下来就按着这个逻辑写

1. 从数据库中读取题目信息

1.1 创建题目数据库(sql)

  1. 标题
  2. 难度
  3. 介绍
  4. 给定的代码
  5. 测试用户答案的代码
  6. 题目序号(自增主键)

1.2 创建题目 类(dao包下的problem)

package dao;

public class Problem {
    private int id;
    private String title;
    private String level;
    private String description;
    private String templateCode;
    private String testCode;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getLevel() {
        return level;
    }

    public void setLevel(String level) {
        this.level = level;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getTemplateCode() {
        return templateCode;
    }

    public void setTemplateCode(String templateCode) {
        this.templateCode = templateCode;
    }

    public String getTestCode() {
        return testCode;
    }

    public void setTestCode(String testCode) {
        this.testCode = testCode;
    }

    @Override
    public String toString() {
        return "Problem{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", level='" + level + '\'' +
                ", description='" + description + '\'' +
                ", templateCode='" + templateCode + '\'' +
                ", testCode='" + testCode + '\'' +
                '}';
    }
}

1.3 实现数据库连接类 (common包下的 DBUtil)

1. 通过datasource.getConnection();连接数据库

2. 再次之前需要知道

1)数据库的地址
2)用户名
3)密码

3. 由于可能多线程调用连接数据库类,所以用多线程的懒汉模式实现datasource赋值

4. 实现getconnection函数

5. 关闭资源函数(connection,statement,resultSet)

package common;

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import com.sun.xml.internal.ws.server.ServerRtException;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class DBUtil{
    // 需要封装和数据库之间的连接操作.
    private static final String URL = "jdbc:mysql://127.0.0.1:3306/oj_database?characterEncoding=utf8&useSSL=false";
    private static final String USERNAME = "root";
    // private static final String PASSWORD = "2222";
    private static final String PASSWORD = "123456";

    private static volatile DataSource dataSource = null;




    private static DataSource getDataSource(){
        if(dataSource == null){
            synchronized (DBUtil.class){
                if(dataSource == null){
                    MysqlDataSource mysqlDataSource = new MysqlDataSource();
                    mysqlDataSource.setURL(URL);
                    mysqlDataSource.setUser(USERNAME);
                    mysqlDataSource.setPassword(PASSWORD);
                    dataSource = mysqlDataSource;
                }
            }
        }
        return dataSource;
    }

    public static Connection getConnection() throws SQLException {
        return getDataSource().getConnection();
    }

    public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

1.4 实现对数据库的题目 进行增删查改(dao包下的 problemDAO)

增删查改的逻辑:

1. 参数有不同的参数

2. 建立connect

3. 预备statement存储操作命令

4. 连接connect

5. 处理命令

6. 执行命令

7. 判断执行的命令是否成功

8. 最后关闭资源

package dao;

import common.DBUtil;
import javafx.scene.layout.Priority;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

// 通过这个类封装了针对 Problem 的增删改查.
// 1. 新增题目
// 2. 删除题目
// 3. 查询题目列表
// 4. 查询题目详情
public class ProblemDAO {
    public void insert(Problem problem) {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            // 1. 和数据库建立连接
            connection = DBUtil.getConnection();
            // 2. 构造 SQL 语句
            String sql = "insert into oj_table values(null, ?, ?, ?, ?, ?)";
            statement = connection.prepareStatement(sql);
            statement.setString(1, problem.getTitle());
            statement.setString(2, problem.getLevel());
            statement.setString(3, problem.getDescription());
            statement.setString(4, problem.getTemplateCode());
            statement.setString(5, problem.getTestCode());
            // 3. 执行 SQL
            int ret = statement.executeUpdate();
            if (ret != 1) {
                System.out.println("题目新增失败!");
            } else {
                System.out.println("题目新增成功!");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, null);
        }
    }

    public void delete(int id) {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            // 1. 和数据库建立连接
            connection = DBUtil.getConnection();
            // 2. 拼装 SQL 语句
            String sql = "delete from oj_table where id = ?";
            statement = connection.prepareStatement(sql);
            statement.setInt(1, id);
            // 3. 执行 SQL
            int ret = statement.executeUpdate();
            if (ret != 1) {
                System.out.println("删除题目失败!");
            } else {
                System.out.println("删除题目成功!");
            }
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, null);
        }
    }

    // 这个操作是把当前题目列表中的所有题都查出来了
    // 万一数据库中的题目特别多, 咋办? 只要实现 "分页查询" 即可. 后台实现分页查询, 非常容易.
    // 前端传过来一个当前的 "页码" , 根据页码算一下, 依据 sql limit offset 语句, 要算出来 offset 是 几
    // 但是前端这里实现一个分页器稍微麻烦一些(比后端要麻烦很多). 此处暂时不考虑分页功能.
    public List<Problem> selectAll() {
        List<Problem> problems = new ArrayList<>();
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            // 1. 和数据库建立连接
            connection = DBUtil.getConnection();
            // 2. 拼装 SQL
            String sql = "select id, title, level from oj_table";
            statement = connection.prepareStatement(sql);
            // 3. 执行 SQL
            resultSet = statement.executeQuery();
            // 4. 遍历 resultSet
            while (resultSet.next()) {
                // 每一行都是一个 Problem 对象
                Problem problem = new Problem();
                problem.setId(resultSet.getInt("id"));
                problem.setTitle(resultSet.getString("title"));
                problem.setLevel(resultSet.getString("level"));
                problems.add(problem);
            }
            return problems;
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, resultSet);
        }
        return null;
    }

    public Problem selectOne(int id) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            // 1. 和数据库建立连接
            connection = DBUtil.getConnection();
            // 2. 拼接 SQL 语句
            String sql = "select * from oj_table where id = ?";
            statement = connection.prepareStatement(sql);
            statement.setInt(1, id);
            // 3. 执行 SQL
            resultSet = statement.executeQuery();
            // 4. 遍历查询结果. (由于 id 是主键, 按照 id 查找的结果一定是唯一的)
            if (resultSet.next()) {
                Problem problem = new Problem();
                problem.setId(resultSet.getInt("id"));
                problem.setTitle(resultSet.getString("title"));
                problem.setLevel(resultSet.getString("level"));
                problem.setDescription(resultSet.getString("description"));
                problem.setTemplateCode(resultSet.getString("templateCode"));
                problem.setTestCode(resultSet.getString("testCode"));
                return problem;
            }
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, resultSet);
        }
        return null;
    }

    private static void testInsert() {
        ProblemDAO problemDAO = new ProblemDAO();
        Problem problem = new Problem();
        // problem.setId();
        problem.setTitle("两数之和");
        problem.setLevel("简单");
        problem.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" +
                "进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗?\n" +
                "\n" +
                "来源:力扣(LeetCode)\n" +
                "链接:https://leetcode-cn.com/problems/two-sum\n" +
                "著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。");
        problem.setTemplateCode("class Solution {\n" +
                "    public int[] twoSum(int[] nums, int target) {\n" +
                "\n" +
                "    }\n" +
                "}");
        problem.setTestCode("    public static void main(String[] args) {\n" +
                "        Solution solution = new Solution();\n" +
                "        // testcase1\n" +
                "        int[] nums = {2,7,11,15};\n" +
                "        int target = 9;\n" +
                "        int[] result = solution.twoSum(nums, target);\n" +
                "        if (result.length == 2 && result[0] == 0 && result[1] == 1) {\n" +
                "            System.out.println(\"testcase1 OK\");\n" +
                "        } else {\n" +
                "            System.out.println(\"testcase1 failed!\");\n" +
                "        }\n" +
                "\n" +
                "        // testcase2\n" +
                "        int[] nums2 = {3,2,4};\n" +
                "        int target2 = 6;\n" +
                "        int[] result2 = solution.twoSum(nums2, target2);\n" +
                "        if (result2.length == 2 && result[0] == 1 && result[1] == 2) {\n" +
                "            System.out.println(\"testcase2 OK\");\n" +
                "        } else {\n" +
                "            System.out.println(\"testcase2 failed!\");\n" +
                "        }\n" +
                "    }\n");
        problemDAO.insert(problem);
        System.out.println("插入成功!");
    }

    private static void testSelectAll() {
        ProblemDAO problemDAO = new ProblemDAO();
        List<Problem> problems = problemDAO.selectAll();
        System.out.println(problems);
    }

    private static void testSelectOne() {
        ProblemDAO problemDAO = new ProblemDAO();
        Problem problem = problemDAO.selectOne(1);
        System.out.println(problem);
    }

    private static void testDelete() {
        ProblemDAO problemDAO = new ProblemDAO();
        problemDAO.delete(1);
    }

    public static void main(String[] args) {
        testInsert();
        // testSelectAll();
        // testSelectOne();
        // testDelete();
    }
}

2. 对用户提交的代码进行编译和运行(利用多进程)

2.1 创建一个接收用户提交代码的 类(complie包下面的 Question)

1. Question包含String code信息

package compile;

// 用这个类来表示一个 task 的输入内容
// 会包含要编译的代码
public class Question {
    private String code;

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }
}

2.2 进行编译和运行的 类(compile包下 CommandUtil)

1. run方法实现 执行cmd指令 并把结果输出到指定文件,并返回状态码

2. Process process = Runtime.getRuntime().exec(cmd);创建进程

3. 通过process.getInputStream();读取进程信息,FileOutputStream输出进程信息

4. process.getErrorStream();FileOutputStream输出错误信息

5. int exitCode = process.waitFor(); 返回子进程的状态码

package compile;

import dao.Problem;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class CommandUtil {
    // 1. 通过 Runtime 类得到 Runtime 实例, 执行 exec 方法
    // 2. 获取到标准输出, 并写入到指定文件中.
    // 3. 获取到标准错误, 并写入到指定文件中.
    // 4. 等待子进程结束, 拿到子进程的状态码, 并返回.
    public static int run(String cmd, String stdoutFile, String stderrFile) {
        try {
            // 1. 通过 Runtime 类得到 Runtime 实例, 执行 exec 方法
            Process process = Runtime.getRuntime().exec(cmd);
            // 2. 获取到标准输出, 并写入到指定文件中.
            if (stdoutFile != null) {
                InputStream stdoutFrom = process.getInputStream();
                FileOutputStream stdoutTo = new FileOutputStream(stdoutFile);
                while (true) {
                    int ch = stdoutFrom.read();
                    if (ch == -1) {
                        break;
                    }
                    stdoutTo.write(ch);
                }
                stdoutFrom.close();
                stdoutTo.close();
            }
            // 3. 获取到标准错误, 并写入到指定文件中.
            if (stderrFile != null) {
                InputStream stderrFrom = process.getErrorStream();
                FileOutputStream stderrTo = new FileOutputStream(stderrFile);
                while (true) {
                    int ch = stderrFrom.read();
                    if (ch == -1) {
                        break;
                    }
                    stderrTo.write(ch);
                }
                stderrFrom.close();
                stderrTo.close();
            }
            // 4. 等待子进程结束, 拿到子进程的状态码, 并返回.
            int exitCode = process.waitFor();
            return exitCode;
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
        return 1;
    }

    public static void main(String[] args) {
        CommandUtil.run("javac", "stdout.txt", "stderr.txt");
    }
}

2.3 实现编辑运行代码的 具体逻辑 (compile包下的 Task)

1. 工作目录,编译的类名,代码文件名,标准输出,错误输出

2. 实现编译运行函数(传入参数Question)

2.1 创建目录
2.2 判断代码合理性
2.3 把用户代码 写入要编译运行的 文件中
2.4 创建编译命令,进行编译,判断是否正确,否则返回错误值
2.5 创建运行指令,进行运行,判断是否正确
package compile;


import common.FileUtil;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

// 每次的 "编译+运行" 这个过程, 就称为是一个 compile.Task
public class Task {
    // 通过一组常量来约定临时文件的名字.
    // 之前这里的名字都是静态常量. 但是现在要实现针对每个请求都有不同的临时目录, 就不能使用静态常量了
    // 这个表示所有临时文件所在的目录
    private String WORK_DIR = null;
    // 约定代码的类名
    private String CLASS = null;
    // 约定要编译的代码文件名.
    private String CODE = null;
    // 约定存放编译错误信息的文件名
    private String COMPILE_ERROR = null;
    // 约定存放运行时的标准输出的文件名
    private String STDOUT = null;
    // 约定存放运行时的标准错误的文件名
    private String STDERR = null;

    public Task() {
        // 在 Java 中使用 UUID 这个类就能生成一个 UUID 了
        WORK_DIR = "./tmp/" + UUID.randomUUID().toString() + "/";
        CLASS = "Solution";
        CODE = WORK_DIR + "Solution.java";
        COMPILE_ERROR = WORK_DIR + "compileError.txt";
        STDOUT = WORK_DIR + "stdout.txt";
        STDERR = WORK_DIR + "stderr.txt";
    }

    // 这个 compile.Task 类提供的核心方法, 就叫做 compileAndRun, 编译+运行 的意思.
    // 参数: 要编译运行的 java 源代码.
    // 返回值: 表示编译运行的结果. 编译出错/运行出错/运行正确.....
    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;
        }
        // 1. 把 question 中的 code 写入到一个 Solution.java 文件中.
        FileUtil.writeFile(CODE, question.getCode());
        // 2. 创建子进程, 调用 javac 进行编译. 注意! 编译的时候, 需要有一个 .java 文件.
        //       如果编译出错, javac 就会把错误信息给写入到 stderr 里. 就可以用一个专门的文件来保存. compileError.txt
        //    需要先把编译命令给构造出来.
        String compileCmd = String.format("javac -encoding utf8 %s -d %s", CODE, WORK_DIR);
        System.out.println("编译命令: " + compileCmd);
        CommandUtil.run(compileCmd, null, COMPILE_ERROR);
        // 如果编译出错了, 错误信息就被记录到 COMPILE_ERROR 这个文件中了. 如果没有编译出错, 这个文件是空文件.
        String compileError = FileUtil.readFile(COMPILE_ERROR);
        if (!compileError.equals("")) {
            // 编译出错!
            // 直接返回 compile.Answer, 让 compile.Answer 里面记录编译的错误信息.
            System.out.println("编译出错!");
            answer.setError(1);
            answer.setReason(compileError);
            return answer;
        }
        // 编译正确! 继续往下执行运行的逻辑
        // 3. 创建子进程, 调用 java 命令并执行
        //       运行程序的时候, 也会把 java 子进程的标准输出和标准错误获取到. stdout.txt, stderr.txt
        String runCmd = String.format("java -classpath %s %s", WORK_DIR, CLASS);
        System.out.println("运行命令: " + runCmd);
        CommandUtil.run(runCmd, STDOUT, STDERR);
        String runError = FileUtil.readFile(STDERR);
        if (!runError.equals("")) {
            System.out.println("运行出错!");
            answer.setError(2);
            answer.setReason(runError);
            return answer;
        }
        // 4. 父进程获取到刚才的编译执行的结果, 并打包成 compile.Answer 对象
        //       编译执行的结果, 就通过刚才约定的这几个文件来进行获取即可.
        answer.setError(0);
        answer.setStdout(FileUtil.readFile(STDOUT));
        return answer;
    }

    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;
    }

    public static void main(String[] args) {
        Task task = new Task();
        Question question = new Question();
        question.setCode("public class Solution {\n" +
                "    public static void main(String[] args) {\n" +
                "        System.out.println(\"hello world\");\n" +
                "    }\n" +
                "}\n");
        Answer answer = task.compileAndRun(question);
        System.out.println(answer);
    }
}

2.4 由于编译运行需要返回答案,所以创建Answer类

1. error错误值(0正常,1编译错误,2运行错误)

2. reason(错误原因)

3. stdout(标准输出)

4. stderr(错误输出)

package compile;

// 表示一个 compile.Task 的执行结果
public class Answer {
    // 错误码. 约定 error 为 0 表示编译运行都 ok, 为 1 表示编译出错, 为 2 表示运行出错(抛异常).
    private int error;
    // 出错的提示信息. 如果 error 为 1, 编译出错了, reason 中就放编译的错误信息, 如果 error 为 2, 运行异常了, reason 就放异常信息
    private String reason;
    // 运行程序得到的标准输出的结果.
    private String stdout;
    // 运行程序得到的标准错误的结果.
    private String stderr;

    public int getError() {
        return error;
    }

    public void setError(int error) {
        this.error = error;
    }

    public String getReason() {
        return reason;
    }

    public void setReason(String reason) {
        this.reason = reason;
    }

    public String getStdout() {
        return stdout;
    }

    public void setStdout(String stdout) {
        this.stdout = stdout;
    }

    public String getStderr() {
        return stderr;
    }

    public void setStderr(String stderr) {
        this.stderr = stderr;
    }

    @Override
    public String toString() {
        return "compile.Answer{" +
                "error=" + error +
                ", reason='" + reason + '\'' +
                ", stdout='" + stdout + '\'' +
                ", stderr='" + stderr + '\'' +
                '}';
    }

}

3. 前后端交互

1. 客户端向服务器请求所有题目 或者 单个题目

通过servlet进行前后端交互

前端:通过problem的URL地址访问(如果没有其它参数,则是查询所有题目,如果有id参数,就是查询具体题目)
后端:返回题目的具体详情
在这里插入图片描述在这里插入图片描述

前端

获取所有题目
        <script>
            // 在页面加载的时候, 尝试从服务器获取题目列表. 通过 ajax 的方式来进行获取
            function getProblems() {
                // 1. 先通过 ajax 从服务器获取到题目列表. 
                $.ajax({
                    url: "problem",
                    type: "GET",
                    success: function(data, status) {
                        // data 是响应的 body, status 是响应的状态码
                        // 2. 把得到的响应数据给构造成 HTML 片段
                        makeProblemTable(data);
                    }
                })
            }

            // 通过这个函数来把数据转换成 HTML 页面片段
            function makeProblemTable(data) {
                let problemTable = document.querySelector("#problemTable");
                for (let problem of data) {
                    let tr = document.createElement("tr");

                    let tdId = document.createElement("td");
                    tdId.innerHTML = problem.id;
                    tr.appendChild(tdId);

                    let tdTitle = document.createElement("td");
                    let a = document.createElement("a");
                    a.innerHTML = problem.title;
                    a.href = 'problemDetail.html?id=' + problem.id;
                    a.target = '_blank';
                    tdTitle.appendChild(a);
                    tr.appendChild(tdTitle);

                    let tdLevel = document.createElement("td");
                    tdLevel.innerHTML = problem.level;
                    tr.appendChild(tdLevel);

                    problemTable.appendChild(tr);
                }
            }

            getProblems();
        </script>
获取一个题目
<script>
            // 通过 ajax 从服务器获取到题目的详情
            function getProblem() {
                // 1. 通过 ajax 给服务器发送一个请求
                $.ajax({
                    url: "problem" + location.search,
                    type: "GET",
                    success: function (data, status) {
                        makeProblemDetail(data);
                    }
                })
            }

            function makeProblemDetail(problem) {
                // 1. 获取到 problemDesc, 把题目详情填写进去
                let problemDesc = document.querySelector("#problemDesc");

                let h3 = document.createElement("h3");
                h3.innerHTML = problem.id + "." + problem.title + "_" + problem.level
                problemDesc.appendChild(h3);

                let pre = document.createElement("pre");
                let p = document.createElement("p");
                p.innerHTML = problem.description;
                pre.appendChild(p);
                problemDesc.appendChild(pre);

                // 2. 把代码的模板填写到编辑框中. 
                // let codeEditor = document.querySelector("#codeEditor");
                // codeEditor.innerHTML = problem.templateCode;
                editor.setValue(problem.templateCode)
</script>

后端

1. objectMapper把要返回的题目列表的String 转成body类型
2. 返回值的状态值设定和类型设定
3. 创建数据库的题目操作类,进行查询
package api;

import com.fasterxml.jackson.databind.ObjectMapper;
import dao.Problem;
import dao.ProblemDAO;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@WebServlet("/problem")
public class ProblemServlet extends HttpServlet {
    //ObjectMapper类(com.fasterxml.jackson.databind.ObjectMapper)是Jackson的主要类,它可以帮助我们快速的进行各个类型和Json类型的相互转换。
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置返回的状态码 200表示成功
        resp.setStatus(200);
        //返回的数据类型
        resp.setContentType("application/json;charset=utf8");

        ProblemDAO problemDAO = new ProblemDAO();
        // 尝试获取 id 参数. 如果能获取到, 说明是获取题目详情; 如果不能获取到, 说明是获取题目列表.
        String idString = req.getParameter("id");
        if (idString == null || "".equals(idString)) {
            // 没有获取到 id 字段. 查询题目列表
            List<Problem> problems = problemDAO.selectAll();
            String respString = objectMapper.writeValueAsString(problems);
            resp.getWriter().write(respString);
        } else {
            // 获取到了题目 id. 查询题目详情
            Problem problem = problemDAO.selectOne(Integer.parseInt(idString));
            String respString = objectMapper.writeValueAsString(problem);
            resp.getWriter().write(respString);
        }

    }
}

2. 后端读取前端提交的代码,进行编译运行,返回结果

在这里插入图片描述

前端提交代码

<script>
                // 3. 给提交按钮注册一个点击事件
                let submitButton = document.querySelector("#submitButton");
                submitButton.onclick = function () {
                    // 点击这个按钮, 就要进行提交. (把编辑框的内容给提交到服务器上)
                    let body = {
                        id: problem.id,
                        // code: codeEditor.value,
                        code: editor.getValue(),
                    };
                    $.ajax({
                        type: "POST",
                        url: "compile",
                        data: JSON.stringify(body),
                        success: function (data, status) {
                            let problemResult = document.querySelector("#problemResult");
                            if (data.error == 0) {
                                // 编译运行没有问题, 把 stdout 显示到页面中
                                problemResult.innerHTML = data.stdout;
                            } else {
                                // 编译运行没有问题, 把 reason 显示到页面中
                                problemResult.innerHTML = data.reason;
                            }
                        }
                    });
                }
            }

</script>

后端处理

1. (首先清楚前端传过来的是什么——编译的代码和题目序号)把前端传来的body读取成string,然后按着数据类型转成相应的类
2. 创建数据库操作函数,查询要编译的题目
3. 处理代码,进行编译和运行
package api;

import com.fasterxml.jackson.databind.ObjectMapper;
import common.CodeInValidException;
import common.ProblemNotFoundException;
import compile.Answer;
import compile.Question;
import compile.Task;
import dao.Problem;
import dao.ProblemDAO;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;

@WebServlet("/compile")
public class CompileServlet extends HttpServlet {
    static class CompileRequest {
        public int id;
        public String code;
    }

    static class CompileResponse {
        // 约定 error 为 0 表示编译运行 ok, error 为 1 表示编译出错, error 为 2 表示运行异常(用户提交的代码异常了), 3 表示其他错误
        public int error;
        public String reason;
        public String stdout;
    }

    private ObjectMapper objectMapper = new ObjectMapper();


//    {
//        "id": 2,
//        "code": "class Solution {\n    public int[] twoSum(int[] nums, int target) {\n        int[] a = {0, 1};\n        return a;\n    }\n}    "
//    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws 35812ServletException, IOException {
        // 临时加一下这个代码, 来获取到 SmartTomcat 的工作目录
        System.out.println("用户的当前工作目录: "+System.getProperty("user.dir"));

        CompileRequest compileRequest = null;
        CompileResponse compileResponse = new CompileResponse();
        try {
            resp.setStatus(200);
            resp.setContentType("application/json;charset=utf8");
            // 1. 先读取请求的正文. 别按照 JSON 格式进行解析
            String body = readBody(req);
            compileRequest = objectMapper.readValue(body, CompileRequest.class);
            // 2. 根据 id 从数据库中查找到题目的详情 => 得到测试用例代码
            ProblemDAO problemDAO = new ProblemDAO();
            Problem problem = problemDAO.selectOne(compileRequest.id);
            if (problem == null) {
                // 为了统一处理错误, 在这个地方抛出一个异常.
                throw new ProblemNotFoundException();
            }
            // testCode 是测试用例的代码
            String testCode = problem.getTestCode();
            // requestCode 是用户提交的代码
            String requestCode = compileRequest.code;
            // 3. 把用户提交的代码和测试用例代码, 给拼接成一个完整的代码.
            String finalCode = mergeCode(requestCode, testCode);
            if (finalCode == null) {
                throw new CodeInValidException();
            }
            // System.out.println(finalCode);
            // 4. 创建一个 Task 实例, 调用里面的 compileAndRun 来进行编译运行.
            Task task = new Task();
            Question question = new Question();
            question.setCode(finalCode);
            Answer answer = task.compileAndRun(question);
            // 5. 根据 Task 运行的结果, 包装成一个 HTTP 响应
            compileResponse.error = answer.getError();
            compileResponse.reason = answer.getReason();
            compileResponse.stdout = answer.getStdout();
        } catch (ProblemNotFoundException e) {
            // 处理题目没有找到的异常
            compileResponse.error = 3;
            compileResponse.reason = "没有找到指定的题目! id=" + compileRequest.id;
        } catch (CodeInValidException e) {
            compileResponse.error = 3;
            compileResponse.reason = "提交的代码不符合要求!";
        } finally {
            String respString = objectMapper.writeValueAsString(compileResponse);
            resp.getWriter().write(respString);
        }
    }

    private static String mergeCode(String requestCode, String testCode) {
        // 1. 查找 requestCode 中的最后一个 }
        int pos = requestCode.lastIndexOf("}");
        if (pos == -1) {
            // 说明提交的代码完全没有 } , 显然是非法的代码.
            return null;
        }
        // 2. 根据这个位置进行字符串截取
        String subStr = requestCode.substring(0, pos);
        // 3. 进行拼接
        return subStr + testCode + "\n}";
    }

    private static String readBody(HttpServletRequest req) throws UnsupportedEncodingException {
        // 1. 先根据 请求头 里面的 ContentLength 获取到 body 的长度(单位是字节)
        int contentLength = req.getContentLength();
        // 2. 按照这个长度准备好一个 byte[] .
        byte[] buffer = new byte[contentLength];
        // 3. 通过 req 里面的 getInputStream 方法, 获取到 body 的流对象.
        try (InputStream inputStream = req.getInputStream()) {
            // 4. 基于这个流对象, 读取内容, 然后把内容放到 byte[] 数组中即可.
            inputStream.read(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 5. 把这个 byte[] 的内容构造成一个 String
        return new String(buffer, "UTF8");
    }
}

4. 前端页面

根据上述逻辑
自己设计即可

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

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

相关文章

剑指offer-3-10

文章目录 3.数组中的重复数字4.二维数组中的查找5.替换空格6.从尾到头打印链表7.重建二叉树&#xff08;⭐&#xff09;8.用两个栈实现队列10.青蛙跳台阶 3.数组中的重复数字 1.用set HashSet set1new HashSet();for(int i0;i<array.length;i){if(set1.contains(array[i])){…

【Anaconda】虚拟环境离线迁移

【Anaconda】虚拟环境离线迁移 项目中常常遇到需要在离线环境中下为anaconda安装有关深度学习的虚拟环境的问题&#xff0c;服务器设备不能连接互联网的环境。 文章目录 【Anaconda】虚拟环境离线迁移前言环境信息导出虚拟环境离线恢复虚拟环境总结 前言 为了实现Anaconda虚拟…

并行for循环,简单的改动让python飞起来

概要 Python在处理大的数据集的时候总是速度感人。代码一旦开始运行&#xff0c;剩下的时间只好满心愧疚地刷手机。 MPI&#xff08;Message Passing Interface&#xff09;是在并行计算中&#xff0c;在不同进程间传递信息的标准解决方案。mpi4py是它的python版本。 网上有大…

OpenUSD联盟:塑造元宇宙的3D未来

一、引言 近日&#xff0c;美国3D内容行业的五家主要公司苹果、英伟达、皮克斯、Adobe和Autodesk联合成立了OpenUSD联盟&#xff08;AOUSD&#xff09;。这一联盟的成立标志着元宇宙领域的一次重要合作&#xff0c;旨在制定元宇宙的3D图形标准。本文将深入探讨OpenUSD联盟的目…

配置NFS,遇到的问题总结(电脑连接WiFi,开发板和电脑直连)

我使用的正点原子的方法&#xff0c;仅展示遇到的问题。 1. 这一步&#xff0c;需要先将Ubuntu关掉&#xff0c;再进行设置。 如果设置了不显示&#xff0c;需要移出网络&#xff0c;再次添加。 先设置Windows的网络&#xff0c;再设置&#xff0c;虚拟机的网络&#xff0c;即…

idea运行web老项目

idea打开老项目 首先你要用idea打开老项目&#xff0c;这里看我之前发的文章就可以啦 运行web项目 1. 编辑配置 2. 添加tomcat项目 3. 设置tomcat参数 选择本地tomcat&#xff0c;注意有的tomcat版本&#xff0c;不然运行不了设置-Dfile.encodingUTF-8 启动&#xff0c;注意…

[openCV]基于拟合中线的智能车巡线方案V3

import cv2 as cv import os import numpy as np# 遍历文件夹函数 def getFileList(dir, Filelist, extNone):"""获取文件夹及其子文件夹中文件列表输入 dir&#xff1a;文件夹根目录输入 ext: 扩展名返回&#xff1a; 文件路径列表"""newDir d…

kafka 理论知识

1 首先要了解kafka是什么 Kafka是一个分布式的消息订阅系统 1.1 kafka存储消息的过程 消息被持久化到一个topic中&#xff0c;topic是按照“主题名-分区”存储的&#xff0c;一个topic可以分为多个partition&#xff0c;在parition(分区)内的每条消息都有一个有序的id号&am…

【基础类】—三栏页面布局的方案和优缺点

一、假设高度已知&#xff0c;中间宽度自适应&#xff0c;三栏&#xff08;列&#xff09;布局的方案有哪些&#xff1f; float浮动、absolute绝对定位、flex弹性盒子、table表格布局、grid网格布局 浮动 float <style>* {margin: 0;padding: 0;}.container {width: 1…

Stable Diffusion 硬核生存指南:WebUI 中的 CodeFormer

本篇文章聊聊 Stable Diffusion WebUI 中的核心组件&#xff0c;强壮的人脸图像面部画面修复模型 CodeFormer 相关的事情。 写在前面 在 Stable Diffusion WebUI 项目中&#xff0c;源码 modules 目录中&#xff0c;有一个有趣的目录叫做 CodeFormer&#xff0c;它就是本文的…

Vue2 第十六节 $nextTick 和 动画与过渡效果

1. $nextTick 2.过渡与动画 一. $nextTick ① 语法&#xff1a;this.$nextTick(回调函数) ② 作用&#xff1a;在下一次DOM更新结束后执行其指定的回调 nextTick所指定的回调会在DOM节点更新后执行 ③ 使用场景&#xff1a;当改变数据后&#xff0c;要基于更新后的新 DOM …

ClickHouse(十):Clickhouse表引擎 - Special系列表引擎

进入正文前&#xff0c;感谢宝子们订阅专题、点赞、评论、收藏&#xff01;关注IT贫道&#xff0c;获取高质量博客内容&#xff01; &#x1f3e1;个人主页&#xff1a;含各种IT体系技术&#xff0c;IT贫道_Apache Doris,Kerberos安全认证,大数据OLAP体系技术栈-CSDN博客 &…

ad+硬件每日学习十个知识点(19)23.7.30 (LDO参数介绍)

文章目录 1.LDO的压差dropout2.线性调整率&#xff08;输入变化对输出的影响&#xff09;3.负载调整率&#xff08;负载变化对输出电压的影响&#xff09;4.纹波抑制比PSRR&#xff08;电源抑制比&#xff09;5.哪些元器件对PSRR影响大&#xff1f;&#xff08;右下角图&#x…

算法通过村——Hash和队列问题解析

算法的备胎Hash和找靠山的队列 备胎Hash Hash&#xff0c;不管是算法&#xff0c;还是在工程中都会大量使用。很多复杂的算法问题都用Hash能够轻松解决&#xff0c;也正是如此&#xff0c;在算法例就显得没什么思维含量&#xff0c;所以Hash是应用里的扛把子&#xff0c;但在算…

Flink State 和 Fault Tolerance详解

有状态操作或者操作算子在处理DataStream的元素或者事件的时候需要存储计算的中间状态&#xff0c;这就使得状态在整个Flink的精细化计算中有着非常重要的地位&#xff1a; 记录数据从某一个过去时间点到当前时间的状态信息。以每分钟/小时/天汇总事件时&#xff0c;状态将保留…

录屏功能怎么录声音?这些方法都能很好的录制

在现代社交媒体的时代&#xff0c;录制视频已成为了人们记录生活和分享经验的重要方式之一。而在录制视频的过程中&#xff0c;声音也是至关重要的一个方面。因此&#xff0c;在手机上使用录屏功能时&#xff0c;如何录制声音成为了一个常见的问题。本文将介绍如何在手机上使用…

Maven可选依赖和排除依赖简单使用

可选依赖 可选依赖指对外隐藏当前所依赖的资源 在maven_04_dao的pom.xml,在引入maven_03_pojo的时候&#xff0c;添加optional <dependency><groupId>com.rqz</groupId><artifactId>maven_03_pojo</artifactId><version>1.0-SNAPSHOT&…

python-Excel数据模型文档转为MySQL数据库建表语句(需要连接数据库)-工作小记

将指定Excel文档转为create table 建表语句。该脚本适用于单一且简单的建表语句 呈现效果 代码 # -*- coding:utf-8 -*- # Time : 2023/8/2 17:50 # Author: 水兵没月 # File : excel_2_mysql建表语句.py import reimport pandas as pd import mysql.connectordb 库名mydb m…

Excel·VBA表格横向、纵向相互转换

如图&#xff1a;对图中区域 A1:M6 横向表格&#xff0c;转换成区域 A1:C20 纵向表格&#xff0c;即 B:M 列转换成每2列一组按行写入&#xff0c;并删除空行。同理&#xff0c;反向操作就是纵向表格转换成横向表格 目录 横向转纵向实现方法1转换结果 实现方法2转换结果 纵向转横…

Socket层代码重构

src/utils新建socket.js import {io} from "socket.io-client"class Socket{constructor(){this.socket io("http://127.0.0.1:5000");}// 连接socketconnect(){this.socket.connect()}login(username,callback){// emit发送this.socket.emit(login,{&quo…