当前位置:   article > 正文

【项目篇1】一个在线OJ系统

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。传入的参数是父进程的路径,返回的是一个子进程

  1. public static void main(String[] args) throws IOException {
  2. //获取到这个类的唯一实例
  3. Runtime runtime= Runtime.getRuntime();
  4. //找到javac这一个进程
  5. //并且返回一个子进程
  6. Process process=runtime.exec("javac");
  7. }

当执行 runtime.exec("javac")的时候,相当于在cmd当中输入了一个对应的命令,那就是:javac。

运行上述的代码,可以看到下面的结果:(发现什么输出都没有)

 一个进程在启动的时候,就会自动打开3个文件:

①标准输入,到对应的键盘上面;

②标准输出,对应到显示器上面;

③标准错误,也会对应到显示器上面;


从子进程当中获取标准输出,并写入目标文件

需要调用process的getInputStream()方法来获取

  1. public static void main(String[] args) throws IOException {
  2. //获取到这个类的唯一实例
  3. Runtime runtime = Runtime.getRuntime();
  4. //找到javac这一个进程
  5. //并且返回一个子进程
  6. Process process = runtime.exec("javac");
  7. //获取到标准输出:从这对象当中读,就可以把子进程当中的标准输出给读出来
  8. InputStream stdoutFrom = process.getInputStream();
  9. //把读取到的内容写入到目标文件当中:text1.txt
  10. FileOutputStream stdoutTo = new FileOutputStream("text1.txt");
  11. while (true) {
  12. int ch = stdoutFrom.read();
  13. if (ch == -1) {
  14. break;
  15. }
  16. stdoutTo.write(ch);
  17. }
  18. stdoutFrom.close();
  19. stdoutTo.close();
  20. }

从子进程当中获取标准错误,并写入目标文件

需要带哦用process的getErrorStream()方法来获取流对象来进行读取。

  1. public static void main(String[] args) throws IOException {
  2. //获取到这个类的唯一实例
  3. Runtime runtime = Runtime.getRuntime();
  4. //找到javac这一个进程
  5. //并且返回一个子进程
  6. Process process = runtime.exec("javac");
  7. //获取到标准输出:从这对象当中读,就可以把子进程当中的标准输出给读出来
  8. InputStream stdoutFrom = process.getInputStream();
  9. //把读取到的内容写入到目标文件当中:text1.txt
  10. FileOutputStream stdoutTo = new FileOutputStream("text1.txt");
  11. while (true) {
  12. int ch = stdoutFrom.read();
  13. if (ch == -1) {
  14. break;
  15. }
  16. stdoutTo.write(ch);
  17. }
  18. stdoutFrom.close();
  19. stdoutTo.close();
  20. //获取标准错误,从这个文件当中读取,就可以把子进程当中的标准错误给读取出来
  21. InputStream errorFrom= process.getErrorStream();
  22. //把标准错误读取到指定的文件夹
  23. FileOutputStream errorTo=new FileOutputStream("text2.txt");
  24. while (true){
  25. int ch=errorFrom.read();
  26. if(ch==-1){
  27. break;
  28. }
  29. errorTo.write(ch);
  30. }
  31. errorFrom.close();
  32. errorTo.close();
  33. }

观察程序运行的结果

上图的内容,就和在cmd当中输入了:javac命令之后的运行结果一样:


3.2进程等待

需要调用process对象的waitFor()方法来进行等待;

调用这个方法的时候,相当于父进程等待子进程执行完毕才会继续往下走

waitFor()方法的返回值是一个整形。只有当进程正常退出的时候,才会返回0。

  1. //通过process的waitFor方法来实现进程的等待
  2. //父进程执行到waitFor的时候,会阻塞等待子进程执行完毕,才继续往下走
  3. int exitCode=process.waitFor();

为什么要了解进程的等待

当用户提交了代码之后,需要令这些代码运行起来,运行结束之后,才可以进行后续的判定对错。

       这一个运行用户代码的过程,就相当于是父进程(服务器进程)等待用户代码的运行(子进程)的一个过程。


3.3封装进程工具类(CommAndUtil)

在这个类当中,定义一个方法:run

确定方法的参数:String cmd,String stdoutFile,String stderrFile

这三个参数分别代表:

cmd:用户点击运行之后输入的命令,也就是javac命令;

stdoutFile:标准输入输出的文件;

stderrFile:运行出错时候输出的文件


编写方法内容 

主要实现的功能是:

功能1:获取到标准的输入、输出文件,并且读取内容到指定文件当中;

功能2:获取到进程错误运行文件,并且把错误的内容读取到这个文件当中;

功能3:令主进程(调用run方法的进程)等待子进程执行结束。


 代码实现

  1. public static int run(String cmd,String stdoutFile,String stderrFile){
  2. Runtime runtime=Runtime.getRuntime();
  3. //创建一个子进程
  4. Process process= null;
  5. try {
  6. process = runtime.exec(cmd);
  7. //读取输入的文件
  8. if(stdoutFile!=null){
  9. //获取到文件的输入流对象:读取文件输入的内容
  10. InputStream inputStream= process.getInputStream();
  11. //获取文件的输出流对象:输出到对应的文件
  12. FileOutputStream stdoutTo=new FileOutputStream(stdoutFile);
  13. while (true){
  14. int ch=inputStream.read();
  15. if(ch==-1){
  16. break;
  17. }
  18. stdoutTo.write(ch);
  19. }
  20. //关闭流对象
  21. inputStream.close();
  22. stdoutTo.close();
  23. }
  24. //读取错误信息的文件
  25. if(stderrFile!=null){
  26. //记住:这里一定是errorStream
  27. InputStream inputStream= process.getErrorStream();
  28. FileOutputStream errFile=new FileOutputStream(stderrFile);
  29. while (true){
  30. int ch= inputStream.read();
  31. if(ch==-1){
  32. break;
  33. }
  34. errFile.write(ch);
  35. }
  36. //关闭流对象
  37. inputStream.close();
  38. errFile.close();
  39. }
  40. //等待子进程执行完毕
  41. return process.waitFor();
  42. } catch (IOException | InterruptedException e) {
  43. e.printStackTrace();
  44. }
  45. //返回1:表示程序出错。
  46. return 1;
  47. }

四、项目模块1:实现编译——运行

模块的任务:

输入:用户提交的代码;

输出:程序的编译结果和运行结果。


编写Question类

这一个类代表的是用户编写的代码,内部封装了一个code属性;

  1. /**
  2. * 这个类表示一个输入的内容
  3. * @author 25043
  4. */
  5. public class Question {
  6. private String code;
  7. public String getCode() {
  8. return code;
  9. }
  10. public void setCode(String code) {
  11. this.code = code;
  12. }
  13. }

编写Answer类

这一个类代表的是用户提交代码之后运行的结果,内部封装了以下几个属性;

属性1:private int code

错误码:约定 * 0为编译运行都通过; * 1为编译出错; * 2表示运行出错(抛出异常)


属性2:private String reason

存放各种异常出现的原因


属性3:private String stdout

运行程序得到的标准输出结果;


属性4:private String stderr

运行程序得到的标准错误结果


  1. /**
  2. * 表示用户提交代码之后的输出结果
  3. * @author 25043
  4. */
  5. public class Answer {
  6. /**
  7. * 错误码:约定
  8. * 0为编译运行都通过;
  9. * 1为编译出错;
  10. * 2表示运行出错(抛出异常)
  11. */
  12. private int code;
  13. /**
  14. * 存放各种异常出现原因
  15. */
  16. private String reason;
  17. /**
  18. * 运行程序得到的标准输出的结果
  19. */
  20. private String stdout;
  21. /**
  22. * 运行程序得到的标准错误的结果
  23. */
  24. private String stderr;
  25. public int getCode() {
  26. return code;
  27. }
  28. public void setCode(int code) {
  29. this.code = code;
  30. }
  31. public String getReason() {
  32. return reason;
  33. }
  34. public void setReason(String reason) {
  35. this.reason = reason;
  36. }
  37. public String getStdout() {
  38. return stdout;
  39. }
  40. public void setStdout(String stdout) {
  41. this.stdout = stdout;
  42. }
  43. public String getStderr() {
  44. return stderr;
  45. }
  46. public void setStderr(String stderr) {
  47. this.stderr = stderr;
  48. }
  49. }

编写Task类

       在这一个类当中,需要提供一个创建运行的方法:compileAndRun。方法的返回值就是Answer对象,为反馈给用户的一个结果,参数就是Question对象,为用户提交的代码

       在这个方法当中,大致的执行流程就是下面这样的:

Task类的一些属性

需要设置一些文件的属性:例如:

①文件所在的目录:

 每一个用户过来一下,都会创建一个新的目录 :./tmp/ +UUID.randomUUID

这样避免了不同的用户之间的相互干扰。


②约定用户提交代码的类名: 

③约定用户代码的文件名: 

④存放编译错误信息的文件名 

⑤运行时候的标准输出的文件名 

⑥运行时错误信息的文件名

  1. public class Task {
  2. /**
  3. * 工作目录
  4. */
  5. private static String WORK_DIR ;
  6. /**
  7. * 类文件
  8. */
  9. private static String CLASS;
  10. /**
  11. * 代码
  12. */
  13. private static String CODE=null;
  14. /**
  15. * 编译异常存放路径
  16. */
  17. private static String COMPILE_ERROR=null;
  18. /**
  19. * 正常输出的路径
  20. */
  21. private static String STDOUT=null;
  22. /**
  23. * 异常输出的路径
  24. */
  25. private static String STDERR=null;
  26. }

下面是编译、运行方法的一些步骤: 

第一步:在方法当中使用File类创建一个目录

  1. //准备好用来存放临时文件的目录
  2. File workDir=new File(WORD_DIR);
  3. if(!workDir.exists()){
  4. //创建多级目录
  5. workDir.mkdirs();
  6. }

第二步:需要把Question的code写入到Solution.java当中

需要提供一个Solution.java这一个类,来存放用户的代码。(FileUtil参考项目模块2)

  1. //第二步:需要把question的code写入到一个Solution.java文件当中,才可以进行编译
  2. FileUtil.write(CODE,question.getCode());

第三步:创建javac子进程,调用javac来进行编译

首先需要构建格式化的字符串编译命令,并且指定文件的位置。

然后使用CommandUtil.run()方法,并且传入的参数是:

compiled:作为编译的命令;

stdoutFile:正常编译时候的输出;由于编译时期只关注编译是否出错,因此这个参数传入null;

strderrFile编译异常时候的输出文件路径。

  1. // 第三步:创建子进程,调用javac进行编译,并且指定路径
  2. //先构造编译命令:-d :指定放置的生成类文件的位置
  3. /*
  4. * javac -encoding +字符集编码:指定源文件使用的字符编码
  5. * 此处要改成:gbk,因为windows系统默认是gbk编码
  6. * 先构造编译命令:-d :指定放置的生成类文件的位置
  7. */
  8. String compileCmd=String.format("javac -encoding gbk %s -d %s ",CODE,WORD_DIR);
  9. //编译这一个文件,看看是否出现错误
  10. CommandUtil.run(compileCmd,null,COMPILE_ERROR);

       并且,在接下来的步骤当中,需要查看是否出现编译出错的情况,如果出现了编译错误,那么直接返回ERROR。

  1. //编译这一个文件,看看是否出现错误
  2. CommandUtil.run(compileCmd,null,COMPILE_ERROR);
  3. //如果编译出错,javac就会把错误写入到stderr当中,用一个专门的文件来保存:compileError.tx
  4. String compileError=FileUtil.readFile(COMPILE_ERROR);
  5. //编译出错的情况
  6. if(!"".equals(compileError)){
  7. //首先需要设置错误信息:直接返回ERROR
  8. answer.setCode(1);
  9. answer.setReason(compileError);
  10. return answer;
  11. }

第四步:编译正确,代码开始运行,校验是否出现运行异常

但是,在这一个阶段,可能会出现用户输入代码产生死循环的情况,因此,还需要考虑死循环的情况(使用线程等待join机制)

  1. long start=System.currentTimeMillis();
  2. Thread thread=new Thread(() -> CommandUtil.run(runCmd,STDOUT,STDERR));
  3. thread.start();
  4. try {
  5. thread.join(problem.getSeconds());
  6. } catch (InterruptedException e) {
  7. e.printStackTrace();
  8. }
  9. long end=System.currentTimeMillis();
  10. //说明超时了
  11. if(end-start>=problem.getSeconds()){
  12. answer.setCode(4);
  13. answer.setReason("您的代码提交超时");
  14. return answer;
  15. }
  16. //检验是否运行出错
  17. String runError=FileUtil.readFile(STDERR);
  18. //运行出错的情况
  19. if(!"".equals(runError)){
  20. System.out.println("运行异常");
  21. answer.setCode(2);
  22. answer.setReason(runError);
  23. return answer;
  24. }

步骤5:没有运行异常,那么就直接返回运行通过

最后,运行完毕之后,删除这个目录

  1. //正常运行
  2. answer.setCode(0);
  3. answer.setStdout(FileUtil.readFile(STDOUT));
  4. return answer;

整体Task类代码实现

  1. /**
  2. * @author 25043
  3. */
  4. public class Task {
  5. /**
  6. * 工作目录
  7. */
  8. private static String WORK_DIR ;
  9. /**
  10. * 类文件
  11. */
  12. private static String CLASS;
  13. /**
  14. * 代码
  15. */
  16. private static String CODE=null;
  17. /**
  18. * 编译异常存放路径
  19. */
  20. private static String COMPILE_ERROR=null;
  21. /**
  22. * 正常输出的路径
  23. */
  24. private static String STDOUT=null;
  25. /**
  26. * 异常输出的路径
  27. */
  28. private static String STDERR=null;
  29. public Task(){
  30. //是每次文件夹名字不同
  31. WORK_DIR="E:/OJSystem/tmp/"+ UUID.randomUUID() +"/";
  32. CODE=WORK_DIR+"Solution.java";
  33. CLASS="Solution";
  34. COMPILE_ERROR=WORK_DIR+"compileError.txt";
  35. STDOUT=WORK_DIR+"stdout.txt";
  36. STDERR=WORK_DIR+"stderr.txt";
  37. }
  38. /**
  39. * 提供的核心方法就是 compileAndRun:含义就是编译和运行
  40. * 要编译运行的java代码@param question
  41. * 编译运行的结果@return
  42. */
  43. public Answer compileAndRun(Question question, Problem problem){
  44. Answer answer=new Answer();
  45. //准备好用来存放临时文件的目录
  46. File workDir=new File(WORK_DIR);
  47. System.out.println("绝对的路径是:"+workDir.getAbsolutePath());
  48. if(!workDir.exists()){
  49. //创建多级目录
  50. workDir.mkdirs();
  51. }
  52. //第二步:需要把question的code写入到一个Solution.java文件当中,才可以进行编译
  53. FileUtil.write(CODE,question.getCode());
  54. // 第三步:创建子进程,调用javac进行编译,并且指定路径
  55. /*
  56. * javac -encoding +字符集编码:指定源文件使用的字符编码
  57. * 此处要改成:gbk,因为windows系统默认是gbk编码
  58. * 先构造编译命令:-d :指定放置的生成类文件的位置
  59. */
  60. String compileCmd=String.format("javac -encoding gbk %s -d %s ",CODE, WORK_DIR);
  61. System.out.println("编译命令:"+compileCmd);
  62. //编译这一个文件,看看是否出现错误
  63. CommandUtil.run(compileCmd,null,COMPILE_ERROR);
  64. //如果编译出错,javac就会把错误写入到stderr当中,用一个专门的文件来保存:compileError.txt
  65. String compileError=FileUtil.readFile(COMPILE_ERROR);
  66. System.out.println(compileError+"...");
  67. //编译出错的情况
  68. if(!"".equals(compileError)){
  69. //首先需要设置错误信息:直接返回ERROR
  70. System.out.println("编译出错");
  71. answer.setCode(1);
  72. answer.setReason(compileError);
  73. return answer;
  74. }
  75. //编译没有出错,得到.class文件,继续往下执行
  76. //第四步:调用java命令并且执行代码
  77. //运行程序的时候,也会把java子进程的标准输入和标准输出获取到:stdout.txt,stderr.txt
  78. String runCmd=String.format("java -classpath %s %s", WORK_DIR,CLASS);
  79. long start=System.currentTimeMillis();
  80. Thread thread=new Thread(() -> CommandUtil.run(runCmd,STDOUT,STDERR));
  81. thread.start();
  82. try {
  83. thread.join(problem.getSeconds());
  84. } catch (InterruptedException e) {
  85. e.printStackTrace();
  86. }
  87. long end=System.currentTimeMillis();
  88. //说明超时了
  89. if(end-start>=problem.getSeconds()){
  90. answer.setCode(4);
  91. answer.setReason("您的代码提交超时");
  92. return answer;
  93. }
  94. //检验是否运行出错(运行异常)
  95. String runError=FileUtil.readFile(STDERR);
  96. //运行出错的情况
  97. if(!"".equals(runError)){
  98. System.out.println("运行异常");
  99. answer.setCode(2);
  100. answer.setReason(runError);
  101. return answer;
  102. }
  103. //正常运行
  104. answer.setCode(0);
  105. answer.setStdout(FileUtil.readFile(STDOUT));
  106. return answer;
  107. }
  108. public static void main(String[] args) {
  109. Task task=new Task();
  110. Question question=new Question();
  111. question.setCode("class Solution { public int[] twoSum(int[] nums, int target) { /*你好*/int[] a={0,1};return a;}public static void main(String[] args){\n" +
  112. " Solution slo=new Solution();\n" +
  113. " //\n" +
  114. " int[] result1=slo.twoSum(new int[]{2,7,11,15},9);\n" +
  115. " if(result1.length==2&&result1[0]==0&&result1[1]==1){\n" +
  116. " System.out.println(\"Test OK\");\n" +
  117. " }else{\n" +
  118. " System.out.println(\"Test Error\");\n" +
  119. " }\n" +
  120. " //\n" +
  121. " int[] result2=slo.twoSum(new int[]{3,2,4},6);\n" +
  122. " if(result2.length==2&&result2[0]==1&&result2[1]==2){\n" +
  123. " System.out.println(\"Test OK\");\n" +
  124. " }else{\n" +
  125. " System.out.println(\"Test Error\");\n" +
  126. " }\n" +
  127. "\n" +
  128. " }\n" +
  129. "}");
  130. Problem problem=new Problem();
  131. problem.setSeconds(4000);
  132. Answer answer=task.compileAndRun(question, problem);
  133. System.out.println("测试类当中的Code:"+question.getCode());
  134. System.out.println(answer.getCode());
  135. System.out.println(answer.getStderr());
  136. System.out.println(answer.getStdout());
  137. }
  138. }

五、项目模块2:封装读取文件的操作(FileUtil)

封装读取文件的方法

给定一个指定的文件路径,返回文件的所有内容。

  1. public static String readFile(String filePath){
  2. //负责把filePath对应的文件内容读取出来,放到返回值当中
  3. StringBuilder buffer=new StringBuilder();
  4. try (FileReader fileReader=new FileReader(filePath)){
  5. while (true){
  6. int ch= fileReader.read();
  7. if(ch==-1){
  8. break;
  9. }
  10. buffer.append((char)ch);
  11. }
  12. } catch (IOException e) {
  13. e.printStackTrace();
  14. }
  15. return buffer.toString();
  16. }

封装写入文件的方法

往指定的filePath当中写入内容:

  1. public static void write(String filePath,String content){
  2. //获取到文件的路径
  3. try (FileWriter fileWriter=new FileWriter(filePath)){
  4. //写入到指定的内容当中
  5. fileWriter.write(content);
  6. } catch (IOException e) {
  7. e.printStackTrace();
  8. }
  9. }

六、题目管理模块

6.1封装一个数据库连接类

  1. /**
  2. * 数据库连接的工具类
  3. * @author 25043
  4. */
  5. public class DataBaseUtil {
  6. /**
  7. * 配置数据库连接的URL
  8. */
  9. private static final String URL="jdbc:mysql://127.0.0.1:3306/MyOJSystem?characterEncoding=utf8&useSSL=false";
  10. /**
  11. * 用户名
  12. */
  13. private static final String USERNAME="root";
  14. /**
  15. * 密码
  16. */
  17. private static final String PASSWORD="20021111aA#";
  18. private volatile static MysqlDataSource dataSource=null;
  19. public static DataSource getDataSource(){
  20. if(dataSource==null){
  21. synchronized (DataBaseUtil.class){
  22. if(dataSource==null){
  23. dataSource=new MysqlDataSource();
  24. dataSource.setURL(URL);
  25. dataSource.setPassword(PASSWORD);
  26. dataSource.setUser(USERNAME);
  27. }
  28. }
  29. }
  30. return dataSource;
  31. }
  32. public static Connection getConnection() throws SQLException {
  33. return getDataSource().getConnection();
  34. }
  35. public static void close(PreparedStatement preparedStatement, Connection connection, ResultSet resultSet){
  36. if(resultSet!=null){
  37. try {
  38. resultSet.close();
  39. } catch (SQLException e) {
  40. e.printStackTrace();
  41. }
  42. }
  43. if(preparedStatement!=null){
  44. try {
  45. preparedStatement.close();
  46. } catch (SQLException e) {
  47. e.printStackTrace();
  48. }
  49. }
  50. if(connection!=null){
  51. try {
  52. connection.close();
  53. } catch (SQLException e) {
  54. e.printStackTrace();
  55. }
  56. }
  57. }
  58. }

6.2设计题目表

设计统一增删改查封装:BaseDao

  1. /**
  2. * 对于增删改查方法的统一封装
  3. *
  4. * @author 25043
  5. */
  6. public class BaseDao {
  7. /**
  8. * 查询集合
  9. * sql语句@param sql
  10. * 传入的二进制字节码@param clazz
  11. * 参数数组@param args
  12. * 泛型@param <T>
  13. * 集合@return
  14. */
  15. public <T> ArrayList<T> queryList(String sql, Class<T> clazz, Object... args) {
  16. //创建泛型集合
  17. ArrayList<T> list = new ArrayList<>();
  18. ResultSet resultSet = getResultSet(sql, args);
  19. try {
  20. //获取结果集的信息
  21. //1、assert <boolean表达式> 如果<boolean表达式>为true,则程序继续执行。 如果为false,则程序抛出AssertionError,并终止执行。
  22. assert resultSet != null;
  23. ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
  24. //获取列数
  25. int colum = resultSetMetaData.getColumnCount();
  26. //遍历结果集
  27. while (resultSet.next()) {
  28. //通过字节码.getDeclaredConstructor()来获取构造器,并且通过newInstance()方法获取到一个对象
  29. T t = clazz.getDeclaredConstructor().newInstance();
  30. //为当前的对象属性赋值
  31. workForField(colum, t, resultSetMetaData, clazz, resultSet);
  32. //赋值完成之后添加到对应的集合当中
  33. list.add(t);
  34. }
  35. return list;
  36. } catch (SQLException | NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchFieldException | AssertionError e) {
  37. e.printStackTrace();
  38. //像上级抛出自定义的异常
  39. throw new DaoException("executeUpdate方法预编译sql语句异常", e);
  40. } finally {
  41. DataBaseUtil.close(null,null,resultSet);
  42. }
  43. }
  44. /**
  45. * 为属性赋值
  46. * 列数@param colum
  47. * 被赋值的对象@param t
  48. * 获取结果集信息的类@param resultSetMetaData
  49. * 被赋值对象的二进制字节码对象@param clazz
  50. * 结果集@param resultSet
  51. * 泛型集合@param <T>
  52. * 异常@throws SQLException
  53. * SQL异常@throws NoSuchFieldException
  54. * 反射异常@throws IllegalAccessException
  55. */
  56. private <T> void workForField(int colum, T t, ResultSetMetaData resultSetMetaData, Class<T> clazz, ResultSet resultSet) throws SQLException, NoSuchFieldException, IllegalAccessException {
  57. for (int i = 0; i < colum; i++) {
  58. //获取列名称
  59. String columName = resultSetMetaData.getColumnLabel(i + 1);
  60. //获取列的属性columnValue值为列名,数据库的列名称,columnValue为数据库表中的数据
  61. Object columnValue = resultSet.getObject(columName);
  62. //如果没有此属性,会报异常:noSuchFieldException
  63. Field field = clazz.getDeclaredField(columName);
  64. //无视属性修饰符
  65. field.setAccessible(true);
  66. //设置属性值,t代表对应的对象,columValue代表对应的值
  67. field.set(t, columnValue);
  68. }
  69. }
  70. /**
  71. * 查询单个对象
  72. * 查询的sql语句@param sql
  73. * 类的二进制字节码文件@param clazz
  74. * 数组@param args
  75. * 泛型@param <T>
  76. * 被查询的对象@return
  77. */
  78. public <T> T queryObject(String sql, Class<T> clazz, Object... args) {
  79. ResultSet resultSet = getResultSet(sql, args);
  80. //创建泛型集合
  81. try {
  82. //获取结果集的信息
  83. assert resultSet != null;
  84. ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
  85. //获取列数
  86. int colum = resultSetMetaData.getColumnCount();
  87. //遍历结果集
  88. T t = null;
  89. while (resultSet.next()) {
  90. t = clazz.getDeclaredConstructor().newInstance();
  91. workForField(colum, t, resultSetMetaData, clazz, resultSet);
  92. }
  93. return t;
  94. } catch (SQLException | NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchFieldException | AssertionError e) {
  95. e.printStackTrace();
  96. throw new DaoException("Dao层发生了sql语句异常", e);
  97. } finally {
  98. DataBaseUtil.close(null,null,resultSet);
  99. }
  100. }
  101. /**
  102. * 封装增删改的方法
  103. * sql语句@param sql
  104. * 可变参数数组@param args
  105. * 执行的行数@return
  106. */
  107. public int update(String sql, Object... args) {
  108. PreparedStatement preparedStatement = null;
  109. Connection connection = DataBaseUtil.getConnection();
  110. System.out.println(connection);
  111. try {
  112. preparedStatement = connection.prepareStatement(sql);
  113. int i = 1;
  114. if (args.length > 0) {
  115. for (Object object : args) {
  116. preparedStatement.setObject(i, object);
  117. i++;
  118. }
  119. }
  120. return preparedStatement.executeUpdate();
  121. } catch (SQLException e) {
  122. e.printStackTrace();
  123. throw new DaoException("Dao层发生了sql语句异常", e);
  124. } finally {
  125. DataBaseUtil.close(preparedStatement,connection,null);
  126. }
  127. }
  128. /**
  129. * 获取结果集
  130. * sql语句@param sql
  131. * 参数数组@param args
  132. * 结果集@return
  133. */
  134. private ResultSet getResultSet(String sql, Object... args) {
  135. Connection connection;
  136. PreparedStatement preparedStatement;
  137. try {
  138. int i = 1;
  139. connection = DataBaseUtil.getConnection();
  140. preparedStatement = connection.prepareStatement(sql);
  141. //遍历数组
  142. if (args.length > 0) {
  143. for (Object object : args) {
  144. preparedStatement.setObject(i, object);
  145. i++;
  146. }
  147. }
  148. return preparedStatement.executeQuery();
  149. } catch (SQLException e) {
  150. e.printStackTrace();
  151. throw new DaoException("Dao层发生了sql语句异常", e);
  152. }
  153. }
  154. }

为Problem类设置以下的属性:

  1. /**
  2. * @author 25043
  3. */
  4. public class Problem {
  5. /**
  6. * 题目的主键ID
  7. */
  8. private int id;
  9. private String tittle;
  10. /**
  11. * 题目的难度
  12. */
  13. private String level;
  14. /**
  15. * 题目的描述
  16. */
  17. private String description;
  18. /**
  19. * 模板代码,也就是题干
  20. */
  21. private String templateCode;
  22. /**
  23. * 测试用例代码
  24. */
  25. private String testCode;
  26. public int getId() {
  27. return id;
  28. }
  29. public void setId(int id) {
  30. this.id = id;
  31. }
  32. public String getTittle() {
  33. return tittle;
  34. }
  35. public void setTittle(String tittle) {
  36. this.tittle = tittle;
  37. }
  38. public String getLevel() {
  39. return level;
  40. }
  41. public void setLevel(String level) {
  42. this.level = level;
  43. }
  44. public String getDescription() {
  45. return description;
  46. }
  47. public void setDescription(String description) {
  48. this.description = description;
  49. }
  50. public String getTemplateCode() {
  51. return templateCode;
  52. }
  53. public void setTemplateCode(String templateCode) {
  54. this.templateCode = templateCode;
  55. }
  56. public String getTestCode() {
  57. return testCode;
  58. }
  59. public void setTestCode(String testCode) {
  60. this.testCode = testCode;
  61. }
  62. }

建表语句如下:

  1. create table oj_table(
  2. id int primary key auto_increment,
  3. title varchar(50),
  4. level varchar(50),
  5. description varchar(4096),
  6. templateCode varchar(4096),
  7. testCode varchar(4096)
  8. );

设置ProblemDao的方法(对于Problem的crud封装):

  1. /**
  2. * 封装了对于problem的增删改查方法
  3. * @author 25043
  4. */
  5. public class ProblemDao extends BaseDao{
  6. public int insert(Problem problem){
  7. String sql="insert into oj_table values(null,?,?,?,?,?)";
  8. return this.update(sql,problem.getTittle(),
  9. problem.getLevel(),
  10. problem.getDescription(),
  11. problem.getTemplateCode(),
  12. problem.getTestCode());
  13. }
  14. public int delete(int id){
  15. String sql="delete from oj_table where id=?";
  16. return this.update(sql,id);
  17. }
  18. public List<Problem> selectAll(){
  19. String sql="select id,tittle,level from oj_table";
  20. return this.queryList(sql,Problem.class);
  21. }
  22. public Problem selectOne(int problemId){
  23. String sql="select*from oj_table where id=?";
  24. return this.queryObject(sql,Problem.class,problemId);
  25. }
  26. }

题目的测试用例(TestCode如何设置)

       测试用例代码就是一个mian方法,然后需要在这个方法内部创建Solution实例,并且通过这个实例调用核心方法(例如leetCode02的两数之和)

       在调用核心方法的时候,传入不同的参数,并且针对返回的结果进行判定

       如果返回结果符合预期,那么显示TestOK,如果不符合,那么就显示"Test failed"

       并且打印出错的详情。


       因此,设计的大致思路就是:

       服务器当中会收到用户提交的Solution代码,然后再从数据库当中查询到测试用例代码,二者进行一个拼接。那么此时Solution类就会有main方法了,就可以单独进行编译和运行了。

       代码实现:        

       完成拼接之后,直接编译运行拼接之后的代码。

       所以,在设置数据库的测试用例字段的时候,只需要把右侧的main方法String的格式存入到problem的测试用例当中。


七、Web模块

7.1题目列表页

这一个页面负责展示所有题目的列表。类似于leetCode上面点击了"题库"之后看到的内容。

请求:GET 路径: /problem

  1. @WebServlet("/problem")
  2. public class ProblemServlet extends HttpServlet {
  3. private ObjectMapper objectMapper=new ObjectMapper();
  4. @Override
  5. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  6. ProblemDao problemDao=new ProblemDao();
  7. String idString=req.getParameter("id");
  8. //尝试获取id参数,如果不能获取到,说明是查询题目列表
  9. if(idString==null||"".equals(idString)){
  10. List<Problem> problems=problemDao.selectAll();
  11. String respString =objectMapper.writeValueAsString(problems);
  12. resp.setCharacterEncoding("utf8");
  13. resp.getWriter().write(respString);
  14. }else{
  15. //如果能获取到,说明是查询题目详情
  16. Problem problem=problemDao.selectOne(Integer.parseInt(idString));
  17. String respString=objectMapper.writeValueAsString(problem);
  18. resp.getWriter().write(respString);
  19. }
  20. }
  21. }

响应:json格式

  1. [
  2. {
  3. id :1,
  4. tittle : "两数之和",
  5. level : "简单"
  6. },
  7. {
  8. id : 2,
  9. tittle : "两数相加",
  10. level : "中等",
  11. }
  12. ]

7.2题目详情页

功能1:展示题目的详细要求

 请求:GET /problem?id=...

 代码同上,但是要注意传递的参数为1.


响应:

  1. {
  2. id :1,
  3. tittle : "两数之和",
  4. level : "简单",
  5. description : "题目的详细要求,包括题干,输入输出信息等等...",
  6. templateCode : "模板代码",
  7. }


功能2:能够拥有一个编辑框,让用户来编写代码,并且提交代码

请求:POST 路径:/compile

提交的数据:(服务器端需要新建一个内部类来接受请求的json数据)

  1. {
  2. id :1,
  3. code :"编辑框当中的代码...(也就是用户输入的代码)"
  4. }

          用户提交的代码就是一个核心代码,但是如果想要编译+运行,还是需要令用户提交的代码+main方法(也就是测试用例代码),才可以进行编译+运行。

 关于第二步:图解一下 

readBody方法内部就是读取contentLength的长度


关于第三步:图解一下

       由于用户提交的是一个大括号+核心代码块的样式的。

       因此,为了把main方法嵌套到用户提交的solution内部,应当考虑:寻找到最后一个"}"所在的位置,然后在这个位置之前拼接测试用例的main方法,再拼接上这个"}"。返回一个finalCode


关于第四步:

需要构建一个Question类来进行设置code。并且调用Task类来编译Question类的code。


响应:json格式

服务器端需要一个内部类来表示响应的json数据

  1. {
  2. error : 0,
  3. reason : "出错的原因",
  4. stdout : "测试用例的输出情况,包含了通过几个用例这样"
  5. }

同时,也需要考虑:用户输入的代码是否恶意等等的情况。 

功能2整体servlet代码实现

  1. @WebServlet("/compile")
  2. public class CompileServlet extends HttpServlet {
  3. /**
  4. * 用于表示接受请求
  5. */
  6. static class CompileAndRequest{
  7. public int id;
  8. public String code;
  9. }
  10. /**
  11. * 用于表示响应
  12. */
  13. static class CompileAndResponse{
  14. public int error;
  15. public String reason;
  16. public String stdout;
  17. }
  18. @Override
  19. protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  20. CompileAndResponse compileAndResponse=new CompileAndResponse();
  21. req.setCharacterEncoding("utf8");
  22. resp.setContentType("application/json;charset=utf8");
  23. //先读取正文,并且按照json的格式进行解析
  24. String body=readBody(req);
  25. ObjectMapper mapper=new ObjectMapper();
  26. //解析json对象
  27. //body是一个json格式的字符串
  28. CompileAndRequest compileAndRequest=mapper.readValue(body,CompileAndRequest.class);
  29. //根据id从数据库当中查找到题目的详情==>得到测试用例代码
  30. ProblemDao problemDao=new ProblemDao();
  31. //根据id查询problem,并且道道测试用例代码
  32. Problem problem= problemDao.selectOne(compileAndRequest.id);
  33. try {
  34. if(problem==null) {
  35. throw new ProblemNotFoundException();
  36. }
  37. //测试用例代码
  38. String testCode=problem.getTestCode();
  39. //用户提交代码
  40. String requestCode=compileAndRequest.code;
  41. //二者进行一个拼接,变成可编译的代码
  42. String finalCode=mergeCode(testCode,requestCode);
  43. if(finalCode==null){
  44. throw new CodeInValidException();
  45. }
  46. //把用户提交的代码和测试用例代码,拼接成一个完整的携带main方法的代码,可以进行编译+运行
  47. System.out.println("Servlet的Code"+finalCode);
  48. //创建Task实例,调用里面的compileAndRun来进行编译运行’
  49. Task task=new Task();
  50. Question question=new Question();
  51. question.setCode(finalCode);
  52. Answer answer=task.compileAndRun(question);
  53. //根据Task运行的结果,包装成一个HTTP响应
  54. compileAndResponse.error=answer.getCode();
  55. compileAndResponse.reason=answer.getReason();
  56. compileAndResponse.stdout=answer.getStdout();
  57. }catch (ProblemNotFoundException e){
  58. compileAndResponse.error=3;
  59. compileAndResponse.reason="题目没有找到 id="+compileAndRequest.id;
  60. }catch (CodeInValidException e){
  61. compileAndResponse.error=3;
  62. compileAndResponse.reason="提交的代码不符合要求";
  63. }finally {
  64. String respString=mapper.writeValueAsString(compileAndResponse);
  65. resp.getWriter().write(respString);
  66. }
  67. }
  68. private static String mergeCode(String testCode, String requestCode) {
  69. //拼接思路:把testCode给放到solution的最后一个大括号的前面即可
  70. //查找requestCode的最后一个}
  71. int pos=requestCode.lastIndexOf("}");
  72. System.out.println(pos);
  73. if(pos==-1){
  74. //说明提交的代码完全没有},显然是非法的代码
  75. return null;
  76. }
  77. //截取到最后一个大括号之前的字符串
  78. String subStr=requestCode.substring(0,pos);
  79. //拼接
  80. return subStr + testCode + "\n" + "}";
  81. }
  82. private static String readBody(HttpServletRequest req) throws UnsupportedEncodingException {
  83. //第一步:获取到请求头的contentLength字段长度(单位:字节)
  84. int contentLength=req.getContentLength();
  85. //第二步:按照这个长度准备一个byte数组
  86. byte[] buffer=new byte[contentLength];
  87. try (InputStream inputStream=req.getInputStream()){
  88. //基于这个流对象进行读取
  89. inputStream.read(buffer);
  90. } catch (IOException e) {
  91. e.printStackTrace();
  92. }
  93. //指定字符编码
  94. return new String(buffer,"utf8");
  95. }
  96. }

八、前端模块

对于这一模块,不会展开详细的介绍;大致就是一个题目列表页+一个代码详情编辑页。

具体的代码可以参考项目源码的webapps目录下面的各个目录。(前端页面我COPY来的)

https://gitee.com/wangjiaxin20021111/OJSystem/tree/master/https://gitee.com/wangjiaxin20021111/OJSystem/tree/master/

      项目测试文档在ProjecTest文件夹下面

题目列表页:

 题目详情+编辑页:

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/548023
推荐阅读
相关标签
  

闽ICP备14008679号