一、准备工作
这篇博客我们分三部分来讲解如何实现一个在线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 {
public static int run(String cmd,String stdoutFile,String stderrFile){
try {
Process process=Runtime.getRuntime().exec(cmd);
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();
}
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();
}
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;
private String stdin;
public String getCode(){
return code;
}
public void setCode(String code){
this.code=code;
}
}
3.创建Answer类(编译的结果)
编译的结果总共有三种编译出错/运行出错/运行正确.
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;
}
}
4.创建Task类,表示一次编译的过程(最重要的一部)
每次的“编译”加“运行”,被称为Task。 这里需要理解
javac 是java语言编程编译器。全称java compiler。javac工具读由java语言编写的类和接口的定义,并将它们编译成字节代码的class文件。javac 可以隐式编译一些没有在命令行中提及的源文件。用 -verbose 选项可跟踪自动编译。当编译源文件时,编译器常常需要它还没有识别出的类型的有关信息。对于源文件中使用、扩展或实现的每个类或接口,编译器都需要其类型信息。这包括在源文件中没有明确提及、但通过继承提供信息的类和接口。
这里就不得不提我们需要打开cmd看看输入cmd有反应,如果没有我们需要在环境变量里面引入jdks的环境变量。
java中的文件名和类名是一样的
我们就把question的文件写入,java的solution中去。
public class Task {
public Answer compileAndRun(Question question) {
}
}
在编译运行过程中可能会生成一些临时文件. 这里统一用临时文件的方式表示. 并约定命名. 这些临时文件放到一个统一的目录中. 这些属性都是 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();
if (ch == -1) {
break;
}
result.append((char) ch);
}
} catch (IOException e) {
e.printStackTrace();
}
return result.toString();
}
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();
}
FileUtil.writeFile(CODE,question.getCode());
String compileCmd=String.format("javac -encoding uft8 %s -d %s",CODE,WORK_DIR);
System.out.println(compileCmd);
CommandUtil.run(compileCmd,null,COMPILE_ERROR);
String compileError=FileUtil.readFile(COMPILE_ERROR);
if (!compileError.equals("")){
answer.setError(1);
answer.setReason(compileError);
return 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模块就做好了。这就是我们的后端不含数据库部分。
|