【在线OJ项目】核心技术之用户提交代码的编译运行

目录

一、认识Java进程编程

二、在线OJ核心思路

三、封装进程的执行

四、封装文件读写

五、封装用户提交代码的编译运行


一、认识Java进程编程

在之前的文章里提到了Java进程编程的相关API【JavaEE】Java中进程编程_1373i的博客-CSDN博客https://blog.csdn.net/qq_61903414/article/details/130497143?spm=1001.2014.3001.5501

二、在线OJ核心思路

在线OJ项目重点在于用户完成题目后,服务器获取到代码后,如何  验证代码的正确性。我们可以开启一个进程或线程去执行用户提交的这段代码,但是为什么不选择线程而选择进程呢,因为进程是相互独立的,如果用户提交恶意代码,我们开启一个进程去执行,此时该进程挂掉不会终止服务器进程。如果我们使用线程去执行这段恶意代码,该线程挂掉会导致整个服务器进程挂掉。所以我们选择使用进程去执行用户提交的代码。一个进程在开始执行时,会自动打开3个属于该进程的文件,分别是标准错误文件,标准输入文件,标准输出文件。当我们开启进程去执行用户提交的代码时,该代码执行时的结果(错误或sout的结果,类似在idea终端打印的错误消息相同)就会保存到对应的文件里

 那么相应的用户提交的代码执行的结果(错误、结果)都会在相对应的文件里,我们只需要去读取相对应的文件里的信息然后返回给前端展示给用户即可。所以在线OJ的核心是用户提交代码的编译与运行,核心步骤是:

获取到前端传来的用户提交代码

--》将用户提交的代码保存到一个.java文件

-》然后开启一个进程通过javac命令将该java文件编译为.class文件

-》然后读取该进程的标准错误文件看是否编译出错,如果编译出错则将编译出错的信息(第几行……错误)-

》然后再开启一个进程通过java命令去运行该代码

-》此时我们就可以读取该进程的标准输出文件与标准错误文件对用户提交代码是否满足题意进行校验

三、封装进程的执行

在前面的文章中我们了解了Java中如何创建进程以及如何让进程等待,现在我们需要对创建进程以及对获取进程结束后的三个文件操作进行封装,执行子进程后将该进程执行的结果即(标准输出、标准输入读取到指定的文件里面)在Java中可以用Process类表示进程,后续封装我们需要使用基于该类进行封装



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

/**
 * 执行对应的java进程,将执行结果写入文件
 */
public class ProcessUtil {
    /**
     * 将创建进程执行相应的代码进程封装
     * @param cmd  指令
     * @param stdoutFile 标准输入的文件复制路径
     * @param stderrFile 标准错误的文件复制路径
     * @return 进程执行的状态码
     */
    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.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 e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return 1;
    }
}

四、封装文件读写

封装完了进程创建以及获取进程的结果(标准输出标准错误)后,此时我们需要将从前端获取到的代码保存到一个指定的.java文件里,所以我们对文件的读写进行封装,将文件内容读取到字符串里,以及将字符串内容写入文件

package com.example.demo.common;

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

/**
 * 文件读写封装
 */
public class FileUtil {
    /**
     * 读取文件内容到String中
     * @param path
     * @return
     */
    public static String readFile(String path) {
        // StringBuilder相比String来说更高效,String它追加的底层是StringBuilder.append.toString
        StringBuilder result = new StringBuilder();

        // 字符流读取
        try (FileReader fileReader = new FileReader(path)) {
            while (true) {
                int ch = fileReader.read();
                if (ch == -1) {
                    break;
                }

                result.append((char) ch);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result.toString();
    }

    /**
     * 将内容写入对应文件
     * @param path
     * @param content
     */
    public static void writeFile(String path,String content) {
        try(FileWriter fileWriter = new FileWriter(path)) {
            fileWriter.write(content);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

五、封装用户提交代码的编译运行

通过上述封装之后,我们可以将用户提交的代码写入到指定的文件,此时我们就需对该代码进行编译运行,在此之间我们先对临时文件与代码的类名进行约定,我们把这些临时文件都放在一个文件夹里

// 所有临时文件的目录
private static final String WORD_DIR = "./tmp/";
// 所有代码的类名
private static final String CLASS = "Main";
// 用户上传代保存码文件
private static final String CODE = WORD_DIR + CLASS + ".java";
// 用户上传代码进程的编译时标准错误文件
private static final String COMPILE_ERROR = WORD_DIR + "compileError.txt";
// 用户上传代码进程的标准输出文件
private static final String STDOUT = WORD_DIR + "stdout.txt";
// 用户上传代码进程的运行时标准错误文件
private static final String STDERR = WORD_DIR + "stderr.txt";

此时我们需要两个对象,一个对象用户表示从前端获取的代码信息

package com.example.demo.model;

import lombok.Data;

/**
 * 表示:用户提交的代码
 */
@Data
public class Question {
    private String code;  // 代码
}

 一个表示用户提交代码执行的结果

package com.example.demo.model;

import lombok.Data;

/**
 *执行结果
 */
@Data
public class Answer {
    private int error;      // 0--ok  1--error 2--throw
    private String reason;
    private String stdout;  // 标准输入
    private String stderr;  // 标准错误
}

此时我们就可以对编译运行进行封装:

思路

首先我们需要将用户提交的代码写入一个java文件,然后我们需要创建一个进程进行编译,然后我们需要查看编译是否出错,也就是查看编译进程的标准错误文件,看是否为空,空则表示无错误,继续进行,如果不为空则表示存在错误,我们则读取该文件将错误信息进行封装后返回。在编译完成后,我们需要创建另一个进程进行运行编译的class文件,运行完成后,我们依旧需要读取运行进程的标准错误文件看是否为空,不空则说明可能存在运行时异常,空则读取标准输出文件将内容封装后返回。

了解了思路后,我们开始编写代码

package com.example.demo.common;

import com.example.demo.model.Answer;
import com.example.demo.model.Question;

import java.io.*;

/**
 * 每次 ”编译+运行“  的一个过程就是一个Task
 */
public class Task {

    /**
     * 这些临时文件 服务器进程获取子进程编译运行代码的结果,也就是进程之间通信
     */
    // 所有临时文件的目录
    private static final String WORD_DIR = "./tmp/";
    // 所有代码的类名
    private static final String CLASS = "Main";
    // 用户上传代保存码文件
    private static final String CODE = WORD_DIR + CLASS + ".java";
    // 用户上传代码进程的编译时标准错误文件
    private static final String COMPILE_ERROR = WORD_DIR + "compileError.txt";
    // 用户上传代码进程的标准输出文件
    private static final String STDOUT = WORD_DIR + "stdout.txt";
    // 用户上传代码进程的运行时标准错误文件
    private static final String STDERR = WORD_DIR + "stderr.txt";

    /**
     * 编译运行代码
     * @param question
     * @return
     */
    public Answer compileAndRun(Question question)  {
        Answer answer = new Answer();

        // 0.创建临时目录
        File workDir = new File(WORD_DIR);
        if (!workDir.exists()) {
            // 目录不存在,创建目录
            workDir.mkdirs();
        }

        // 1.将question里的code(用户提交的代码)写入java文件  :类名与文件名必须相同,此处规定为Main.java
        FileUtil.writeFile(CODE,question.getCode());

        // 2.创建子进程,调用javac命令编译   如果编译出错就会写入标准错误文件
        String compileCmd = String.format("javac -encoding utf8 %s -d %s",CODE,WORD_DIR);
        System.out.println("编译命令生成:" + compileCmd);
        ProcessUtil.run(compileCmd,null,COMPILE_ERROR); // 开始编译

            // 读取编译错误文件:如果为空则编译正确,如果有内容则编译有错误
        String compileError = FileUtil.readFile(COMPILE_ERROR);
        if (!compileError.equals("")) {
            // 编译错误,构造错误信息返回
            System.out.println("编译出错:" + compileError);
            answer.setError(1);
            answer.setReason(compileError);
            return answer;
        }

        // 3.创建子进程,调用java命令执行    会把标准输入与标准输出获取到
        String runCmd = String.format("java -classpath %s %s",WORD_DIR,CLASS);
        System.out.println("运行命令生成:" + runCmd);
        ProcessUtil.run(runCmd,STDOUT,STDERR);

            // 读取运行时标准错误文件, 正常情况用户不可能存在标准错误。如果该文件空则正常,如果不为空则存在异常
        String runError = FileUtil.readFile(STDERR);
        if (!runError.equals("")) {
            // 运行时出错,存在异常
            System.out.println("运行出错:" + runError);
            answer.setError(2);
            answer.setStderr(runError);
            return answer;
        }

        // 4.父进程获取编译结果,打包为Answer对象进行返回
        String runOut = FileUtil.readFile(STDOUT);
        answer.setError(0);
        answer.setStdout(runOut);

        return answer;
    }

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        Task task = new Task();
        Question question = new Question();
        question.setCode("hello main");
        task.compileAndRun(question);
    }
}

要注意的是一个java的类名必须与文件名相同所以我们在约定文件名时必须规定前端用户输入代码创建类时需提示用户类名。

项目gitee地址1886i (PG1886) - Gitee.comhttps://gitee.com/PG1886

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
SJTU OJ是上海交通大学在线评测系统的简称。它是一个提供给学生练习编程和解决问题的平台。 首先,学生需要注册并登录SJTO OJ系统。系统会为每个注册用户分配一个唯一的用户ID和密码,以保证账户安全。 上机编程练习是SJTO OJ的主要功能之一。学生可以在系统中选择不同的编程题目,例如算法题、数据结构题、数学题等等。每道题目都附带了详细的题目描述和输入输出样例。学生需要根据题目要求,编写相应的程序,并在系统中提交代码。系统会自动编译运行学生提交代码,并对其进行评测。评测结果包括通过样例的数量、程序运行时间、内存占用等信息。 除了上机编程练习,SJTO OJ还提供了一些其他功能。例如,学生可以查看自己的解题记录和成绩,统计自己的编程能力和进步情况。他们可以参加在线比赛,与其他学生一同竞争,提高自己的编程水平。 作为一名学生,使用SJTO OJ可以有效地提升自己的编程技能和解决问题的能力。通过参与编程练习和比赛,学生可以不断学习新知识,发现并改进自己的不足之处。此外,SJTO OJ还为学生提供了一个交流的平台,他们可以与其他学生分享自己的解题思路和经验。 总之,SJTO OJ是一个非常有用的在线评测系统,通过使用它,学生可以提高自己的编程能力,并享受与其他同学交流和竞争的乐趣。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

1886i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值