简单读懂一个在线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. 从数据库中读取题目信息
1.1 创建题目数据库(sql)
- 标题
- 难度
- 介绍
- 给定的代码
- 测试用户答案的代码
- 题目序号(自增主键)
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. 前端页面
根据上述逻辑
 自己设计即可






![[openCV]基于拟合中线的智能车巡线方案V3](https://img-blog.csdnimg.cn/6d6e0e96c2dc49d2b5af82306de736db.png)












