一.效果展示
题目列表页
题目详情页
二.项目的组成
此在线OJ项目由两个页面组成,主要实现:
题目的列表展示页(题目ID,题目标题,题目描述)
题目详情页展示(题目ID,题目标题,题目描述,题目编辑框,题目提交,编辑运行结果)
三.项目准备
1.创建项目
创建一个Maven项目,并引入相关依赖(servlet ,mysql)
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
在main创建目录webapp,在webapp创建目录WeB-INF,在WEB-INF创建文件web.xml
编写web.xml代码
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>
2.操作文件的读写,通过IO流,相关类实现
InputStream 只是一个抽象类,要使用还需要具体的实现类。
字节流(以字节为基本单位,适用于二进制文件)
FileinputStream 读数据:把数据从从硬盘读取到内存中
FileOutputstream 写数据:把数据写入文件中
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
//把一个文件内容写入到另一个文件中去(文件拷贝)
public class TestFile {
public static void main(String[] args) throws IOException {
//先指定初始文件的路径
//起始文件路径
String srcpath="d:\\test1.txt";
//结束文件路径
String destpath="d:\\test2.txt";
//先读写文件之前打开文件夹
FileInputStream fileInputStream=new FileInputStream(srcpath);
//打开结束文件夹
FileOutputStream fileOutputStream=new FileOutputStream(destpath);
//写操作,把第一个文件的内容循环写入第二个文件夹里面
while (true){
int ch=fileInputStream.read();
if (ch==-1){
//文件读取完毕
break;
}
fileOutputStream.write(ch);
}
//读写操作完成后需要关闭文件夹
fileInputStream.close();
fileOutputStream.close();
}
}
注意:read方法一次返回一个字节,用int来接收,主要是因为:(1)在java中不存在无符号类型的,byte这样的类型也还是有符号的 (-128~127),按字节读取数据,并不需要将这些数据进行算术运算,所以正负无意义的。(2)read读取完毕之后需要返回-1,也就是EOF,来确定文件已经读取完毕,所以要使用int 来接收
字符流(以字符为基本单位,适用于文本文件)
FileReader FileWriter
3.多进程编程
1.进程:(任务管理器)一个跑起来的程序,就是一个进程 ,进程是操作系统资源分配的基本单位
- 描述:使用结构体(PCB)来进行描述 (pid进程身份标识符,内存指针:进程有哪些资源,文件描述符表)
- 组织:通过双向链表把PCB串在一起(创建一个进程,本质上就是创建一个PCB这样的结构体对象,把它插入到我们的链表中,任务管理器可以查看这个进程列表,本质上就是遍历这个PCB链表)
- 进程之间相互独立
2.多进程
- 并发编程实现的重要方式
- 操作系统本身就是按照多进程方式进行工作的(并发)
3.线程:包含在进程中,一个进程默认有一个线程,也可以有多个线程 ,每一个线程都是一个执行流,可以单独在CPU上进行调度。同一个进程中的这些线程公用一份资源(内存+文件),把线程叫做轻量级进程,创建和销毁线程开销比较小。
4.多线程和多进程的优缺点
- 多线程:充分利用多核CPU,提高效率 ; 只是在创建第一个线程的时候需要申请资源,后续再创建新的线程就不需要; 缺点:互相进行干扰,线程数目也不是越多越好,CPU核心数目有限,可能会影响线程不安全问题
- 多进程:不进行互相干扰
- 进程是资源管理的基本单位,线程是调度执行的基本单位
在线OJ有一个服务器进程,运行着Servlet,接收用户请求,返回响应(所有的用户都可以登录做题,用户之间不影响)
用户提交代码,是一个独立的逻辑----------用多进程的方式 ,因为无法控制用户提交代码的正确性,如果使用多线程,之间会互相影响
使用多进程编程
5.操作系统给用户提供了很多和多进程编程相关的接口,用户只需要创建进程和进程等待即可
- 创建进程(创建出一个新进程来执行一系列任务,被创建出进程叫做子进程,父进程:服务器进程)
Runtime.exec(参数是一个字符串,表示一个可执行程序路径,执行这个方法,就会把指定路径的可执行程序,创建出进程并执行)
- 进程等待(父子进程是并发执行的,希望父进程等待子进程执行完毕后,在执行后续代码,在线OJ让用户提交代码,编译执行代码之后再把响应返回给用户,父进程执行到waitFore就会等待阻塞,一直阻塞到子进程执行完毕为止)
6.javac是控制台程序,它的输出是输出到“标准输出和标准错误”这两个特殊符号文件中,要想1看到程序运行效果,就需要获取到标准输出和标准错误内容
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
//程序已经开始运行,只需要标准输出,和标准错误
public class Commandutil {
public static int run(String cmd,String stdoutFile,String stderrFile){
try {
//通过Runtime获取到runtime对象,这是一个单例模式
Process process=Runtime.getRuntime().exec(cmd);
//对标准输出进行重定向,文件有内容
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();
}
//对标准错误进行重定向
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();
}
//进程等待
try {
int exitCode= process.waitFor();
return exitCode;
} catch (InterruptedException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
return -1;
}
public static void main(String[] args) {
Commandutil.run("javac","./stdout.txt","./stderr.txt");
}
}
4.编译运行设计
1.设计Question
public class Question {
//用户提交代码
private String code;
}
2.代码提交后响应
public class Answer {
//服务器返回响应
private int error;
private String reason;
private String stdout;
private String stderr;
}
3.对读写文件进一步封装(要进程间通信,就需要很多读写文件操作,一个负责读整个文件内容,返回字符串,一个负责将字符串写入文件中,这些读取文件都是文本文件,所以使用字符流更合适)
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileUtil {
//一次读取整个文件
public static String readFile(String filePath) throws IOException {
StringBuilder stringBuilder=new StringBuilder();
try (FileInputStream fileInputStream=new FileInputStream(filePath)){
int ch=fileInputStream.read();
while (true){
if (ch==-1){
break;
}
//一定要把ch转成char在append
stringBuilder.append((char) ch);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
//返回字符串
return stringBuilder.toString();
}
//一次写入整个文件
public static void writeFile(String filepath,String content) {
try(FileOutputStream fileOutputStream=new FileOutputStream(filepath)) {
fileOutputStream.write(content.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
String是不可变对象,要修改必须要创建一个新对象,再把内容拷贝过去,所以用Stringbuilder,多个线程同时修改同一个变量,线程安全
4.Task进行编译运行过程展示 (输入:用户提交的代码,输出:程序的编译和运行结果,参数:要编译的java源代码,返回值:编译运行结果,编译需要.java文件,编译出错,javac就会把错误信息写入到stderr中)
- 临时文件
//0.准备好存放临时文件的目录
private final String WORK_DIR="./tmp/";
//编译代码类名
private final String CLASS="Solution";
//代码
private final String CODE=WORK_DIR+"Solution.java";
//输出错误
private final String STDOUT=WORK_DIR+"stdout.txt";
//标准错误
private final String STDERR=WORK_DIR+"stderr.txt";
//编译错误
private final String COMPILE_ERROR=WORK_DIR+"compile_error.txt";
为什么需要这么多临时文件:为了进程间通信,javac子进程分开编译和运行代码时,需要知道要编译和要运行的代码是什么。
- 编译只关注编译错误放在哪个文件(error=0 Ok =1编译出错 =2运行出错(抛异常)
public Answer compileAndrun(Question question) throws IOException {
Answer answer=new Answer();
//0.准备好存放临时文件的目录
File workDir=new File(WORK_DIR);
if (!workDir.exists()){
//创建多级目录
workDir.mkdirs();
}
//1.把question中的code写入.java文件中
FileUtil.writeFile(CODE,question.getCode());
//2.创建子进程,调用javac编译,编译需要一个.java文件,如果编译出错,会写入标准错误信息文件,
//构造编译命令
String compileCmd=String.format("javac-ending utf8 %s -d %s",CODE,WORK_DIR);
System.out.println("编译命令: "+compileCmd);
//只关注标准错误放哪个文件
Commandutil.run(compileCmd,null,COMPILE_ERROR);
//如果编译出错,记录这个文件里面
String compileError=FileUtil.readFile(COMPILE_ERROR);
if (!compileError.equals("")){
//有错误信息
System.out.println("编译出错");
answer.setError(1);
answer.setReason(compileError);
return answer;
}
- 编译运行关注输出错误和标准错误
//编译正确,继续运行
//3.创建子进程调用java命令去执行运行程序,关注标准输出,和标准错误
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;
}
- 将运行结构包装到最终Answer对象中,编译执行结果,通过文件获取
answer.setError(0);
answer.setStdout(FileUtil.readFile(STDOUT));
return answer;
5. 题目管理模块设计
1.创建数据库和数据表
create database oj_databases charset utf8;
use oj_databases;
create table oj_table;
//题目id
id int primary key auto_increment(自增主键)
//标题
title varchar(50)
//题目难度
level varchar(50)
//题目描述
descripation varchar(50)
//测试代码
testCode varchar(50)
//编辑器代码
templateCode varchar(50)
2.封装数据库(建立连接和关闭连接)
package common;
import com.mysql.cj.jdbc.MysqlDataSource;
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 = "20010211";
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();
}
}
}
}
数据库的一些操作,一会细细看一下
3.题目表的操作(一个实体类对象对应表中一条记录)Problem
//创建一个实体类
public class Problem {
private int id;
private String title;
private String level;
private String desription;
private String templateCode;
private String testCode;
4. 负责题目的管理
- 增加题目信息
// 插入题目信息
public void insert(Problem problem) {
// 1. 获取数据库连接
Connection connection = DBUtil.getConnection();
PreparedStatement statement = null;
String sql = "insert into oj_table values(null, ?, ?, ?, ?, ?)";
try {
// 2. 拼装 SQL 语句
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());
System.out.println(statement);
// 3. 执行 SQL 语句
statement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(connection, statement, null);
}
}
- 查找所有信息(题目列表页)
public List<Problem> selectAll() {
ArrayList<Problem> problems = new ArrayList<>();
// 1. 获取数据库连接
Connection connection = DBUtil.getConnection();
PreparedStatement statement = null;
ResultSet resultSet = null;
String sql = "select id,title,level from oj_table";
try {
// 2. 拼装 SQL 语句
statement = connection.prepareStatement(sql);
// 3. 执行 SQL 语句
resultSet = statement.executeQuery();
// 4. 遍历查询出的结果
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);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(connection, statement, resultSet);
}
return problems;
}
- 根据题目id,查找详情页信息
public Problem selectOne(int problemId) {
// 1. 获取数据库连接
Connection connection = DBUtil.getConnection();
PreparedStatement statement = null;
ResultSet resultSet = null;
String sql = "select * from oj_table where id = ?";
try {
// 2. 拼装 SQL 语句
statement = connection.prepareStatement(sql);
statement.setInt(1, problemId);
// 3. 执行 SQL 语句
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;
}
- 删除题目信息
//删除
public void delete(int id){
Connection connection=null;
PreparedStatement statement=null;
try {
//1.和数据库建立连接
connection=DBUtil.getConnection();
//构造sql语句
String sql="delete from oj_table where id=?";
statement= connection.prepareStatement(sql);
//sql动态替换
statement.setInt(1,id);
//执行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);
}
}
- 添加测试用例代码
private static void testInsert() {
Problem problem = new Problem();
problem.setId(1);
problem.setTitle("两数之和");
problem.setLevel("简单");
problem.setDesription("给定一个整数数组 nums 和一个整数目标值 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 <= 103\n" +
"-109 <= nums[i] <= 109\n" +
"-109 <= target <= 109\n" +
"只会存在一个有效答案\n" +
"\n" +
"来源:力扣(LeetCode)\n" +
"链接:https://leetcode-cn.com/problems/two-sum\n" +
"著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。");
problem.setTemplateCode(" public int[] twoSum(int[] nums, int target) {\n" +
" \n" +
" }");
problem.setTestCode(" public static void main(String[] args) {\n" +
" Solution solution = new Solution();\n" +
" int[] arr = {2,7,11,15};\n" +
" int target = 9;\n" +
" int[] result = solution.twoSum(arr, 9);\n" +
" if (result.length == 2 && result[0] == 1 && result[1] == 2) {\n" +
" System.out.println(\"TestCase OK!\");\n" +
" } else {\n" +
" System.out.println(\"TestCase Failed! arr: {2, 7, 11, 15}, target: 9\");\n" +
" }\n" +
"\n" +
" int[] arr2 = {3,2,4};\n" +
" int target2 = 6;\n" +
" int[] result2 = solution.twoSum(arr2, target2);\n" +
" if (result2.length == 2 && result2[0] == 1 && result2[1] == 2) {\n" +
" System.out.println(\"TestCaseOK!\");\n" +
" } else {\n" +
" System.out.println(\"TestCaseFailed!\");\n" +
" }\n" +
" }\n");
ProblemDAO problemDAO = new ProblemDAO();
problemDAO.insert(problem);
}
测试用例代码就是一个main函数,在这个方法里面会创建Soulation实例,并且调用里面操作方法,调入方法时传入不同参数,并且针对返回结果进行判定。如果返回结果符合预期,就打印(Tset OK),不符合则打印(Test Filed)
服务器会收到用户提交的Soulation类完整代码,然后从数据库找到相对应的测试用例代码(main方法),就把这个两个代码进行拼接,此时Soulation就有main函数,可以单独1编译和执行。
6.设计前后端交互的API
通过Json格式,引入第三方库来实现前后端交互API
- 第一个API,向服务器请求,题目列表
请求:GET/problem
响应:【
{
id:1,
title:"两数之和“,
level:"简单”
},
{
id:2,
title:"两数之积“,
level:"简单”
}
...............
】
- 第二个API,向服务器请求,获取指定题目详情信息
请求:GET/problem?id=1
响应:{
id=1,
title:"两数之和“,
level:"简单”,
description:"......................",
templateCode:"代码模板“
}
- 第三个API,向服务器发送当前的编码,并且获取到结果 (编译)
GET需要把代码放入到URL中,通过query string来发送请求(需要对代码中字符进行urlencode)
POST需要把代码放入body中
请求:POST/compile
{
id=1,
code:"..........",
}
服务器要根据id从数据库中拿到1测试用例代码和用户提交代码进行拼接
响应:
{
error: 0 (0表示编译运行成功,1表示编译出错,2表示运行出错)
reason:"出错原因”
staout:“测试用例输出情况,包含通过几个用例信息”
}
第一二个API
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 {
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");
//尝试获取id参数,能获取到,则是获取题目详情,反之则获取题目列表
String idString=req.getParameter("id");
ProblemDAO problemDAO=new ProblemDAO();
if (idString==null||"".equals(idString)){
//没有获取到id,是题目列表页
//查询所有题目列表
List<Problem> problems= problemDAO.selectAll();
String respString=objectMapper.writeValueAsString(problems);
//响应字符串写入,设置http响应中的body部分
//http协议抱头中就要求Content-length来描述body长度(自动设置的),Content-type来描述body类型(需要手动去设置)
resp.getWriter().write(respString);
}else {
//获取到题目id,查询题目详情
Problem problem=problemDAO.selectOne(Integer.parseInt(idString));
String respString=objectMapper.writeValueAsString(problem);
resp.getWriter().write(respString);
}
}
}
第三个API
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当前内部类实例就不依赖外部类实例,如果不加就需要先创建外部类实例,再加内部类实例
static class CompileRequest{
public int id;
public String code;
}
static class CompileResponse{
//此处约定error为0表示ok,1编译错误,3其他错误 2运行错误
public int error;
public String reason;
public String stdout;
}
private ObjectMapper objectMapper=new ObjectMapper();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
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);
//如果找不到id,处理异常
if (problem==null){
//为了统一处理错误,抛出异常
throw new ProblemNotFoundException();
}
//测试用力代码
String testCode=problem.getTestCode();
//用户提交代码
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();
String respString=objectMapper.writeValueAsString(compileResponse);
resp.getWriter().write(respString);
} catch (ProblemNotFoundException e) {
//处理id题目没有找到异常
compileResponse.error=3;
compileResponse.reason="没有找到指定题目! id="+compileRequest.id;
String respString=objectMapper.writeValueAsString(compileResponse);
resp.getWriter().write(respString);
}catch (CodeInValidException e){
compileResponse.error=3;
compileResponse.reason="提交代码不符合要求!";
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. 把 testCode 拼接到后面, 并再拼接上一个 } 就好了
return requestCode.substring(0, pos) + testCode + "}";
}
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");
}
}
测试用例和提价代码拼接(将testCode中的main方法嵌入到RequestCode里面,把testCode放到Soulation最后一个}前面即可)
- 先在requestCode查找最后一个}前面即可
- 根据刚才查找的结果进行字符串截取
- 把刚才截取的结果拼接上测试用例代码+拼接}
7.进行优化
- 因为每次有一个请求就需要生成一组临时文件,如果同一时刻有n个请求一起,这些请求的临时文件和目录都是一样的,此时多个请求之间会出现相互干扰的情况(类似于线程安全的问题)
办法:让每一个请求有自己的目录去生成这些临时文件,使用其他唯一ID来作为目录名字
每个请求都生成唯一的UUID,进一步创建一个以UUID命名的临时目录,请求生成临时文件放到 临时目录即可。
package compile;
import common.FileUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
//每次编译和运行过程称为一个Task
public class Task {
//通过一组常量来约定临时文件的名字
// 存放临时文件的目录.
private String WORK_DIR = null;
// 编译代码的类名
private String CLASS = null;
// 编译代码的文件名
private String CODE = null;
//运行时标准输出的文件名
private String STDOUT = null;
//运行时标准错误的文件名
private String STDERR = null;
//编译错误信息文件名
private String COMPILE_ERROR = null;
public Task(){
WORK_DIR="./tmp/"+ UUID.randomUUID().toString()+"/";
CLASS="Solution";
CODE=WORK_DIR + "Solution.java";
COMPILE_ERROR=WORK_DIR + "compile_error.txt";
STDOUT=WORK_DIR + "stdout.txt";
STDERR=WORK_DIR + "stderr.txt";
}
- 代码安全性问题(不能保证用户提交代码安全性,所以需要设置黑名单来处理安全问题(docker相当于轻量级虚拟机,每次用户提交代码都会分配一个docker容器,让用户提交的代码在容器里面执行,即使代码有恶意操作,也就是把docker容器损坏,对物理机无影响)
package compile;
import common.FileUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
//每次编译和运行过程称为一个Task
public class Task {
//通过一组常量来约定临时文件的名字
// 存放临时文件的目录.
private String WORK_DIR = null;
// 编译代码的类名
private String CLASS = null;
// 编译代码的文件名
private String CODE = null;
//运行时标准输出的文件名
private String STDOUT = null;
//运行时标准错误的文件名
private String STDERR = null;
//编译错误信息文件名
private String COMPILE_ERROR = null;
public Task(){
WORK_DIR="./tmp/"+ UUID.randomUUID().toString()+"/";
CLASS="Solution";
CODE=WORK_DIR + "Solution.java";
COMPILE_ERROR=WORK_DIR + "compile_error.txt";
STDOUT=WORK_DIR + "stdout.txt";
STDERR=WORK_DIR + "stderr.txt";
}
//这个Task提供核心方法就是编译和运行
//参数:编译运行的代码
//返回值:编译运行的结果
public Answer compileAndrun(Question question) {
Answer answer = new Answer();
//先准备好用来存放临时变量的目录
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写到一个.java文件中(类名和文件名是一致的,约定都是soulation)
FileUtil.writeFile(CODE, question.getCode());
// //2.创建子进程,调用javac编译,编译需要有一个.java文件,如果编译出错,javac就会把错误信息写道stderr里,可以用一个文件compliErro.txt进行保存
// //先把编译命令构造出来
String compileCmd=String.format("javac -encoding utf8 %s -d %s",CODE,WORK_DIR);
System.out.println("编译命令:"+compileCmd);
// //只关注标准错误放哪个文件
CommandUtil.run(compileCmd,null,COMPILE_ERROR);
// //如果编译出错,就会记录到这个文件里面,反之则为空
String compileError= FileUtil.readFile(COMPILE_ERROR);
if (!compileError.equals("")){
// //编译出错,返回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.父进程获取到编译的结果,打包到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){
//找到任意恶意代码特征,不安全
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" +
"}");
Answer answer= task.compileAndrun(question);
System.out.println(answer);
}
}
8.前端页面设计
1.题目列表页(展示当前都有哪些题目,点击题目可以跳转到详情页)
2.题目详情页(展示题目具体信息)
需要让页面通过ajax方式从服务器去获取数据
- 题目列表页(使用ajax与后端进行交互)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Charcoal - Free Bootstrap 4 UI Kit</title>
<meta name="description" content="Charcoal is a free Bootstrap 4 UI kit build by @attacomsian at Wired Dots." />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--Bootstrap 4-->
<link rel="stylesheet" href="css/bootstrap.min.css">
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark fixed-top sticky-navigation">
<a class="navbar-brand font-weight-bold" href="#">小加的OJ系统</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topMenu" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="topMenu">
</div>
</nav>
<!--hero section-->
<section class="bg-hero">
<div class="container">
<div class="row vh-100">
<div class="col-sm-12 my-auto text-center">
<h1>我的OJ系统</h1>
<p class="lead text-capitalize my-4">
基于 Java Sevlet搭建的在线 OJ 平台
</p>
<a href="https://gitee.com" class="btn btn-outline-light btn-radius btn-lg">项目链接</a>
</div>
</div>
</div>
</section>
<!--components-->
<section class="my-5 pt-5">
<div class="container">
<!-- Tables -->
<div class="row mb-5" id="tables">
<div class="col-sm-12">
<div class="mt-3 mb-5">
<h3>题目列表</h3>
<table class="table">
<thead class="thead-dark">
<tr>
<th>ID编号</th>
<th>题目标题</th>
<th>难度</th>
</tr>
</thead>
<tbody id="problemTable">
<!-- <tr>
<td>1</td>
<td>
<a href="#">两数之和</a>
</td>
<td>简单</td>
</tr> -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</section>
<!--footer-->
<section class="py-5 bg-dark">
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3 col-sm-8 offset-sm-2 col-xs-12 text-center">
<!-- <h3>Upgrade to Pro Version</h3>
<p class="pt-2">
We are working on <b>Charcoal Pro</b> which will be released soon. The pro version
will have a lot more components, sections, icons, plugins and example pages.
Join the waiting list to get notified when we release it (plus discount code).
</p>
<a class="btn btn-warning" href="https://wireddots.com/newsletter">Join Waiting List</a>
<hr class="my-5"/> -->
<p class="pt-2 text-muted">
© by 加率
</p>
</div>
</div>
</div>
</section>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js"></script>
<script src="js/app.js"></script>
<script>
//在页面加载时尝试从服务器获取题目列表,用ajax方式
function getProblems(){
//1.先通过ajax从服务区获取题目列表
$.ajax({
url:"problem",
type:"GET",
success:function(data,status){
//data是响应的body,status是响应状态码
//得到的响应数据构造成HTML片段
makeProblemTable(data);
}
})
}
function makeProblemTable(data){
//通过这个函数来吧数据转换成HTML页面片段
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>
</body>
</html>
- 题目详情页
先把题目列表页拷贝一份, 修改名字为 problemDetail.html
调整页面内容. 去掉表格了.
- 使用一个 jumbotron 表示题目详情
- 使用一个 textarea 表示代码编辑框
- 使用 button 表示提交按钮.
- 再使用一个 jumbotron 表示题目运行结果
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Charcoal - Free Bootstrap 4 UI Kit</title>
<meta name="description" content="Charcoal is a free Bootstrap 4 UI kit build by @attacomsian at Wired Dots." />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--Bootstrap 4-->
<link rel="stylesheet" href="css/bootstrap.min.css">
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark fixed-top sticky-navigation">
<a class="navbar-brand font-weight-bold" href="#">小加的OJ系统</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topMenu" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="topMenu">
</div>
</nav>
<!--hero section-->
<section class="bg-hero">
<div class="container">
<div class="row vh-100">
<div class="col-sm-12 my-auto text-center">
<h1>我的OJ系统</h1>
<p class="lead text-capitalize my-4">
基于 Java Sevlet搭建的在线 OJ 平台
</p>
<a href="https://gitee.com" class="btn btn-outline-light btn-radius btn-lg">项目链接</a>
</div>
</div>
</div>
</section>
<!--components-->
<section class="my-5 pt-5">
<div class="container">
<div class="row mt-4">
<div class="col-sm-12 pb-4">
<div class="jumbotron jumbotron-fluid">
<div class="container" id="problemDesc">
<!-- <h1>Container fluid size jumbotron</h1>
<p>Think BIG with a Bootstrap Jumbotron!</p> -->
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-sm-12 pb-4">
<div class="form-group">
<label for="codeEdit">代码编辑框</label>
<div id="editor" style="min-height:400px">
<textarea class="form-control" id="codeEdit" style="width: 100%; height:400 px;"></textarea>
</div>
</div>
</div>
</div>
<button type="button" class="btn btn-outline-primary" id="submitButton">提交</button>
<div class="row mt-4">
<div class="col-sm-12 pb-4">
<div class="jumbotron jumbotron-fluid">
<div class="container" >
<pre id="problemResult">
</pre>
<!-- <h1>Container fluid size jumbotron</h1>
<p>Think BIG with a Bootstrap Jumbotron!</p> -->
</div>
</div>
</div>
</div>
</section>
<!--footer-->
<section class="py-5 bg-dark">
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3 col-sm-8 offset-sm-2 col-xs-12 text-center">
<!-- <h3>Upgrade to Pro Version</h3>
<p class="pt-2">
We are working on <b>Charcoal Pro</b> which will be released soon. The pro version
will have a lot more components, sections, icons, plugins and example pages.
Join the waiting list to get notified when we release it (plus discount code).
</p>
<a class="btn btn-warning" href="https://wireddots.com/newsletter">Join Waiting List</a>
<hr class="my-5"/> -->
<p class="pt-2 text-muted">
© by 加率
</p>
</div>
</div>
</div>
</section>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js"></script>
<script src="js/app.js"></script>
<script src="https://cdn.bootcss.com/ace/1.2.9/ace.js"></script>
<script src="https://cdn.bootcss.com/ace/1.2.9/ext-language_tools.js"></script>
<script>
console.log(location.search);
function initAce() {
// 参数 editor 就对应到刚才在 html 里加的那个 div 的 id
let editor = ace.edit("editor");
editor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true
});
editor.setTheme("ace/theme/twilight");
editor.session.setMode("ace/mode/java");
editor.resize();
document.getElementById('editor').style.fontSize = '20px';
return editor;
}
let editor = initAce();
//通过ajax从服务器获取到题目详情
function getProblem(){
// 1.通过ajax给服务器发送一个请求
$.ajax({
url:"problem"+location.search,
type:"GET",
success:function(data,status){
makeProblemDetail(data);
}
})
}
function makeProblemDetail(problem){
//先获取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.desription;
pre.appendChild(p);
problemDesc.appendChild(pre);
//代码模板填写到编辑框
// let codeEdit =document.querySelector("#codeEdit");
// codeEdit.innerHTML=problem.templateCode;
editor.setValue(problem.templateCode);
//给提交按钮注册一个点击事件
let submitButton=document.querySelector("#submitButton");
submitButton.onclick=function(){
//点击这个按钮就需要进行提交
let body={
id:problem.id,
//code:codeEdit.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){
//编译运行没有问题
problemResult.innerHTML=data.stdout;
}else{
//编译运行有问题
problemResult.innerHTML=data.reason;
}
}
});
}
}
getProblem();
</script>
</body>
</html>
- 注意1:数据库的题目要求,换行都用\n来表示,HTML的换行是br标签(让服务器返回数据时,\n都替换成br;给页面标签里面套一层pre标签,pre标签可识别\n)
- 注意2:编辑框点击提交按钮,不能编译,引入ace.js,它是有前端的编辑器,会重新绘制页面(绘制div #editor,原来那个textarea就没有了,所以需要把代码模板填写到编辑框),还需要初始化编辑框
四.项目测试
1.对在线OJ项目设计相关测试用例
2.对在线OJ项目进行单元测试(单元测试:对程序最小单元进行测试)
-
新增一条数据记录
public void insert(Problem problem) {
try {
// 1. 获取数据库连接
connection = DBUtil.getConnection();
String sql = "insert into oj_table values(null, ?, ?, ?, ?, ?)";
// 2. 拼装 SQL 语句
statement = connection.prepareStatement(sql);
statement.setString(1, problem.getTitle());
statement.setString(2, problem.getLevel());
statement.setString(3, problem.getDesription());
statement.setString(4, problem.getTemplateCode());
statement.setString(5, problem.getTestCode());
System.out.println(statement);
// 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);
}
}
对新增功能进行验证
private static void testInsert() {
Problem problem = new Problem();
problem.setId(1);
problem.setTitle("两数之和");
problem.setLevel("简单");
problem.setDesription("给定一个整数数组 nums 和一个整数目标值 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 <= 103\n" +
"-109 <= nums[i] <= 109\n" +
"-109 <= target <= 109\n" +
"只会存在一个有效答案\n" +
"\n" +
"来源:力扣(LeetCode)\n" +
"链接:https://leetcode-cn.com/problems/two-sum\n" +
"著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。");
problem.setTemplateCode(" public int[] twoSum(int[] nums, int target) {\n" +
" \n" +
" }");
problem.setTestCode(" public static void main(String[] args) {\n" +
" Solution solution = new Solution();\n" +
" int[] arr = {2,7,11,15};\n" +
" int target = 9;\n" +
" int[] result = solution.twoSum(arr, 9);\n" +
" if (result.length == 2 && result[0] == 1 && result[1] == 2) {\n" +
" System.out.println(\"TestCase OK!\");\n" +
" } else {\n" +
" System.out.println(\"TestCase Failed! arr: {2, 7, 11, 15}, target: 9\");\n" +
" }\n" +
"\n" +
" int[] arr2 = {3,2,4};\n" +
" int target2 = 6;\n" +
" int[] result2 = solution.twoSum(arr2, target2);\n" +
" if (result2.length == 2 && result2[0] == 1 && result2[1] == 2) {\n" +
" System.out.println(\"TestCaseOK!\");\n" +
" } else {\n" +
" System.out.println(\"TestCaseFailed!\");\n" +
" }\n" +
" }\n");
ProblemDAO problemDAO = new ProblemDAO();
problemDAO.insert(problem);
}
验证:符合预期,运行结果显示新增成功,数据库也有相应的修改
-
删除一条题目信息
//删除
public void delete(int id){
Connection connection=null;
PreparedStatement statement=null;
try {
//1.和数据库建立连接
connection=DBUtil.getConnection();
//构造sql语句
String sql="delete from oj_table where id=?";
statement= connection.prepareStatement(sql);
//sql动态替换
statement.setInt(1,id);
//执行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);
}
}
对删除功能进行验证
private static void testSelectOne(){
ProblemDAO problemDAO=new ProblemDAO();
Problem problem=problemDAO.selectOne(1);
System.out.println(problem);
}
验证:符合预期,运行结果显示删除成功,数据库也有相应的修改
-
通过id查找其中一条详细信息
//根据id查找题目详情
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是主键,所以查询结果一定是唯一的,用if进行查找
if (resultSet.next()){
Problem problem=new Problem();
problem.setId(resultSet.getInt("id"));
problem.setTitle(resultSet.getString("title"));
problem.setLevel(resultSet.getString("level"));
problem.setDesription(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 testSelectOne(){
ProblemDAO problemDAO=new ProblemDAO();
Problem problem=problemDAO.selectOne(1);
System.out.println(problem);
}
验证:符合预期,运行结果显示题目详情信息,数据库也可以显示
-
查找所有题目
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);
//执行sql,可以获取每一行
resultSet= statement.executeQuery();
//遍历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;
}
验证:符合预期,数据库可以显示所新增的所有题目
3.对在线OJ项目进行自动化测试
-
先给pom.xml里面配置,将selenium依赖导入
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.0.0</version>
</dependency>
-
在test的java里面新建类进行对应操作
对首页和题目列表页进行自动化测试
public class FirstAutotest {
//题目列表页
public void dilirebaTest() throws InterruptedException {
EdgeOptions options = new EdgeOptions();
options.addArguments("--remote-allow-origins=*");
EdgeDriver driver = new EdgeDriver(options);
Thread.sleep(5000);
//打开网址
driver.get("http:127.0.0.1:8080/Java-OJ-2023/index.html");
Thread.sleep(5000);
//找到页面链接的地方并定位跳转
driver.findElement(By.xpath("/html/body/section[1]/div/div/div/a")).click();
Thread.sleep(5000);
driver.quit();
}
对题目详情页进行自动化测试
//题目详情页
public void detail() throws InterruptedException {
EdgeOptions options = new EdgeOptions();
options.addArguments("--remote-allow-origins=*");
EdgeDriver driver = new EdgeDriver(options);
Thread.sleep(5000);
//进入到列表页
driver.get("http:127.0.0.1:8080/Java-OJ-2023/index.html");
Thread.sleep(5000);
//找到标题的链接并点击
driver.findElement(By.xpath("//*[@id=\"problemTable\"]/tr[1]/td[2]/a")).click();
Thread.sleep(5000);
//找到题目编辑框
driver.executeScript("//*[@id=\"editor\"]/div[2]/div");
Thread.sleep(5000);
driver.quit();
}