在线 OJ 项目(二) · 操作数据库 · 设计前后端交互的 API · 实现在线编译运行功能
- 一、操作数据库前的准备
- 二、封装操作数据库数据的相关操作
- 三、设计前后端交互的 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;}@Overridepublic 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. 构造 SQLString 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. 执行 SQLint 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();@Overrideprotected 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;}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) {// 1. 读取请求的正文String body = readBody(req);// 2. 根据 id 从数据库中查找到题目的详情 - 得到测试用例代码// 3. 把用户提交的代码和测试用例代码,拼接成一个完整的代码// 4. 创建一个 Task 实例,调用里面的 compileAndRun 来解析编译运行// 5. 根据 Task 运行的结果,包装成一个 HTTP 响应}
我们要实现编译运行功能,需要经过以下几个步骤:
- 读取请求的正文
- 根据 id 从数据库中查找到题目的详情 - 得到测试用例代码
- 把用户提交的代码和测试用例代码,拼接成一个完整的代码
- 创建一个 Task 实例,调用里面的 compileAndRun 来解析编译运行
- 根据 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();@Overrideprotected 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);
进行测试~
能够得到数据,没问题~
在线 OJ 项目(二) · 操作数据库 · 设计前后端交互的 API · 实现在线编译运行功能相关推荐
- 数据库与身份认证(数据库的基本概念,安装并配置 MySQL,MySQL 的基本使用,在项目中操作 MySQL,前后端的身份认证)
theme: channing-cyan 数据库与身份认证 1. 数据库的基本概念 1.1 什么是数据库 数据库(database)是用来组织.存储和管理数据的仓库. 当今世界是一个充满着数据的互联网 ...
- 【项目实训6】前后端交互的格式以及规则
前后端交互的方式有多种,在经过讨论以后选择了socket进行传递数据,数据格式为json.由于c++并不能直接操作json格式,需要通过jsoncpp第三方库进行操作. jsoncpp使用也很方便,下 ...
- 在线 OJ 项目(三) · 处理项目异常 · UUID · 校验代码的安全性 · 阶段性总结
一.处理异常 二.区分不同请求的工作目录 UUID 对 Task 类进行重构 三.校验代码的安全性 四.阶段性总结 书接上回,我们自己测试没问题,是因为使用了正常数据:万一用户输入的是非法的请求,该咋 ...
- 在线OJ项目(3)------实现接口与网页前端进行交互
我们先想一下:我们要具体进行设计那些网页呢?有几个页面?都是干啥的?如何设计前后端交互的接口? 当前我们已经把数据库的相关操作给封装好了,接下来我们可以进行设计一些API,也就是HTTP风格的接口,通 ...
- python前后端交互_Django基础之简单的前后端交互
Python Python开发 Python语言 Django基础之简单的前后端交互 学习Django有一段时间了,最近刚好写了一个小项目,用到了前后端交互,刚开始写前后端交互确实很让人头晕目眩呢,下 ...
- 在线 OJ 项目(四) · 前端设计与项目总结
一.页面设计 题目列表页 题目详情页 二.获取到后台数据 实现思路 遇到换行问题 小结 引入 ace.js 三.项目总结 接下来将实现 online-oj 项目的前端界面. 先随便从各大网站上下载网页 ...
- 天猫整站SSM项目(二)数据库设计
天猫整站SSM项目(二)数据库设计 一.数据库设计分析 二.创建数据库 1. 建立数据库 2. 表与表之间的关系 2.1 表![在这里插入图片描述](https://img-blog.csdnimg. ...
- 项目小结之数据库设计
最近做了一个小项目完整的数据库设计,想总结一些设计上的所得,希望大家多多指教. 有时一个项目,普通程序员一般不会去接触数据库设计,一般都有专业的DBA或是老程序员去设计,下面是我推测的几点可能原因: ...
- 数据库设计(二)——数据库设计原则
一.数据库表的设计原则 1.不应该针对整个系统进行数据库设计,而应该根据系统架构中的组件划分,针对每个组件所处理的业务进行组件单元的数据库设计:不同组件间所对应的数据库表之间的关联应尽可能减少,如果不 ...
最新文章
- 什么是采样层(pooling)
- vc++实现无进程无DLL无硬盘文件无启动项的ICMP后门后门程序
- 用C语言实现扫雷小游戏(附上思路+项目展示+源代码)
- java和python的比较-如何对比Python和Java,只需三分钟告诉你!
- mybaits十九:bind绑定
- 深入浅出浏览器渲染原理
- 当今世界最受人们重视的十大经典算法
- 详解ScheduledExecutorService的周期性执行方法
- 算法训练 最大的算式(动态规划)
- 关于wParam和lParam
- 追本溯源 —— 汉语词汇含义的演化
- Lucene6.5.0 下中文分词IKAnalyzer编译和使用
- input和textarea的区别
- 学习笔记之 prim算法和kruskal算法
- 云计算(1)---基础知识
- oracle select输出dbms,PLSQL 中如何使用 dbms_output 输出结果
- fdisk添加分区引起的Linux Error: 22: Invalid argument
- 音质好的linux主机,实测:ASIO 的音质更好?
- 51单片机温控风扇仿真原理图 C语言程序,51单片机温控风扇仿真原理图+C语言程序与实物制作...
- 计算机键盘的型号,标准电脑键盘尺寸