【项目篇1】一个在线OJ系统
目录
一、前言:项目背景
功能1:能够管理题目
功能2:可以展示题目列表
功能3:题目详情页
功能4:可以令用户提交代码,并验证提交的情况
注意事项:
功能5:反馈运行的结果
二、项目搭建
三、Java如何进行多进程编程
3.1进程的创建
runntime.exec("父进程的路径")
从子进程当中获取标准输出,并写入目标文件
从子进程当中获取标准错误,并写入目标文件
观察程序运行的结果
3.2进程等待
为什么要了解进程的等待
3.3封装进程工具类(CommAndUtil)
确定方法的参数:String cmd,String stdoutFile,String stderrFile
编写方法内容
代码实现
四、项目模块1:实现编译——运行
模块的任务:
编写Question类
编写Answer类
属性1:private int code
属性2:private String reason
属性3:private String stdout
属性4:private String stderr
编写Task类
Task类的一些属性
①文件所在的目录:
②约定用户提交代码的类名:
③约定用户代码的文件名:
④存放编译错误信息的文件名
⑤运行时候的标准输出的文件名
⑥运行时错误信息的文件名
下面是编译、运行方法的一些步骤:
第一步:在方法当中使用File类创建一个目录
第二步:需要把Question的code写入到Solution.java当中
第三步:创建javac子进程,调用javac来进行编译
第四步:编译正确,代码开始运行,校验是否出现运行异常
步骤5:没有运行异常,那么就直接返回运行通过
整体Task类代码实现
五、项目模块2:封装读取文件的操作(FileUtil)
封装读取文件的方法
封装写入文件的方法
六、题目管理模块
6.1封装一个数据库连接类
6.2设计题目表
设计统一增删改查封装:BaseDao
为Problem类设置以下的属性:
建表语句如下:
设置ProblemDao的方法(对于Problem的crud封装):
题目的测试用例(TestCode如何设置)
七、Web模块
7.1题目列表页
请求:GET 路径: /problem
响应:json格式
7.2题目详情页
功能1:展示题目的详细要求
功能2:能够拥有一个编辑框,让用户来编写代码,并且提交代码
八、前端模块
一、前言:项目背景
回顾一下我们常见的OJ平台,例如:leetcode,牛客等等,他们都有哪些功能?
功能1:能够管理题目
例如可以保存题目的信息、保存本题的测试用例。
测试用例就是用来验证用户提交的代码是否都通过。
而leetcode和牛客默认的规则就是:只有测试用例都通过了,才会显示"ac"。
功能2:可以展示题目列表
例如:当点击"hot 100"的时候,我们可以看到高频100的题目一样。
功能3:题目详情页
能够展示某个题目的详情和代码编辑框
功能4:可以令用户提交代码,并验证提交的情况
当用户点击"提交"的按钮之后,网页就会把当前的代码提交到服务器上面,并且执行代码,给出一些是否通过用例的结果。
注意事项:
此处,用户提交的代码,一定是要以"多进程"的方式来完成的。
回顾一下进程的线程的区别?已经在这一篇文章当中提到了:
初识多线程编程_革凡成圣211的博客-CSDN博客多线程,线程创建的方式,run方法https://blog.csdn.net/weixin_56738054/article/details/127971676?spm=1001.2014.3001.5501 其中有一个很重要的区别就是:线程没有进程安全。多进程并发执行的时候,其中一个进程挂了,由于在真实的内存条当中,各个进程是相互隔离的,那么就不会导致其他的进程崩溃。
因此,当其中一个用户提交的代码出现异常的时候,为了不影响此时其他用户提交的运行情况,那么就需要使用"多进程"来进行编程。
因此,下面我们也会介绍一下怎样通过Java实现多进程编程
功能5:反馈运行的结果
用户可以查看历史的提交记录,以及本次提交的结果(通过了多少用例,有多少没有通过)
本次的情况:
历史的情况:
二、项目搭建
选择JavaEnterprise+Web应用程序即可
然后需要导入数据库连接的jar包(在pom.xml文件当中)
三、Java如何进行多进程编程
站在操作系统的角度(例如Linux)提供了很多的和多进程编程有关的接口,例如进程的创建、进程的销毁、进程的等待、进程之间通信等等...
但是,在Java当中,对于系统提供的这一些操作进行了封装,最终只提供了两个操作:
1、进程的创建;
2、进程的等待。
3.1进程的创建
由原先的父进程创建出来若干个子进程。一个父进程,可以有多个子进程。
服务器进程,相当于父进程;它会根据用户发送过来的代码再创建出子进程。
runntime.exec("父进程的路径")
这一个方法返回的是一个进程对象:Process。传入的参数是父进程的路径,返回的是一个子进程
public static void main(String[] args) throws IOException {//获取到这个类的唯一实例Runtime runtime= Runtime.getRuntime();//找到javac这一个进程//并且返回一个子进程Process process=runtime.exec("javac");}
当执行 runtime.exec("javac")的时候,相当于在cmd当中输入了一个对应的命令,那就是:javac。
运行上述的代码,可以看到下面的结果:(发现什么输出都没有)
一个进程在启动的时候,就会自动打开3个文件:
①标准输入,到对应的键盘上面;
②标准输出,对应到显示器上面;
③标准错误,也会对应到显示器上面;
从子进程当中获取标准输出,并写入目标文件
需要调用process的getInputStream()方法来获取
public static void main(String[] args) throws IOException {//获取到这个类的唯一实例Runtime runtime = Runtime.getRuntime();//找到javac这一个进程//并且返回一个子进程Process process = runtime.exec("javac");//获取到标准输出:从这对象当中读,就可以把子进程当中的标准输出给读出来InputStream stdoutFrom = process.getInputStream();//把读取到的内容写入到目标文件当中:text1.txtFileOutputStream stdoutTo = new FileOutputStream("text1.txt");while (true) {int ch = stdoutFrom.read();if (ch == -1) {break;}stdoutTo.write(ch);}stdoutFrom.close();stdoutTo.close();}
从子进程当中获取标准错误,并写入目标文件
需要带哦用process的getErrorStream()方法来获取流对象来进行读取。
public static void main(String[] args) throws IOException {//获取到这个类的唯一实例Runtime runtime = Runtime.getRuntime();//找到javac这一个进程//并且返回一个子进程Process process = runtime.exec("javac");//获取到标准输出:从这对象当中读,就可以把子进程当中的标准输出给读出来InputStream stdoutFrom = process.getInputStream();//把读取到的内容写入到目标文件当中:text1.txtFileOutputStream stdoutTo = new FileOutputStream("text1.txt");while (true) {int ch = stdoutFrom.read();if (ch == -1) {break;}stdoutTo.write(ch);}stdoutFrom.close();stdoutTo.close();//获取标准错误,从这个文件当中读取,就可以把子进程当中的标准错误给读取出来InputStream errorFrom= process.getErrorStream();//把标准错误读取到指定的文件夹FileOutputStream errorTo=new FileOutputStream("text2.txt");while (true){int ch=errorFrom.read();if(ch==-1){break;}errorTo.write(ch);}errorFrom.close();errorTo.close();}
观察程序运行的结果
上图的内容,就和在cmd当中输入了:javac命令之后的运行结果一样:
3.2进程等待
需要调用process对象的waitFor()方法来进行等待;
调用这个方法的时候,相当于父进程等待子进程执行完毕才会继续往下走。
waitFor()方法的返回值是一个整形。只有当进程正常退出的时候,才会返回0。
//通过process的waitFor方法来实现进程的等待
//父进程执行到waitFor的时候,会阻塞等待子进程执行完毕,才继续往下走
int exitCode=process.waitFor();
为什么要了解进程的等待
当用户提交了代码之后,需要令这些代码运行起来,运行结束之后,才可以进行后续的判定对错。
这一个运行用户代码的过程,就相当于是父进程(服务器进程)等待用户代码的运行(子进程)的一个过程。
3.3封装进程工具类(CommAndUtil)
在这个类当中,定义一个方法:run
确定方法的参数:String cmd,String stdoutFile,String stderrFile
这三个参数分别代表:
cmd:用户点击运行之后输入的命令,也就是javac命令;
stdoutFile:标准输入输出的文件;
stderrFile:运行出错时候输出的文件
编写方法内容
主要实现的功能是:
功能1:获取到标准的输入、输出文件,并且读取内容到指定文件当中;
功能2:获取到进程错误运行文件,并且把错误的内容读取到这个文件当中;
功能3:令主进程(调用run方法的进程)等待子进程执行结束。
代码实现
public static int run(String cmd,String stdoutFile,String stderrFile){Runtime runtime=Runtime.getRuntime();//创建一个子进程Process process= null;try {process = runtime.exec(cmd);//读取输入的文件if(stdoutFile!=null){//获取到文件的输入流对象:读取文件输入的内容InputStream inputStream= process.getInputStream();//获取文件的输出流对象:输出到对应的文件FileOutputStream stdoutTo=new FileOutputStream(stdoutFile);while (true){int ch=inputStream.read();if(ch==-1){break;}stdoutTo.write(ch);}//关闭流对象inputStream.close();stdoutTo.close();}//读取错误信息的文件if(stderrFile!=null){//记住:这里一定是errorStreamInputStream inputStream= process.getErrorStream();FileOutputStream errFile=new FileOutputStream(stderrFile);while (true){int ch= inputStream.read();if(ch==-1){break;}errFile.write(ch);}//关闭流对象inputStream.close();errFile.close();}//等待子进程执行完毕return process.waitFor();} catch (IOException | InterruptedException e) {e.printStackTrace();}//返回1:表示程序出错。return 1;}
四、项目模块1:实现编译——运行
模块的任务:
输入:用户提交的代码;
输出:程序的编译结果和运行结果。
编写Question类
这一个类代表的是用户编写的代码,内部封装了一个code属性;
/* 这个类表示一个输入的内容* @author 25043*/
public class Question {private String code;public String getCode() {return code;}public void setCode(String code) {this.code = code;}
}
编写Answer类
这一个类代表的是用户提交代码之后运行的结果,内部封装了以下几个属性;
属性1:private int code
错误码:约定 * 0为编译运行都通过; * 1为编译出错; * 2表示运行出错(抛出异常)
属性2:private String reason
存放各种异常出现的原因
属性3:private String stdout
运行程序得到的标准输出结果;
属性4:private String stderr
运行程序得到的标准错误结果
/* 表示用户提交代码之后的输出结果* @author 25043*/
public class Answer {/* 错误码:约定* 0为编译运行都通过;* 1为编译出错;* 2表示运行出错(抛出异常)*/private int code;/* 存放各种异常出现原因*/private String reason;/* 运行程序得到的标准输出的结果*/private String stdout;/* 运行程序得到的标准错误的结果*/private String stderr;public int getCode() {return code;}public void setCode(int code) {this.code = code;}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;}
}
编写Task类
在这一个类当中,需要提供一个创建和运行的方法:compileAndRun。方法的返回值就是Answer对象,为反馈给用户的一个结果,参数就是Question对象,为用户提交的代码。
在这个方法当中,大致的执行流程就是下面这样的:
Task类的一些属性
需要设置一些文件的属性:例如:
①文件所在的目录:
每一个用户过来一下,都会创建一个新的目录 :./tmp/ +UUID.randomUUID
这样避免了不同的用户之间的相互干扰。
②约定用户提交代码的类名:
③约定用户代码的文件名:
④存放编译错误信息的文件名
⑤运行时候的标准输出的文件名
⑥运行时错误信息的文件名
public class Task {/* 工作目录*/private static String WORK_DIR ;/* 类文件*/private static String CLASS;/* 代码*/private static String CODE=null;/* 编译异常存放路径*/private static String COMPILE_ERROR=null;/* 正常输出的路径*/private static String STDOUT=null;/* 异常输出的路径*/private static String STDERR=null;
}
下面是编译、运行方法的一些步骤:
第一步:在方法当中使用File类创建一个目录
//准备好用来存放临时文件的目录File workDir=new File(WORD_DIR);if(!workDir.exists()){//创建多级目录workDir.mkdirs();}
第二步:需要把Question的code写入到Solution.java当中
需要提供一个Solution.java这一个类,来存放用户的代码。(FileUtil参考项目模块2)
//第二步:需要把question的code写入到一个Solution.java文件当中,才可以进行编译FileUtil.write(CODE,question.getCode());
第三步:创建javac子进程,调用javac来进行编译
首先需要构建格式化的字符串编译命令,并且指定文件的位置。
然后使用CommandUtil.run()方法,并且传入的参数是:
compiled:作为编译的命令;
stdoutFile:正常编译时候的输出;由于编译时期只关注编译是否出错,因此这个参数传入null;
strderrFile:编译异常时候的输出文件路径。
// 第三步:创建子进程,调用javac进行编译,并且指定路径//先构造编译命令:-d :指定放置的生成类文件的位置/ javac -encoding +字符集编码:指定源文件使用的字符编码* 此处要改成:gbk,因为windows系统默认是gbk编码* 先构造编译命令:-d :指定放置的生成类文件的位置
*/String compileCmd=String.format("javac -encoding gbk %s -d %s ",CODE,WORD_DIR);//编译这一个文件,看看是否出现错误
CommandUtil.run(compileCmd,null,COMPILE_ERROR);
并且,在接下来的步骤当中,需要查看是否出现编译出错的情况,如果出现了编译错误,那么直接返回ERROR。
//编译这一个文件,看看是否出现错误CommandUtil.run(compileCmd,null,COMPILE_ERROR);//如果编译出错,javac就会把错误写入到stderr当中,用一个专门的文件来保存:compileError.txString compileError=FileUtil.readFile(COMPILE_ERROR);//编译出错的情况if(!"".equals(compileError)){//首先需要设置错误信息:直接返回ERRORanswer.setCode(1);answer.setReason(compileError);return answer;}
第四步:编译正确,代码开始运行,校验是否出现运行异常
但是,在这一个阶段,可能会出现用户输入代码产生死循环的情况,因此,还需要考虑死循环的情况(使用线程等待join机制)
long start=System.currentTimeMillis();Thread thread=new Thread(() -> CommandUtil.run(runCmd,STDOUT,STDERR));thread.start();try {thread.join(problem.getSeconds());} catch (InterruptedException e) {e.printStackTrace();}long end=System.currentTimeMillis();//说明超时了if(end-start>=problem.getSeconds()){answer.setCode(4);answer.setReason("您的代码提交超时");return answer;}//检验是否运行出错String runError=FileUtil.readFile(STDERR);//运行出错的情况if(!"".equals(runError)){System.out.println("运行异常");answer.setCode(2);answer.setReason(runError);return answer;}
步骤5:没有运行异常,那么就直接返回运行通过
最后,运行完毕之后,删除这个目录
//正常运行answer.setCode(0);answer.setStdout(FileUtil.readFile(STDOUT));
return answer;
整体Task类代码实现
/* @author 25043*/
public class Task {/* 工作目录*/private static String WORK_DIR ;/* 类文件*/private static String CLASS;/* 代码*/private static String CODE=null;/* 编译异常存放路径*/private static String COMPILE_ERROR=null;/* 正常输出的路径*/private static String STDOUT=null;/* 异常输出的路径*/private static String STDERR=null;public Task(){//是每次文件夹名字不同WORK_DIR="E:/OJSystem/tmp/"+ UUID.randomUUID() +"/";CODE=WORK_DIR+"Solution.java";CLASS="Solution";COMPILE_ERROR=WORK_DIR+"compileError.txt";STDOUT=WORK_DIR+"stdout.txt";STDERR=WORK_DIR+"stderr.txt";}/* 提供的核心方法就是 compileAndRun:含义就是编译和运行* 要编译运行的java代码@param question* 编译运行的结果@return*/public Answer compileAndRun(Question question, Problem problem){Answer answer=new Answer();//准备好用来存放临时文件的目录File workDir=new File(WORK_DIR);System.out.println("绝对的路径是:"+workDir.getAbsolutePath());if(!workDir.exists()){//创建多级目录workDir.mkdirs();}//第二步:需要把question的code写入到一个Solution.java文件当中,才可以进行编译FileUtil.write(CODE,question.getCode());// 第三步:创建子进程,调用javac进行编译,并且指定路径/ javac -encoding +字符集编码:指定源文件使用的字符编码* 此处要改成:gbk,因为windows系统默认是gbk编码* 先构造编译命令:-d :指定放置的生成类文件的位置*/String compileCmd=String.format("javac -encoding gbk %s -d %s ",CODE, WORK_DIR);System.out.println("编译命令:"+compileCmd);//编译这一个文件,看看是否出现错误CommandUtil.run(compileCmd,null,COMPILE_ERROR);//如果编译出错,javac就会把错误写入到stderr当中,用一个专门的文件来保存:compileError.txtString compileError=FileUtil.readFile(COMPILE_ERROR);System.out.println(compileError+"...");//编译出错的情况if(!"".equals(compileError)){//首先需要设置错误信息:直接返回ERRORSystem.out.println("编译出错");answer.setCode(1);answer.setReason(compileError);return answer;}//编译没有出错,得到.class文件,继续往下执行//第四步:调用java命令并且执行代码//运行程序的时候,也会把java子进程的标准输入和标准输出获取到:stdout.txt,stderr.txtString runCmd=String.format("java -classpath %s %s", WORK_DIR,CLASS);long start=System.currentTimeMillis();Thread thread=new Thread(() -> CommandUtil.run(runCmd,STDOUT,STDERR));thread.start();try {thread.join(problem.getSeconds());} catch (InterruptedException e) {e.printStackTrace();}long end=System.currentTimeMillis();//说明超时了if(end-start>=problem.getSeconds()){answer.setCode(4);answer.setReason("您的代码提交超时");return answer;}//检验是否运行出错(运行异常)String runError=FileUtil.readFile(STDERR);//运行出错的情况if(!"".equals(runError)){System.out.println("运行异常");answer.setCode(2);answer.setReason(runError);return answer;}//正常运行answer.setCode(0);answer.setStdout(FileUtil.readFile(STDOUT));return answer;}public static void main(String[] args) {Task task=new Task();Question question=new Question();question.setCode("class Solution { public int[] twoSum(int[] nums, int target) { /*你好*/int[] a={0,1};return a;}public static void main(String[] args){\\n" +" Solution slo=new Solution();\\n" +" //\\n" +" int[] result1=slo.twoSum(new int[]{2,7,11,15},9);\\n" +" if(result1.length==2&&result1[0]==0&&result1[1]==1){\\n" +" System.out.println(\\"Test OK\\");\\n" +" }else{\\n" +" System.out.println(\\"Test Error\\");\\n" +" }\\n" +" //\\n" +" int[] result2=slo.twoSum(new int[]{3,2,4},6);\\n" +" if(result2.length==2&&result2[0]==1&&result2[1]==2){\\n" +" System.out.println(\\"Test OK\\");\\n" +" }else{\\n" +" System.out.println(\\"Test Error\\");\\n" +" }\\n" +"\\n" +" }\\n" +"}");Problem problem=new Problem();problem.setSeconds(4000);Answer answer=task.compileAndRun(question, problem);System.out.println("测试类当中的Code:"+question.getCode());System.out.println(answer.getCode());System.out.println(answer.getStderr());System.out.println(answer.getStdout());}
}
五、项目模块2:封装读取文件的操作(FileUtil)
封装读取文件的方法
给定一个指定的文件路径,返回文件的所有内容。
public static String readFile(String filePath){//负责把filePath对应的文件内容读取出来,放到返回值当中StringBuilder buffer=new StringBuilder();try (FileReader fileReader=new FileReader(filePath)){while (true){int ch= fileReader.read();if(ch==-1){break;}buffer.append((char)ch);}} catch (IOException e) {e.printStackTrace();}return buffer.toString();}
封装写入文件的方法
往指定的filePath当中写入内容:
public static void write(String filePath,String content){//获取到文件的路径try (FileWriter fileWriter=new FileWriter(filePath)){//写入到指定的内容当中fileWriter.write(content);} catch (IOException e) {e.printStackTrace();}}
六、题目管理模块
6.1封装一个数据库连接类
/* 数据库连接的工具类* @author 25043*/
public class DataBaseUtil {/* 配置数据库连接的URL*/private static final String URL="jdbc:mysql://127.0.0.1:3306/MyOJSystem?characterEncoding=utf8&useSSL=false";/* 用户名*/private static final String USERNAME="root";/* 密码*/private static final String PASSWORD="20021111aA#";private volatile static MysqlDataSource dataSource=null;public static DataSource getDataSource(){if(dataSource==null){synchronized (DataBaseUtil.class){if(dataSource==null){dataSource=new MysqlDataSource();dataSource.setURL(URL);dataSource.setPassword(PASSWORD);dataSource.setUser(USERNAME);}}}return dataSource;}public static Connection getConnection() throws SQLException {return getDataSource().getConnection();}public static void close(PreparedStatement preparedStatement, Connection connection, ResultSet resultSet){if(resultSet!=null){try {resultSet.close();} catch (SQLException e) {e.printStackTrace();}}if(preparedStatement!=null){try {preparedStatement.close();} catch (SQLException e) {e.printStackTrace();}}if(connection!=null){try {connection.close();} catch (SQLException e) {e.printStackTrace();}}}}
6.2设计题目表
设计统一增删改查封装:BaseDao
/* 对于增删改查方法的统一封装 @author 25043*/
public class BaseDao {/* 查询集合* sql语句@param sql* 传入的二进制字节码@param clazz* 参数数组@param args* 泛型@param <T>* 集合@return*/public <T> ArrayList<T> queryList(String sql, Class<T> clazz, Object... args) {//创建泛型集合ArrayList<T> list = new ArrayList<>();ResultSet resultSet = getResultSet(sql, args);try {//获取结果集的信息//1、assert <boolean表达式> 如果<boolean表达式>为true,则程序继续执行。 如果为false,则程序抛出AssertionError,并终止执行。assert resultSet != null;ResultSetMetaData resultSetMetaData = resultSet.getMetaData();//获取列数int colum = resultSetMetaData.getColumnCount();//遍历结果集while (resultSet.next()) {//通过字节码.getDeclaredConstructor()来获取构造器,并且通过newInstance()方法获取到一个对象T t = clazz.getDeclaredConstructor().newInstance();//为当前的对象属性赋值workForField(colum, t, resultSetMetaData, clazz, resultSet);//赋值完成之后添加到对应的集合当中list.add(t);}return list;} catch (SQLException | NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchFieldException | AssertionError e) {e.printStackTrace();//像上级抛出自定义的异常throw new DaoException("executeUpdate方法预编译sql语句异常", e);} finally {DataBaseUtil.close(null,null,resultSet);}}/* 为属性赋值* 列数@param colum* 被赋值的对象@param t* 获取结果集信息的类@param resultSetMetaData* 被赋值对象的二进制字节码对象@param clazz* 结果集@param resultSet* 泛型集合@param <T>* 异常@throws SQLException* SQL异常@throws NoSuchFieldException* 反射异常@throws IllegalAccessException*/private <T> void workForField(int colum, T t, ResultSetMetaData resultSetMetaData, Class<T> clazz, ResultSet resultSet) throws SQLException, NoSuchFieldException, IllegalAccessException {for (int i = 0; i < colum; i++) {//获取列名称String columName = resultSetMetaData.getColumnLabel(i + 1);//获取列的属性columnValue值为列名,数据库的列名称,columnValue为数据库表中的数据Object columnValue = resultSet.getObject(columName);//如果没有此属性,会报异常:noSuchFieldExceptionField field = clazz.getDeclaredField(columName);//无视属性修饰符field.setAccessible(true);//设置属性值,t代表对应的对象,columValue代表对应的值field.set(t, columnValue);}}/* 查询单个对象* 查询的sql语句@param sql* 类的二进制字节码文件@param clazz* 数组@param args* 泛型@param <T>* 被查询的对象@return*/public <T> T queryObject(String sql, Class<T> clazz, Object... args) {ResultSet resultSet = getResultSet(sql, args);//创建泛型集合try {//获取结果集的信息assert resultSet != null;ResultSetMetaData resultSetMetaData = resultSet.getMetaData();//获取列数int colum = resultSetMetaData.getColumnCount();//遍历结果集T t = null;while (resultSet.next()) {t = clazz.getDeclaredConstructor().newInstance();workForField(colum, t, resultSetMetaData, clazz, resultSet);}return t;} catch (SQLException | NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchFieldException | AssertionError e) {e.printStackTrace();throw new DaoException("Dao层发生了sql语句异常", e);} finally {DataBaseUtil.close(null,null,resultSet);}}/* 封装增删改的方法* sql语句@param sql* 可变参数数组@param args* 执行的行数@return*/public int update(String sql, Object... args) {PreparedStatement preparedStatement = null;Connection connection = DataBaseUtil.getConnection();System.out.println(connection);try {preparedStatement = connection.prepareStatement(sql);int i = 1;if (args.length > 0) {for (Object object : args) {preparedStatement.setObject(i, object);i++;}}return preparedStatement.executeUpdate();} catch (SQLException e) {e.printStackTrace();throw new DaoException("Dao层发生了sql语句异常", e);} finally {DataBaseUtil.close(preparedStatement,connection,null);}}/* 获取结果集* sql语句@param sql* 参数数组@param args* 结果集@return*/private ResultSet getResultSet(String sql, Object... args) {Connection connection;PreparedStatement preparedStatement;try {int i = 1;connection = DataBaseUtil.getConnection();preparedStatement = connection.prepareStatement(sql);//遍历数组if (args.length > 0) {for (Object object : args) {preparedStatement.setObject(i, object);i++;}}return preparedStatement.executeQuery();} catch (SQLException e) {e.printStackTrace();throw new DaoException("Dao层发生了sql语句异常", e);}}
}
为Problem类设置以下的属性:
/* @author 25043*/
public class Problem {/* 题目的主键ID*/private int id;private String tittle;/* 题目的难度*/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 getTittle() {return tittle;}public void setTittle(String tittle) {this.tittle = tittle;}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;}
}
建表语句如下:
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)
);
设置ProblemDao的方法(对于Problem的crud封装):
/* 封装了对于problem的增删改查方法* @author 25043*/
public class ProblemDao extends BaseDao{public int insert(Problem problem){String sql="insert into oj_table values(null,?,?,?,?,?)";return this.update(sql,problem.getTittle(),problem.getLevel(),problem.getDescription(),problem.getTemplateCode(),problem.getTestCode());}public int delete(int id){String sql="delete from oj_table where id=?";return this.update(sql,id);}public List<Problem> selectAll(){String sql="select id,tittle,level from oj_table";return this.queryList(sql,Problem.class);}public Problem selectOne(int problemId){String sql="select*from oj_table where id=?";return this.queryObject(sql,Problem.class,problemId);}}
题目的测试用例(TestCode如何设置)
测试用例代码就是一个mian方法,然后需要在这个方法内部创建Solution实例,并且通过这个实例调用核心方法(例如leetCode02的两数之和)
在调用核心方法的时候,传入不同的参数,并且针对返回的结果进行判定。
如果返回结果符合预期,那么显示TestOK,如果不符合,那么就显示"Test failed"
并且打印出错的详情。
因此,设计的大致思路就是:
服务器当中会收到用户提交的Solution代码,然后再从数据库当中查询到测试用例代码,二者进行一个拼接。那么此时Solution类就会有main方法了,就可以单独进行编译和运行了。
代码实现:
完成拼接之后,直接编译运行拼接之后的代码。
所以,在设置数据库的测试用例字段的时候,只需要把右侧的main方法以String的格式存入到problem的测试用例当中。
七、Web模块
7.1题目列表页
这一个页面负责展示所有题目的列表。类似于leetCode上面点击了"题库"之后看到的内容。
请求:GET 路径: /problem
@WebServlet("/problem")
public class ProblemServlet extends HttpServlet {private ObjectMapper objectMapper=new ObjectMapper();@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {ProblemDao problemDao=new ProblemDao();String idString=req.getParameter("id");//尝试获取id参数,如果不能获取到,说明是查询题目列表if(idString==null||"".equals(idString)){List<Problem> problems=problemDao.selectAll();String respString =objectMapper.writeValueAsString(problems);resp.setCharacterEncoding("utf8");resp.getWriter().write(respString);}else{//如果能获取到,说明是查询题目详情Problem problem=problemDao.selectOne(Integer.parseInt(idString));String respString=objectMapper.writeValueAsString(problem);resp.getWriter().write(respString);}}
}
响应:json格式
[{id :1,tittle : "两数之和",level : "简单"},{id : 2,tittle : "两数相加",level : "中等",}
]
7.2题目详情页
功能1:展示题目的详细要求
请求:GET /problem?id=...
代码同上,但是要注意传递的参数为1.
响应:
{id :1,tittle : "两数之和",level : "简单",description : "题目的详细要求,包括题干,输入输出信息等等...",templateCode : "模板代码",
}
功能2:能够拥有一个编辑框,让用户来编写代码,并且提交代码
请求:POST 路径:/compile
提交的数据:(服务器端需要新建一个内部类来接受请求的json数据)
{id :1,code :"编辑框当中的代码...(也就是用户输入的代码)"
}
用户提交的代码就是一个核心代码,但是如果想要编译+运行,还是需要令用户提交的代码+main方法(也就是测试用例代码),才可以进行编译+运行。
关于第二步:图解一下
readBody方法内部就是读取contentLength的长度
关于第三步:图解一下
由于用户提交的是一个大括号+核心代码块的样式的。
因此,为了把main方法嵌套到用户提交的solution内部,应当考虑:寻找到最后一个"}"所在的位置,然后在这个位置之前拼接测试用例的main方法,再拼接上这个"}"。返回一个finalCode
关于第四步:
需要构建一个Question类来进行设置code。并且调用Task类来编译Question类的code。
响应:json格式
服务器端需要一个内部类来表示响应的json数据
{error : 0,reason : "出错的原因",stdout : "测试用例的输出情况,包含了通过几个用例这样"
}
同时,也需要考虑:用户输入的代码是否恶意等等的情况。
功能2整体servlet代码实现
@WebServlet("/compile")
public class CompileServlet extends HttpServlet {/* 用于表示接受请求*/static class CompileAndRequest{public int id;public String code;}/* 用于表示响应*/static class CompileAndResponse{public int error;public String reason;public String stdout;}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {CompileAndResponse compileAndResponse=new CompileAndResponse();req.setCharacterEncoding("utf8");resp.setContentType("application/json;charset=utf8");//先读取正文,并且按照json的格式进行解析String body=readBody(req);ObjectMapper mapper=new ObjectMapper();//解析json对象//body是一个json格式的字符串CompileAndRequest compileAndRequest=mapper.readValue(body,CompileAndRequest.class);//根据id从数据库当中查找到题目的详情==>得到测试用例代码ProblemDao problemDao=new ProblemDao();//根据id查询problem,并且道道测试用例代码Problem problem= problemDao.selectOne(compileAndRequest.id);try {if(problem==null) {throw new ProblemNotFoundException();}//测试用例代码String testCode=problem.getTestCode();//用户提交代码String requestCode=compileAndRequest.code;//二者进行一个拼接,变成可编译的代码String finalCode=mergeCode(testCode,requestCode);if(finalCode==null){throw new CodeInValidException();}//把用户提交的代码和测试用例代码,拼接成一个完整的携带main方法的代码,可以进行编译+运行System.out.println("Servlet的Code"+finalCode);//创建Task实例,调用里面的compileAndRun来进行编译运行’Task task=new Task();Question question=new Question();question.setCode(finalCode);Answer answer=task.compileAndRun(question);//根据Task运行的结果,包装成一个HTTP响应compileAndResponse.error=answer.getCode();compileAndResponse.reason=answer.getReason();compileAndResponse.stdout=answer.getStdout();}catch (ProblemNotFoundException e){compileAndResponse.error=3;compileAndResponse.reason="题目没有找到 id="+compileAndRequest.id;}catch (CodeInValidException e){compileAndResponse.error=3;compileAndResponse.reason="提交的代码不符合要求";}finally {String respString=mapper.writeValueAsString(compileAndResponse);resp.getWriter().write(respString);}}private static String mergeCode(String testCode, String requestCode) {//拼接思路:把testCode给放到solution的最后一个大括号的前面即可//查找requestCode的最后一个}int pos=requestCode.lastIndexOf("}");System.out.println(pos);if(pos==-1){//说明提交的代码完全没有},显然是非法的代码return null;}//截取到最后一个大括号之前的字符串String subStr=requestCode.substring(0,pos);//拼接return subStr + testCode + "\\n" + "}";}private static String readBody(HttpServletRequest req) throws UnsupportedEncodingException {//第一步:获取到请求头的contentLength字段长度(单位:字节)int contentLength=req.getContentLength();//第二步:按照这个长度准备一个byte数组byte[] buffer=new byte[contentLength];try (InputStream inputStream=req.getInputStream()){//基于这个流对象进行读取inputStream.read(buffer);} catch (IOException e) {e.printStackTrace();}//指定字符编码return new String(buffer,"utf8");}
}
八、前端模块
对于这一模块,不会展开详细的介绍;大致就是一个题目列表页+一个代码详情编辑页。
具体的代码可以参考项目源码的webapps目录下面的各个目录。(前端页面我COPY来的)
https://gitee.com/wangjiaxin20021111/OJSystem/tree/master/https://gitee.com/wangjiaxin20021111/OJSystem/tree/master/
项目测试文档在ProjecTest文件夹下面
题目列表页:
题目详情+编辑页: