快乐人的web项目(Servlet)----在线OJ(上)后端部分(不含数据库)

发布时间:2024-05-25 19:01

一、准备工作

这篇博客我们分三部分来讲解如何实现一个在线oj,可以拿牛客网的在线oj系统作为参考,我们这里是一个基础篇。

1.创建项目

使用 IDEA 创建一个 Maven 项目.
1 ) 菜单 -> 文件 -> 新建项目 -> Maven

2) 引入依赖在中央仓库 https://mvnrepository.com/中搜索 "servlet"和mysql, 一般第一个结果就是. (强调一下注意版本,mysql最好用5开头的);
\"在这里插入图片描述\"
\"在这里插入图片描述\"
3)将下面的这些代码复制到pom.xml中\"在这里插入图片描述\"
如下图红色方框所示记得加在”<dependencis“中
\"在这里插入图片描述\"
4)然后点击main如图创建wed.xml\"在这里插入图片描述\"
在该wed.xml界面复制如下代码
“http://java.sun.com/dtd/web-app_2_3.dtd” >会标红此刻我们不需要去搭理他,默认忽略

<!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>

二、编辑模块设计

1.封装CommandUtil类

\"在这里插入图片描述\"
如图在java下面创建一个名为CommandUtil的类在这个类中我们放入如下代码
这里我们会用到文件io的知识和线程等待还有异常处理的知识。

简单提一下字节流和字符流(帮助大家理解)
如果数据所在的文件通过windows自带的记事本打开并能读懂里面的内容,就用字符流,其他用字节流。
如果你什么都不知道,就用字节流。
InputStream & FileInputStream
InputStream字节输入流,用来将文件中的数据读取到java程序
FileInputStream就是他的子类
OutputStream & FileOutputStream
字节输出流,将数据输出到指定文件中,
通过这套组合我们可以把文件A的内容读取出来写入文件B

多进程编程
进程 == “任务”. 是一个 "动作"就是我们打开任务管理器出来的内一堆玩意,多进程是实现并发编程的一种重要实现方式
为什么是进程不是线程?
如果一个进程挂了, 不会影响到其他进程. 如果一个线程挂了, 则整个进程都要异常终止.

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

public class CommandUtil {
    //1.通过Runtime类得到实例,执行exec方法
    //2.希望获取到标准输出,并写入到指定的文件中
    //3.获取到标准的错误,并写入到指定文件中
    //4.等待子进程结束,拿到状态码,并返回。
    public  static  int run(String cmd,String stdoutFile,String stderrFile){
        try {
            //1.通过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.getInputStream();
                FileOutputStream stderrTO=new FileOutputStream(stderrFile);
                while (true){
                    int ch=stderrFrom.read();
                    if(ch==-1){
                        break;
                    }
                    stderrTO.write(ch);
                }
                stderrFrom.close();
                stderrTO.close();
            }
            //进程等待父进程执行到waitfor的时候就会阻塞,直到子进程完毕
            int exitCode=process.waitFor();
            System.out.println(exitCode);
        }catch (IOException |InterruptedException e){
            e.printStackTrace();
        }
        return 1;
    }

}
理解 "标准输入", "标准输出", "标准错误" 这几个重要概念.
需要手动实现重定向的过程.
exec 执行过程是异步的. 可以使用 waitFor 方法阻塞等待命令执行结束.

接下来,基于刚刚准备好的CommandUtil,我们来实现一个完整的“编译运行”这样的模块。
要做的就是,用户输入,程序相应做错出反应,来判断这个oj结果是否正确。因此我们创建如下四类。

基于刚刚准备好的CommandUtil,实现一个完整的编译运行这样的模块。

2. 创建Question类

用这个类来表示要编译代码,一个task的编译代码。我们直接用 String然后直接用get和set方法。

public class Question {
    private String code;
    // 其实这个 stdin 没有用上
    private String stdin;
    public  String getCode(){
        return code;
    }
    public  void setCode(String code){
        this.code=code;
    }
}

3.创建Answer类(编译的结果)

编译的结果总共有三种编译出错/运行出错/运行正确.

public class Answer {
    //错误码如果error为0表示运行ok,1为编译错误,2为运行出错。
    private int error;
    //出错的提示信息,如果error为1,出错了放错误信息,如果为0,放运行出错的信息。
    private String reason;
    //运行程序得到的便准输出
    private String stdout;
    //运行程序得到的标准错误
    private String stderr;
     public int getError() {
        return error;
    }

    public void setError(int error) {
        this.error = error;
    }

    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;
    }
}

4.创建Task类,表示一次编译的过程(最重要的一部)

每次的“编译”加“运行”,被称为Task。
这里需要理解

javac 是java语言编程编译器。全称java compiler。javac工具读由java语言编写的类和接口的定义,并将它们编译成字节代码的class文件。javac 可以隐式编译一些没有在命令行中提及的源文件。用 -verbose 选项可跟踪自动编译。当编译源文件时,编译器常常需要它还没有识别出的类型的有关信息。对于源文件中使用、扩展或实现的每个类或接口,编译器都需要其类型信息。这包括在源文件中没有明确提及、但通过继承提供信息的类和接口。

这里就不得不提我们需要打开cmd看看输入cmd有反应,如果没有我们需要在环境变量里面引入jdks的环境变量。
java中的文件名和类名是一样的
我们就把question的文件写入,java的solution中去。
public class Task {
    // 通过这个方法封装编译命令, 并得到编译运行结果.
    //compileAndRun的意思是编译加运行.
    //返回值就是编译的结果
    public Answer compileAndRun(Question question) {
        // 0. 把question中的文件写入到.java文件中去。
        // 1. 根据 Question 创建.java临时文件,创建子进程,用javac进行编译。需要先将Question中间的文件写入到一个。java的文件中去。
        // 2. 创建子进程,调用javac把错误信息写入到stdout.txt文件stderr.txt文件
        // 3. 创建子进程,调用java并执行,读stdout.txt文件stderr.txt文件
        // 4. 父进程获取到刚刚的结果并包装到最终 Answer 对象中
        //编辑结果通过刚刚的文件获取即可。
    }
}

在编译运行过程中可能会生成一些临时文件. 这里统一用临时文件的方式表示. 并约定命名. 这些临时文件放到一个统一的目录中.
这些属性都是 Task 类的成员因此我们将他放入Task类中

为什么搞这么多临时文件,最主要目的是为了进程间通信
进程和进程之间,是独立存在的,一个进程很难影响到其它进程。
我们这里用的简单粗暴的方法,临时文件。
只要某个东西可以被多个进程同时访问到,就可以用来进行进程间通信。
\"在这里插入图片描述\"

// 存放临时文件的目录.(进程间通信)
private final String WORK_DIR = "./tmp/";
// 编译代码的类名
private final String CLASS = "Solution";
// 编译代码的文件名
private final String CODE = WORK_DIR + "Solution.java";
private final String STDIN = WORK_DIR + "stdin.txt";
//标准输出
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";

5.创建 FileUtil

对于文本来说字符流会很省事。

import java.io.*;

public class FileUtil {
    //负责把文件读取出来
    public static String readFile(String filePath) {
        //多个线程修改同一个变量,才会触发线程安全问题
        StringBuilder result = new StringBuilder();
        try (FileReader fileReader = new FileReader(filePath)) {
            while (true) {
                int ch = fileReader.read();
                // fileReader.read();
                if (ch == -1) {
                    break;
                }
                result.append((char) ch);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result.toString();
    }

    //负责把content写入到filePath对应的文件中
    public static void writeFile(String filePath, String content) {
        try (FileWriter fileWriter = new FileWriter(filePath)) {
            fileWriter.write(content);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

有了以上模块我们就可以编写task代码了,就和在上面的task上提到一样。这就是类的方法。
分析一下步骤。先实例化一个Answer然后创建一个文件用来放入我们写入的代码,然后通过cmd进行编程,判断然后将不同情况返回到不同的文件中去。

 public  Answer compileAndRun(Question question){
        Answer answer=new Answer();
        File workDir=new File(WORK_DIR);
        //如果不存在就创建一个
        if(!workDir.exists()){
            workDir.mkdir();
        }
        //1.把question中的code写入到一个Solution.java文件中。
        FileUtil.writeFile(CODE,question.getCode());

        //2.创建子进程,调用javac编译。
        String compileCmd=String.format("javac -encoding uft8 %s -d %s",CODE,WORK_DIR);
        System.out.println(compileCmd);
        CommandUtil.run(compileCmd,null,COMPILE_ERROR);

        //3.创建子进程,调用java命令并执行
        String compileError=FileUtil.readFile(COMPILE_ERROR);
        if (!compileError.equals("")){
            answer.setError(1);
            answer.setReason(compileError);
            return  answer;
        }
        //4.父进程获取到刚才的编译执行结果,并打包成Answer对象
        return null;
    }
    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);
    }
}

我们用这个代码测试一下然后去tmp找这俩文件会发现Solution文件写入成功,然后compileError.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;
        }

剩下最后一种情况了。正确的情况我们接着写即可

 answer.setError(0);
        answer.setStdout(FileUtil.readFile(STDOUT));
        return answer;

这下我们的task模块就做好了。这就是我们的后端不含数据库部分。

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号