目录
一、认识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