在线 OJ 项目(二) · 操作数据库 · 设计前后端交互的 API · 实现在线编译运行功能

一、操作数据库前的准备

设计数据库表

我们需要对数据库中存储的题目进行操作.

创建一个 “题目表” oj_table
题目的序号 id. 作为题目表的自增主键。
标题 title.
难度 level. 题目分为 “简单”,“中等”,“困难” 三种难度。
描述 description. 题目的基本描述,示例,提示等信息。
代码模板 templateCode. 给用户展示的初始代码,用户要在此代码模板上开发。
测试用例 testCode. 一组测试的代码,判断用户的代码是否正确。

create database if not exists oj_database;

use oj_database;

drop table if exists oj_table;
create table oj_table(
    id int primary key auto_increment,
    title varchar(50),
    level varchar(50),
    description varchar(4096),
    templateCode varchar(4096),
    testCode varchar(4096)
);

封装数据库操作 DBUtil

public class DBUtil {
    private static final String URL = "jdbc:mysql://127.0.0.1:3306/oj_database?characterEncoding=utf8%useSSL=false";
    // 自己电脑上的 MySQL 账户密码
    private static final String USERNAME = "root";
    private static final String PASSWORD = "root";

    //懒汉式
    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();
            }
        }
    }
}

存储题目实体类的Problem

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 + '\'' +
                '}';
    }
}

二、封装操作数据库数据的相关操作

ProblemDAO

// 通过这个类来实现题目的增删改查
// 1. 新增题目
// 2. 删除题目
// 3. 查询题目列表
// 4. 查询题目详情
public class ProblemDAO {

    // 1. 新增题目
    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, ?, ?, ?, ?, ?)";
            // 3. 动态替换
            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());
            // 4. 执行 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);
        }
    }

    // 2. 删除题目
    public void delete(int id) {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            connection = DBUtil.getConnection();
            String sql = "delete from oj_table where id = ?";
            statement = connection.prepareStatement(sql);
            statement.setInt(1, id);
            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);
        }
    }

    // 3. 查询题目
    public List<Problem> selectAll() {
        List<Problem> problems = new ArrayList<>();
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            connection = DBUtil.getConnection();
            String sql = "select id, title, level from oj_table";
            statement = connection.prepareStatement(sql);
            resultSet = statement.executeQuery();
            while (resultSet.next()) {
                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 e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, resultSet);
        }
        return null;
    }

    // 4. 查询题目详情
    public Problem selectOne(int id) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            connection = DBUtil.getConnection();
            String sql = "select * from oj_table where id = ?";
            statement = connection.prepareStatement(sql);
            statement.setInt(1, id);
            resultSet = statement.executeQuery();
            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 e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, resultSet);
        }
        return null;
    }
}

单元测试

测试功能是否有问题.

    // 单元测试
    private static void testInsert() {
        ProblemDAO problemDAO = new ProblemDAO();
        Problem problem = new Problem();
        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" +
                " \n" +
                "\n" +
                "进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗?\n" +
                "\n" +
                "来源:力扣(LeetCode)\n" +
                "链接:https://leetcode.cn/problems/two-sum\n" +
                "著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。");
        problem.setTemplateCode("class Solution {\n" +
                "    public int[] twoSum(int[] nums, int target) {\n" +
                "\n" +
                "    }\n" +
                "}");
        problem.setTestCode("    // 这个 main 方法就相当于测试用例的代码.\n" +
                "    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" +
                "    }");
        problemDAO.insert(problem);
    }


    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(2);
        System.out.println(problem);
    }


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


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

三、设计前后端交互的 API

已经把数据库的相关操作封装好了。

接下来可以设计服务器提供的 API,一些 HTTP 风格的接口,通过这些接口和网页前端进行交互。

需要设计哪些网页?

a)题目列表页。功能是展示当前题目的列表。方法:向服务器发送请求,题目的列表。

b)题目详情页。
功能一:展示题目的详细要求。方法:向服务器请求,获取指定题目的详细信息。
功能二:能够有一个代码编辑框,让用户来编写代码。此过程不需要和服务器交互,前端实现。
功能三:有提交按钮,点击提交按钮,就能把用户编辑的代码发送到服务器上,进行编译和运行,最后返回结果。方法:向服务器发送用户当前编写的代码,并获取到结果。

约定 API

目前比较流行的前后端交互的方式,主要是通过 JSON 格式来组织的。我们可以引入第三方库来帮忙解析 JSON 格式,会方便很多。

Jackson 依赖导入

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.13.0</version>
</dependency>

具体设计以下几个前后端交互的 API

向服务器请求,题目的列表.
请求:GET /problem
响应:[{id:1, title:“两数之和”, level:“简单”,}, {id:2, title:“两数相加”, level:“简单”,}]

向服务器请求,获取指定题目的详细信息.
请求:GET /problem?id=1
响应:{id:1, title:“两数之和”, level:“简单”, description:“题目的详细要求…”, templateCode:“代码模板”, testCode:" ",}

向服务器发送用户当前编写的代码,并且获取到结果.
请求:POST /compile {id:1, code:“编辑框的代码…”}
响应:{error:0, reason:“出错的详细原因”, stdout:“测试用例的输出情况,包含了通过几个用户这样的信息”}

编写获取题目列表和题目详细信息的功能

@WebServlet("/problem")
public class ProblemServlet extends HttpServlet {
    // json 的核心类
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 设置状态码和格式
        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();
            // 将 problems 进行转换成 json 结构的字符串
            String respString = objectMapper.writeValueAsString(problems);
            // 将得到的字符串响应回去-设置 HTTP 响应的 body 部分
            resp.getWriter().write(respString);
        } else {
            // 获取到了题目的 id,查询题目详情
            Problem problem = problemDAO.selectOne(Integer.parseInt(idString));
            String respString = objectMapper.writeValueAsString(problem);
            resp.getWriter().write(respString);
        }
    }
}

然后配置并启动 Tomcat,我们来测试是否能接收到请求。

能够通过网页请求到数据。

通过 Postman 也能够显示数据。
在这里插入图片描述

由于代码中通过 if 区分两种 API,所以我们可以尝试获取前端请求的 id 来测试是否能够获取到数据。

查找 id 为 1 的题目,显示 null。

通过 Postman 查询 id 为 4 的题目,能够显示。

在这里插入图片描述

很显然可以获取到数据,接下来我们实现在线编译运行功能。


四、实现在线编译运行功能

用户提交的代码,只是一个 Solution 这样的类,里面包含了一个核心方法。而要想编译运行,还需要一个 main 方法。main 方法在测试用例代码中,测试用例代码就在数据库中。

当前编译运行,请求和响应都是 JSON 格式的数据。为了方便解析和构造,就可以创建两个类,来对应这两个 JSON 结构。

    static class CompileRequest {
        public int id;
        public String code;
    }

    static class CompileResponse {
        // 0 表示没问题,1 表示编译出错,2 表示运行异常,3 表示其它错误
        public int error;
        public String reason;
        public String stdout;
    }

这两个类可以写在 CompileServlet 类中.

@WebServlet("/compile")
public class CompileServlet extends HttpServlet {

    static class CompileRequest {
        public int id;
        public String code;
    }

    static class CompileResponse {
        // 0 表示没问题,1 表示编译出错,2 表示运行异常,3 表示其它错误
        public int error;
        public String stdout;
        public String reason;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
        // 1. 读取请求的正文
        String body = readBody(req);

        // 2. 根据 id 从数据库中查找到题目的详情 - 得到测试用例代码

        // 3. 把用户提交的代码和测试用例代码,拼接成一个完整的代码

        // 4. 创建一个 Task 实例,调用里面的 compileAndRun 来解析编译运行

        // 5. 根据 Task 运行的结果,包装成一个 HTTP 响应
    }

我们要实现编译运行功能,需要经过以下几个步骤:

  1. 读取请求的正文
  2. 根据 id 从数据库中查找到题目的详情 - 得到测试用例代码
  3. 把用户提交的代码和测试用例代码,拼接成一个完整的代码
  4. 创建一个 Task 实例,调用里面的 compileAndRun 来解析编译运行
  5. 根据 Task 运行的结果,包装成一个 HTTP 响应

先看第一步,读取到请求的正文

我们使用个方法 readBody,封装一下获取请求正文的操作。

//获取请求头中的内容,转换成字符串类型
    private static String readBody(HttpServletRequest req) throws UnsupportedEncodingException {
        // 1. 根据请求头里面的 ContentLength 获取到 body 的长度
        int contentLength = req.getContentLength();

        // 2. 按照这个长度准备好一个 byte[]
        byte[] buffer = new byte[contentLength];

        // 3. 通过 req 里面的方法,获取到 body 的流对象
        try(InputStream inputStream = req.getInputStream()) {

            // 4. 基于这个流对象,读取内容,然后把内容放到 byte[] 数字中即可
            inputStream.read(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 5. 把这个 byte[] 的内容构造成一个 String,同时设置转换字符集格式
        return new String(buffer, "utf8");
    }
return new String(buffer, "utf8");

这段代码,相当于把一个二进制数据,转换成一个文本数据。
把 byte[] (以字节为单位),转换成 String (以字符为单位)。

而后续的 "utf8" 是为了在转换的过程中指定字符集,告诉编码方式。
从请求中读取的 byte[] 不清楚是哪种格式,需要在构造 String 的时候告诉 String,当前的 byte[] 是按照啥样的格式来编码。

补充完 readBody 方法,我们继续

package api;

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

import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
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;

/**
 * Created by cc
 * Description:
 * User: CZH
 * Date: 2023-01-29
 * Time: 14:43
 */
@WebServlet("/compile")
public class CompileServlet extends HttpServlet {

    static class CompileRequest {
        public int id;
        public String code;
    }
    static class CompileResponse {
        // 0 表示没问题,1 表示编译出错,2 表示运行异常,3 表示其它错误
        public int error;
        public String stdout;
        public String reason;
    }

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1. 读取请求的正文,别按照 JSON 格式解析
        String body = readBody(req);
        // 类对象,获取类的信息
        CompileRequest compileRequest = objectMapper.readValue(body, CompileRequest.class);
        // 2. 根据 id 从数据库中查找到题目的详情 - 得到测试用例代码
       
        // 3. 把用户提交的代码和测试用例代码,拼接成一个完整的代码
      
        // 4. 创建一个 Task 实例,调用里面的 compileAndRun 来解析编译运行
   
        // 5. 根据 Task 运行的结果,包装成一个 HTTP 响应
    }

    // 通过请求头获取数据,转换成String 返回
    private static String readBody(HttpServletRequest req) throws UnsupportedEncodingException {
        int contentLength = req.getContentLength();
        byte[] bytes = new byte[contentLength];
        try(InputStream inputStream = req.getInputStream()) {
            inputStream.read(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new String(bytes, "utf8");
    }
}

这段代码,就是根据类对象 CompileRequest.class,获取到 CompileRequest 这个类都有哪些属性,叫什么名字,依次遍历这些属性。

例如拿到 id 这个属性,就去 json 字符串中找 key 为 id 的键值对。发现 value 是 2,于是就把 2 赋值到 new 出来的 CompileRequest 的 id 字段中。

完成步骤二的代码.
根据 id 从数据库中查找到题目的详情,从而得到测试用例代码。

      // 2. 根据 id 从数据库中查找到题目的详情 - 得到测试用例代码
        ProblemDAO problemDAO = new ProblemDAO();
        Problem problem = problemDAO.selectOne(compileRequest.id);
        // testCode 是测试用例的代码
        String testCode = problem.getTestCode();
        // requestCode 是用户提交的代码
        String requestCode = compileRequest.code;

完成步骤三的代码.
把用户提交的代码和测试用例代码,拼接成一个完整的代码。

拼接的思路呢,其实就是把 testCode 的这个 main 方法,嵌入到 requestCode 里面,做法就是把 testCode 放到 Solution 的最后一个 } 的前面即可~

    // 3. 把用户提交的代码和测试用例代码,拼接成一个完整的代码
    String finalCode = mergeCode(requestCode, testCode);
    
    // 拼接代码
    private static String mergeCode(String requestCode, String testCode) {
        // 1. 查找 requestCode 最后一个 }
        int pos = requestCode.lastIndexOf("}");
        if (pos == -1) {
            return null;
        }
        // 2. 截取字符串
        String substring = requestCode.substring(0, pos);
        // 3. 拼接字符串并返回
        return substring + testCode + "\n}";
    }

到这里我们测试一波~

目前看没什么问题,继续…

完成步骤四代码.
创建一个 Task 实例,调用里面的 compileAndRun 来解析编译运行.

        // 4. 创建一个 Task 实例,调用里面的 compileAndRun 来解析编译运行
        Task task = new Task();
        Question question = new Question();
        question.setCode(finalCode);
        Answer answer = task.compileAndRun(question);

完成步骤五代码.
根据 Task 运行的结果,包装成一个 HTTP 响应.

// 5. 根据 Task 运行的结果,包装成一个 HTTP 响应
        CompileResponse compileResponse = new CompileResponse();
        compileResponse.error = answer.getError();
        compileResponse.reason = answer.getReason();
        compileResponse.stdout = answer.getStdout();
        String respString = objectMapper.writeValueAsString(compileResponse);
        resp.getWriter().write(respString);

进行测试~

能够得到数据,没问题~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值