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

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

1. 项目设计

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

2. 项目效果图

3. 创建项目

① 创建一个 maven 项目

② 创建 webapp/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>

④ 导入依赖

<dependencies>
        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.26</version>
        </dependency>
        <!-- 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>
    </dependencies>

⑤ 验证 创建 HelloServlet

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;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("hello");
    }
}

⑥ 运行 smartTomcat

4. 项目的前置知识

4.1 文件的IO操作

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

示例: 了解读文件写文件

import java.io.*;

public class TestIO {
    public static final String srcPath = "./tmp/text1.txt";
    public static final String destPath = "./tmp/text2.txt";
    public static void main(String[] args) throws IOException {
        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();
    }
}

运行之后

4.2 进程和线程

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

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

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

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

示例: 进程创建

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class TestExec {
    public static void main(String[] args) throws IOException {
        // Runtime 在 JVM 中是一个单例
        Runtime runtime = Runtime.getRuntime();
        // 1. 进程的创建
        Process process = runtime.exec("javac");
        // 获取到子进程的标准输出和标准错误, 把这里的内容写入两个文件.
        // a. 标准输出
        InputStream stdoutFrom = process.getInputStream();
        FileOutputStream stdoutTo = new FileOutputStream("stdout.txt");
        while(true){
            int ch = stdoutFrom.read();
            if(ch == -1) break;
            stdoutTo.write(ch);
        }
        stdoutFrom.close();
        stdoutTo.close();
        // b. 标准错误
        InputStream stderrFrom = process.getErrorStream();
        FileOutputStream stderrTo = new FileOutputStream("stderr.txt");
        while(true) {
            int ch = stderrFrom.read();
            if (ch == -1) break;
            stderrTo.write(ch);
        }
        stderrFrom.close();
        stderrTo.close();
    }
}

示例: 进程等待

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

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

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

5. 编译功能的实现

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

创建一个 CommandUtil 类

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

package compile;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class CommandUtil {
    /**
     * 1. 通过 Runtime 类得到 Runtime 实例, 执行 exec 方法
     * 2. 获取到标准输出, 并写入到指定文件中
     * 3. 获取到标准错误, 并写入到指定文件中
     * 4. 等待子进程结束, 拿到子进程的状态码
     * @param cmd cmd 中的命令
     * @param stdoutFile 标准输出文件地址
     * @param stderrFile 标准错误文件地址
     * @return 返回状态码
     */
    public static int run(String cmd, String stdoutFile, String stderrFile) {
        try {
            // 1. 通过 Runtime 类得到 Runtime 实例, 执行 exec 方法
            Process process = Runtime.getRuntime().exec(cmd);
            // 2. 获取到标准输出, 并写入到指定文件中
            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();
            }
            // 3. 获取到标准错误, 并写入到指定文件中
            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();
            }
            // 4. 等待子进程结束, 拿到子进程的状态码
            int exitCode = process.waitFor();
            return exitCode;
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
        return 1;
    }
}

测试这个类:

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

创建一个类 Question

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

/**
 * 这是包含了要编译的代码
 */
public class Question {
    private String code;

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }
}

创建一个类 Answer

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

public class Answer {
    // error 为状态码.
    // 0 编译通过
    // 1 表示编译出错
    // 2 表示运行出错
    // 3 表示其他错误
    private int error;
    // reason 为出错的提示信息.
    // error=1, reason 就是错误信息
    // error=2, reason 就是异常信息
    private String reason;
    // 运行程序得到的标准输出的结果
    private String stdout;
    // 运行程序得到的标准错误的结果
    private String stderr;

	//...一堆getter和setter 省略
}

创建一个类 FileUtil

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

package common;

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

/**
 * 读写文件的操作
 */
public class FileUtil {
    /**
     * 读文件
     * @param filePath 读取的文件
     * @return 返回读取的内容
     */
    public static String readFile(String filePath) {
        StringBuilder result = new StringBuilder();
        try(FileReader fileReader = new FileReader(filePath)){
            while (true) {
                int ch = fileReader.read();
                if (ch == -1){
                    break;
                }
                result.append((char)ch);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result.toString();
    }
    /**
     * 写文件
     * @param filePath 要写入的文件
     * @param content 写入的内容
     */
    public static void writeFile(String filePath, String content) {
        try(FileWriter fileWriter = new FileWriter(filePath)){
            fileWriter.write(content);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

创建一个类 Task

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

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

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

实现 compileAndRun方法

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 final String WORK_DIR = "./tmp/";
    // 约定代码的类名
    private final String CLASS = "Solution";
    // 约定要编译的代码文件名
    private final String CODE = WORK_DIR + "Solution.java";
    // 约定存放编译错误信息的文件名
    private final String COMPILE_ERROR = WORK_DIR + "compile_error.txt";
    // 约定存放运行时的标准输出的文件名
    private final String STDOUT = WORK_DIR + "stdout.txt";
    // 约定存放运行时的标准错误的文件名
    private final String STDERR = WORK_DIR + "stderr.txt";

    /**
     * 编译 + 运行
     * @param question 要编译运行的 java 源代码
     * @return 编译运行的结果
     */
    public Answer compileAndRun(Question question) {
        Answer answer = new Answer();
        // 创建临时文件的目录
        File workDir = new File(WORK_DIR);
        if(!workDir.exists()){
            System.out.println("创建成功!");
            workDir.mkdirs();
        }
        // 1. 把 question 中的 code 写入到一个 Solution.java 文件中
        FileUtil.writeFile(CODE,question.getCode());
        // 2. 创建子进程, 调用 javac 进行编译. (这里需要 .java 文件)
        // 如果编译出错, 放入到 compileError.txt
        String compileCmd = String.format("javac -encoding utf8 %s -d %s",CODE,WORK_DIR);
        // 对于 javac 进程来说, 不关心他的标准输出.
        CommandUtil.run(compileCmd,null,COMPILE_ERROR);
        // 读取编译错误的信息.
        String compileError = FileUtil.readFile(COMPILE_ERROR);
        if (!"".equals(compileError)){
            // 编译错误
            // 返回 Answer 让 Answer中记录编译错误的信息.
            System.out.println("编译出错");
            answer.setError(1);
            answer.setReason(compileError);
            return answer;
        }
        // 3. 创建子进程, 调用 java 命令并执行
        // 运行程序时候, 获取 java 子进程的标准输出 和 标准错误
        String runCmd = String.format("java -classpath %s %s",WORK_DIR,CLASS);
        CommandUtil.run(runCmd,STDOUT,STDERR);
        String runError = FileUtil.readFile(STDERR);
        if (!"".equals(runError)) {
            System.out.println("运行出错!");
            answer.setError(2);
            answer.setReason(runError);
            return answer;
        }
        // 4. 父进程获取到刚才的编译执行的结果, 并打包成 Answer对象
        answer.setError(0);
        answer.setStdout(FileUtil.readFile(STDOUT));
        return answer;
        // 编译执行的结果, 就通过刚刚约定的这几个文件来获取即可

    }
}

相关文章