在线OJ(一)
目标:仿照牛客/leetcode这类在线OJ网站,实现在线做题/判题功能
核心功能
- 题目列表页,展示当前系统中所有的题目。
- 题目详情页,显示题目的具体要求和代码模板,提供一个编辑框供用户来编辑代码。
- 代码的编译/运行/测试功能,能够针对用户提交的代码,进行编译运行,并自动执行测试用例,返回测试结果。
编译模块:给定一个java代码文件(只考虑单个文件的情况,不考虑多文件复杂工程)能够通过代码来控制jdk进行编译运行。通过借助Runtime这个对象,可以创建出一个子进程,并且让子进程来执行一个相关的命令。编译:javac,运行:java
进程与线程
进程是资源分配的基本单位。
线程是CPU调度和执行的基本单位
线程相比于进程的优势:
1.线程更轻量,创建一个线程开销比创建进程低很多;销毁一个线程的开销也比销毁一个进程开销低很多。
2.同一个进程中的所有线程共享着一些数据。
线程相比于进程的劣势:
1.线程代码编写更困难,涉及线程安全。
2.线程代码调试也更加困难。
3.对于程序的稳定性也就提出了更高要求。
CommandUtil
如何利用子进程来执行命令?接下来就是对CommandUtil的讲解,其本质就是利用了jdk的Runtime对象。
exec方法干了两件事:1.创建子进程。2.进程程序替换
这里的运行结果实际上是父进程的输出结果,那么如果我们想要获取子进程的输出结果就需要使用到“重定向”功能,把进程输出的内容写到指定的文件中。具体实现:实现重定向,需要先获取到子进程对象,借助exec返回的Process对象,通过标准输入输出来重定向保存子进程输出结果。
执行结果发现:文件是有了,但是内容没有,为什么?因为一个命令的输出内容也可能是通过标准错误来打印的。操作系统中的任何一个进程,启动的时候都会自动打开三个文件:标准输入、标准输出、标准错误。并通过文件管理的方式,将其组织起来。我们只需要将上图第三步复制,修改为标准错误重定向就好了(process.getErrStream())。
执行结果:
首先,生成了两个对应的文件。
第二,在stderrFile.txt文件中可以看到内容,也就是我们在cmd中直接输入javac的内容。
当然,我们还需要考虑一件事,本身预期目标是用父进程中的run方法来控制子进程执行功能。在执行run的过程中,子进程也在执行。当run执行结束之后,也必须确保子进程也已经执行完了。但是此时这个代码中,子进程和父进程之间是并发关系,谁先执行完时无法确定的,为了能够明确让子进程先执行完,就需要让父进程进行等待。
CommandUtil代码
import java.io.*;
public class CommandUtil {
public static int run(String cmd,String stdoutFile,String stderrFile) throws IOException, InterruptedException {
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec(cmd);
if(stdoutFile != null){
InputStream stdoutFrom = process.getInputStream();
OutputStream stdoutTo = new FileOutputStream(stdoutFile);
int ch = -1;
while((ch = stdoutFrom.read()) != -1){
stdoutTo.write(ch);
}
stdoutFrom.close();
stdoutTo.close();
}
if(stderrFile != null){
InputStream stderrFrom = process.getErrorStream();
OutputStream stderrTo = new FileOutputStream(stderrFile);
int ch = -1;
while((ch = stderrFrom.read()) != -1){
stderrTo.write(ch);
}
stderrFrom.close();
stderrTo.close();
}
int exitCode = process.waitFor();
return exitCode;
}
public static void main(String[] args) throws IOException, InterruptedException {
run("javac","f:/stdoutFile.txt","f:/stderrFile.txt");
}
}
到目前为止,已经实现了一个类,可以帮助我们完成执行某个指定的命令。下一步操作,就需要借助刚才这个类,把整个java程序的编译和运行过程都组合到一起。
CompileAndRun
需要对代码进行编译和运行,那么就需要先将代码(code)和输入(stdin)进行封装。
public class Question {
private String code;
private String stdin;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getStdin() {
return stdin;
}
public void setStdin(String stdin) {
this.stdin = stdin;
}
}
编译运行之后的结果也要进行封装,例如:错误码,出错原因,标准输出,标准错误。
public class Answer {
private int error;
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;
}
@Override
public String toString() {
return "Answer{" +
"error=" + error +
", reason='" + reason + '\'' +
", stdout='" + stdout + '\'' +
", stderr='" + stderr + '\'' +
'}';
}
}
读写文件是比较频繁的操作,所以将其封装为一个工具类,方便使用。
import java.io.*;
public class FileUtil {
public static String readFile(String filePath){
try(FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader)){
StringBuilder stringBuilder = new StringBuilder();
String line = "";
while((line = bufferedReader.readLine()) != null){
stringBuilder.append(line);
}
return stringBuilder.toString();
}catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static void writeFile(String filePath,String content){
try(FileWriter fileWriter = new FileWriter(filePath)){
fileWriter.write(content);
} catch (IOException e) {
e.printStackTrace();
}
}
}
在编译运行过程中依赖了一些临时文件,需要约定一下这些临时文件的名字,这些临时文件就是为了把执行过程中涉及到的各种中间结果都记录下来,方便调试。例如保存在哪个目录下(WORK_DIR)、代码类名(CLASS)、文件名(CODE)、标准输入、标准输出、标准错误、编译错误。
编译运行(comileAndRun)的具体过程:
1.需要先创建好临时文件的目录。
2.根据Question对象,构造需要的临时文件(文件名,标准输入)。
3.构造编译命令并执行,再判断是否编译出错。
4.构造运行命令并执行,再判断是否运行出错。
5.将最终结果封装到Answer并返回。
import java.io.File;
import java.io.IOException;
public class Task {
private static final String WORK_DIR = "./tmp/";
private static final String CLASS = "Solution";
private static final String CODE = "Solution.java";
private static final String STDIN = WORK_DIR + "stdin.txt";
private static final String STDOUT = WORK_DIR + "stdout.txt";
private static final String STDERR = WORK_DIR + "stderr.txt";
private static final String COMPILE_ERROR = WORK_DIR + "compile_error.txt";
public Answer compileAndRun(Question question) throws IOException, InterruptedException {
Answer answer = new Answer();
File worDir = new File(WORK_DIR);
if(!worDir.exists()){
worDir.mkdirs();
}
FileUtil.writeFile(CODE,question.getCode());
FileUtil.writeFile(STDIN,question.getStdin());
String cmd = String.format(
"javac -encoding utf8 %s -d %s",CODE,WORK_DIR
);
System.out.println("编译命令:"+cmd);
CommandUtil.run(cmd,null,COMPILE_ERROR);
String compileError = FileUtil.readFile(COMPILE_ERROR);
if(!"".equals(compileError)){
System.out.println("编译出错");
answer.setError(1);
answer.setReason(compileError);
return answer;
}
cmd = String.format(
"java -classpath %s %s",WORK_DIR,CLASS
);
System.out.println("运行命令:" + cmd);
CommandUtil.run(cmd,STDOUT,STDERR);
String stdError = FileUtil.readFile(STDERR);
if(!"".equals(stdError)){
System.out.println("运行出错");
answer.setError(2);
answer.setReason(stdError);
answer.setStderr(stdError);
return answer;
}
answer.setError(0);
answer.setStdout(FileUtil.readFile(STDOUT));
return answer;
}
public static void main(String[] args) throws IOException, InterruptedException {
Question question = new Question();
question.setCode(
"public class Solution {\n" +
" public static void main(String[] args) {\n" +
" System.out.println(\"hello\");\n" +
" }\n" +
"}\n"
);
question.setStdin("");
Task task = new Task();
Answer answer = task.compileAndRun(question);
System.out.println(answer);
}
}
输出结果
小总结:到目前为止,我们已经可以将代码进行编译运行,并返回执行结果。其实主要难点是写CommandUtil,得理解对Runtime对象的使用,并了解进程的相关知识。后面再整合编译和运行过程,其实就是对我们再熟悉不过的java运行过程的实现,先生成.java文件(也就是代码)、将其编译成.class文件、最后运行该文件并返回。
运行之后的目录结构
|