JavaWeb 项目 --- 在线 OJ 平台 (一)

x33g5p2x  于2022-05-24 转载在 Java  
字(8.1k)|赞(0)|评价(0)|浏览(582)

1. 项目设计

① 题目列表页 (展示当前的所有题目)
② 题目详情页 (展示当前的题目详情)
③ 题目代码编辑功能 (详情页里,能够编辑代码)
④ 题目提交功能 (详情页里,编辑完成后,可以提交代码的功能)

2. 项目效果图

3. 创建项目

① 创建一个 maven 项目

② 创建 webapp/WEB-INF/web.xml

③ 写入 web.xml

  1. <!DOCTYPE web-app PUBLIC
  2. "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
  3. "http://java.sun.com/dtd/web-app_2_3.dtd" >
  4. <web-app>
  5. <display-name>Archetype Created Web Application</display-name>
  6. </web-app>

④ 导入依赖

  1. <dependencies>
  2. <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
  3. <dependency>
  4. <groupId>mysql</groupId>
  5. <artifactId>mysql-connector-java</artifactId>
  6. <version>5.1.26</version>
  7. </dependency>
  8. <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
  9. <dependency>
  10. <groupId>javax.servlet</groupId>
  11. <artifactId>javax.servlet-api</artifactId>
  12. <version>3.1.0</version>
  13. <scope>provided</scope>
  14. </dependency>
  15. </dependencies>

⑤ 验证 创建 HelloServlet

  1. import javax.servlet.ServletException;
  2. import javax.servlet.annotation.WebServlet;
  3. import javax.servlet.http.HttpServlet;
  4. import javax.servlet.http.HttpServletRequest;
  5. import javax.servlet.http.HttpServletResponse;
  6. import java.io.IOException;
  7. @WebServlet("/hello")
  8. public class HelloServlet extends HttpServlet {
  9. @Override
  10. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  11. resp.getWriter().write("hello");
  12. }
  13. }

⑥ 运行 smartTomcat

4. 项目的前置知识

4.1 文件的IO操作

本项目 需要从一个文件中读取信息 也需要给文件写入信息.就需要用到文件的IO操作.
具体看博客: 文件的IO操作

示例: 了解读文件写文件

  1. import java.io.*;
  2. public class TestIO {
  3. public static final String srcPath = "./tmp/text1.txt";
  4. public static final String destPath = "./tmp/text2.txt";
  5. public static void main(String[] args) throws IOException {
  6. FileInputStream fileInputStream = new FileInputStream(srcPath);
  7. FileOutputStream fileOutputStream = new FileOutputStream(destPath);
  8. while(true){
  9. int ch = fileInputStream.read();
  10. if(ch == -1){
  11. break;
  12. }
  13. fileOutputStream.write(ch);
  14. }
  15. fileInputStream.close();
  16. fileOutputStream.close();
  17. }
  18. }

运行之后

4.2 进程和线程

当前的项目是一个在线OJ的平台, 虽然线程相比于进程更轻量, 多个线程之间共用着同一个进程的地址空间, 某个线程挂了, 就可能把整个进程也搞挂了.
如果是多进程, 某个进程挂了, 就不会影响其他的进程.
用户提交的代码, 可能出现很多问题. 所以这里要采用多进程的方法来执行.

Java 中 就可以使用Runtime.exec方法来解决这个问题.
这个方法的参数是一个字符串, 表示一个可执行的路径. 执行这个方法就, 就会把指定的可执行程序, 创建出进程并执行.

标准输入 标准输出 标准错误

  1. 标准输入 : 对应到键盘
  2. 标准输出 : 对应到显示器
  3. 标准错误 : 对应到显示器

示例: 进程创建

  1. import java.io.FileInputStream;
  2. import java.io.FileOutputStream;
  3. import java.io.IOException;
  4. import java.io.InputStream;
  5. public class TestExec {
  6. public static void main(String[] args) throws IOException {
  7. // Runtime 在 JVM 中是一个单例
  8. Runtime runtime = Runtime.getRuntime();
  9. // 1. 进程的创建
  10. Process process = runtime.exec("javac");
  11. // 获取到子进程的标准输出和标准错误, 把这里的内容写入两个文件.
  12. // a. 标准输出
  13. InputStream stdoutFrom = process.getInputStream();
  14. FileOutputStream stdoutTo = new FileOutputStream("stdout.txt");
  15. while(true){
  16. int ch = stdoutFrom.read();
  17. if(ch == -1) break;
  18. stdoutTo.write(ch);
  19. }
  20. stdoutFrom.close();
  21. stdoutTo.close();
  22. // b. 标准错误
  23. InputStream stderrFrom = process.getErrorStream();
  24. FileOutputStream stderrTo = new FileOutputStream("stderr.txt");
  25. while(true) {
  26. int ch = stderrFrom.read();
  27. if (ch == -1) break;
  28. stderrTo.write(ch);
  29. }
  30. stderrFrom.close();
  31. stderrTo.close();
  32. }
  33. }

示例: 进程等待

想要把用户的代码, 编译执行之后,再把响应返回给用户. 就需要把进程执行的顺序进行调整.

这段代码添加到上面代码后面就ok了

  1. // 2. 进程等待
  2. // 执行到这里就会阻塞等待, 直到子进程执行完毕
  3. int exitCode = process.waitFor();
  4. // 会输出错误码
  5. System.out.println(exitCode);

5. 编译功能的实现

创建一个 包compile 用来放编译功能的代码

创建一个 CommandUtil 类

这个类是用来对命令行进行调用的.
通过执行cmd命令. 将标准输出或标准错误写入到对应的文件.并返回状态码.
具体实现:

  1. package compile;
  2. import java.io.FileOutputStream;
  3. import java.io.IOException;
  4. import java.io.InputStream;
  5. public class CommandUtil {
  6. /**
  7. * 1. 通过 Runtime 类得到 Runtime 实例, 执行 exec 方法
  8. * 2. 获取到标准输出, 并写入到指定文件中
  9. * 3. 获取到标准错误, 并写入到指定文件中
  10. * 4. 等待子进程结束, 拿到子进程的状态码
  11. * @param cmd cmd 中的命令
  12. * @param stdoutFile 标准输出文件地址
  13. * @param stderrFile 标准错误文件地址
  14. * @return 返回状态码
  15. */
  16. public static int run(String cmd, String stdoutFile, String stderrFile) {
  17. try {
  18. // 1. 通过 Runtime 类得到 Runtime 实例, 执行 exec 方法
  19. Process process = Runtime.getRuntime().exec(cmd);
  20. // 2. 获取到标准输出, 并写入到指定文件中
  21. if (stdoutFile != null) {
  22. InputStream stdoutFrom = process.getInputStream();
  23. FileOutputStream stdoutTo = new FileOutputStream(stdoutFile);
  24. while (true) {
  25. int ch = stdoutFrom.read();
  26. if (ch == -1) {
  27. break;
  28. }
  29. stdoutTo.write(ch);
  30. }
  31. stdoutFrom.close();
  32. stdoutTo.close();
  33. }
  34. // 3. 获取到标准错误, 并写入到指定文件中
  35. if (stderrFile != null) {
  36. InputStream stderrFrom = process.getErrorStream();
  37. FileOutputStream stderrTo = new FileOutputStream(stderrFile);
  38. while (true) {
  39. int ch = stderrFrom.read();
  40. if (ch == -1) {
  41. break;
  42. }
  43. stderrTo.write(ch);
  44. }
  45. stderrFrom.close();
  46. stderrTo.close();
  47. }
  48. // 4. 等待子进程结束, 拿到子进程的状态码
  49. int exitCode = process.waitFor();
  50. return exitCode;
  51. } catch (IOException | InterruptedException e) {
  52. e.printStackTrace();
  53. }
  54. return 1;
  55. }
  56. }

测试这个类:

  1. public static void main(String[] args) {
  2. CommandUtil.run("javac", "stdout.txt","stderr.txt");
  3. }

创建一个类 Question

这个类是放的要编译运行的代码.

  1. /**
  2. * 这是包含了要编译的代码
  3. */
  4. public class Question {
  5. private String code;
  6. public String getCode() {
  7. return code;
  8. }
  9. public void setCode(String code) {
  10. this.code = code;
  11. }
  12. }

创建一个类 Answer

这个类是放的运行后的结果.
首先有一个状态码, 表示当前的运行的状态.
有一个reason. 表示出错的信息.
有一个stdout 表示程序得到的标准输出的结果
有一个stderr, 表示待续得到的标准错误的结果

  1. public class Answer {
  2. // error 为状态码.
  3. // 0 编译通过
  4. // 1 表示编译出错
  5. // 2 表示运行出错
  6. // 3 表示其他错误
  7. private int error;
  8. // reason 为出错的提示信息.
  9. // error=1, reason 就是错误信息
  10. // error=2, reason 就是异常信息
  11. private String reason;
  12. // 运行程序得到的标准输出的结果
  13. private String stdout;
  14. // 运行程序得到的标准错误的结果
  15. private String stderr;
  16. //...一堆getter和setter 省略
  17. }

创建一个类 FileUtil

这个类放到 common 包里, 这个类封装了对文件的读写操作

  1. package common;
  2. import java.io.FileReader;
  3. import java.io.FileWriter;
  4. import java.io.IOException;
  5. /**
  6. * 读写文件的操作
  7. */
  8. public class FileUtil {
  9. /**
  10. * 读文件
  11. * @param filePath 读取的文件
  12. * @return 返回读取的内容
  13. */
  14. public static String readFile(String filePath) {
  15. StringBuilder result = new StringBuilder();
  16. try(FileReader fileReader = new FileReader(filePath)){
  17. while (true) {
  18. int ch = fileReader.read();
  19. if (ch == -1){
  20. break;
  21. }
  22. result.append((char)ch);
  23. }
  24. } catch (IOException e) {
  25. e.printStackTrace();
  26. }
  27. return result.toString();
  28. }
  29. /**
  30. * 写文件
  31. * @param filePath 要写入的文件
  32. * @param content 写入的内容
  33. */
  34. public static void writeFile(String filePath, String content) {
  35. try(FileWriter fileWriter = new FileWriter(filePath)){
  36. fileWriter.write(content);
  37. } catch (IOException e) {
  38. e.printStackTrace();
  39. }
  40. }
  41. }

创建一个类 Task

这个类表示一次运行编译的结果
传入一个要编译的代码 question, 返回编译运行后的结果 answer

首先需要约定一系列临时文件的名字

  1. // 约定临时文件所在的目录
  2. private final String WORK_DIR = "./tmp/";
  3. // 约定代码的类名
  4. private final String CLASS = "Solution";
  5. // 约定要编译的代码文件名
  6. private final String CODE = WORK_DIR + "Solution.java";
  7. // 约定存放编译错误信息的文件名
  8. private final String COMPILE_ERROR = WORK_DIR + "compile_error.txt";
  9. // 约定存放运行时的标准输出的文件名
  10. private final String STDOUT = WORK_DIR + "stdout.txt";
  11. // 约定存放运行时的标准错误的文件名
  12. private final String STDERR = WORK_DIR + "stderr.txt";

实现 compileAndRun方法

  1. package compile;
  2. import common.FileUtil;
  3. import java.io.File;
  4. import java.util.ArrayList;
  5. import java.util.List;
  6. import java.util.UUID;
  7. /**
  8. * Task 运行的结果
  9. */
  10. public class Task {
  11. // 约定临时文件所在的目录
  12. private final String WORK_DIR = "./tmp/";
  13. // 约定代码的类名
  14. private final String CLASS = "Solution";
  15. // 约定要编译的代码文件名
  16. private final String CODE = WORK_DIR + "Solution.java";
  17. // 约定存放编译错误信息的文件名
  18. private final String COMPILE_ERROR = WORK_DIR + "compile_error.txt";
  19. // 约定存放运行时的标准输出的文件名
  20. private final String STDOUT = WORK_DIR + "stdout.txt";
  21. // 约定存放运行时的标准错误的文件名
  22. private final String STDERR = WORK_DIR + "stderr.txt";
  23. /**
  24. * 编译 + 运行
  25. * @param question 要编译运行的 java 源代码
  26. * @return 编译运行的结果
  27. */
  28. public Answer compileAndRun(Question question) {
  29. Answer answer = new Answer();
  30. // 创建临时文件的目录
  31. File workDir = new File(WORK_DIR);
  32. if(!workDir.exists()){
  33. System.out.println("创建成功!");
  34. workDir.mkdirs();
  35. }
  36. // 1. 把 question 中的 code 写入到一个 Solution.java 文件中
  37. FileUtil.writeFile(CODE,question.getCode());
  38. // 2. 创建子进程, 调用 javac 进行编译. (这里需要 .java 文件)
  39. // 如果编译出错, 放入到 compileError.txt
  40. String compileCmd = String.format("javac -encoding utf8 %s -d %s",CODE,WORK_DIR);
  41. // 对于 javac 进程来说, 不关心他的标准输出.
  42. CommandUtil.run(compileCmd,null,COMPILE_ERROR);
  43. // 读取编译错误的信息.
  44. String compileError = FileUtil.readFile(COMPILE_ERROR);
  45. if (!"".equals(compileError)){
  46. // 编译错误
  47. // 返回 Answer 让 Answer中记录编译错误的信息.
  48. System.out.println("编译出错");
  49. answer.setError(1);
  50. answer.setReason(compileError);
  51. return answer;
  52. }
  53. // 3. 创建子进程, 调用 java 命令并执行
  54. // 运行程序时候, 获取 java 子进程的标准输出 和 标准错误
  55. String runCmd = String.format("java -classpath %s %s",WORK_DIR,CLASS);
  56. CommandUtil.run(runCmd,STDOUT,STDERR);
  57. String runError = FileUtil.readFile(STDERR);
  58. if (!"".equals(runError)) {
  59. System.out.println("运行出错!");
  60. answer.setError(2);
  61. answer.setReason(runError);
  62. return answer;
  63. }
  64. // 4. 父进程获取到刚才的编译执行的结果, 并打包成 Answer对象
  65. answer.setError(0);
  66. answer.setStdout(FileUtil.readFile(STDOUT));
  67. return answer;
  68. // 编译执行的结果, 就通过刚刚约定的这几个文件来获取即可
  69. }
  70. }

相关文章