在线OJ项目(1)------实现代码编译运行功能

1)什么是在线OJ:在线网页版本的编程平台,打开一个网站,上面就可以看到很多的算法题,我们就可以在线做题,在线提交,提交之后就能看到提交运行结果,是否运行通过

leetcode,牛客网,在线做题平台;

2)一个在线OJ平台的核心功能:

1)在数据库里面可以进行管理题目,保存很多的题目信息,题目信息包括题干+测试用例;

2)可以进行展示题目列表页,通过一个网页,可以把我们所有的题目进行罗列,可以展示题目列表

3)可以进行展示题目详情页,展示某个题的详细信息,以及提供代码编辑框,每一个题目都对应着一个题目详情页;

4)提交并运行题目:在我们的题目详情页里面有一个提交按钮,咱们的网页的所写的执行代码给发送到后台服务器上面,服务器就会执行这些代码,并且进行正确性的判断,给出一些是否通过用例的结果;

5)查看运行结果:有另一个结果页面,能展示上次提交是否通过,以及错误的用例信息

在我们的代码编辑框下面有一个正方形来表示展示结果,只要我们提交代码,我们就可以在这个正方形下面展示我们的所写代码执行测试用例的结果;

1.创建一个Maven项目,引入依赖http://mvnrepository.com/

1)MYSQL connector(5.1.49)

2)Servlet(3.1)

3)Jackon:2.13.0用来识别json格式的字符串

加上dependency标签里面存放我们引入的依赖

我们需要在main目录下面创建两个目录,第一个目录是java目录,里面存放我们的Java代码,另一个目录是webapp里面存放我们的前端代码,然后我们创建WEB-INF目录,创建web.xml文件,直接复制粘贴

3.进程和线程

1)进程也是可以称之为任务的,操作系统要想完成一个具体的动作,就要创建出对应的进程,一个程序没有运行的时候,就被称之为是一个可执行文件,是磁盘上的一份普通数据,但是当一个程序跑起来之后进行实现,就被称之为进程了;

2)为了实现并发编程,同时执行多个任务,我们就引入了多进程编程,我们会把一个大的任务,进行拆分成很多小的任务,创建多个进程,让每一个进程负责出其中的一部分任务;

3)这样也就带来了一个问题,创建销毁进程是比较低效的,比较重量,那么我们又引入了线程,每一个线程都是一个独立的执行流,一个进程包含了一个或者多个线程,因此在Java这个圈子里面,大部分的并发编程都是通过多线程的方式来进行的,但是说多进程编程的方式也是可以的,创建线程销毁线程比创建进程销毁进程更高效;

4)进程相比于线程的优势:在于说进程的独立性,在操作系统上面,同一时刻是在运行着很多的进程的,如果说某一个进程挂啦,是不会影响到其他进程的,每一个进程有着各自的地址空间,相比之下,由于多个线程之间,是在共用这同一个进程的地址空间,如果说某一个线程挂啦,就很有可能把整个进程带走,这个时候这个进程的所有线程也会崩溃,所以说所进程编程比多线程编程要更加稳定;

5)在这个项目中,进程带来的稳定性是十分重要的

对于我们的在线OJ项目来说,有一个服务器进程,运行着Servlet,接收用户的请求,并进行返回响应,来进行处理各个客户端的请求,用户在这里面执行的代码,其实也是一个独立的逻辑,这个逻辑时使用多进程比较好,因为我们无法进行控制用户到底提交了什么样的代码,代码很有可能是出现问题的,很有可能是一运行就会出现崩溃的,如果我们使用多线程,就会很有可能会因为某一个用户的代码把整个服务器进程都带走的糟糕情况;

7)这样说,如果说采用多线程的方式,那就意味着一个服务器是要给很多用户进行服务的,不是在给一个用户使用,而是在给多个用户提供服务,如果说因为某一个用户提交的代码把服务器给带走了,那就不好了,其他用户就用不了;

6)Java中进行多进程编程,主要是做这几件事,站在操作系统的角度来讲,linux是提供了很多和多进程编程相关的API接口,比如说进程创建,进程终止,进程程序替换,进程间的通信,进程等待,但是在Java中对系统的提供的这些操作进行了限制,最终只给用户提供了两个操作,进程创建,进程等待

7)创建一个新的进程,让这个新的进程来进行执行一系列的任务

子进程:被创建出来的进程,称之为子进程

父进程:创建子进程的进程称之为父进程;

重要:

咱们的服务器进程,就相当于是父进程

然后根据用户发送过来的代码,创建出子进程

8)每一个用户提交了一份代码,服务器(父进程)就要创建出一个子进程来进行处理,来进行处理这段逻辑,一个父进程可以有多个子进程,但是一个子进程只能有一个父进程;

9)Runtime是JDK(java.lang)中自带的一个类,通过这个类,我们就可以直接创建出子进程,

Runtime runtime=Runtime.getRuntime(),创建一个RunTime实例

调用Runtime中的exec方法,里面的参数是一个字符串,表示一个可执行程序的路径,执行这个方法,就会把指定路径的可执行程序,创建出进程并执行;

10)Runtime在JVM是一个单例模式,我们可以通过getRuntime()方法得到唯一的实例,会出现IOException;

11)一个进程在进行启动的时候,会自动地打开三种文件:

这里面的文件是指一些硬件设备和软件资源

1)标准输入----对应到键盘

2)标准输出-----对应到显示器,程序运行成功之后的输出结果就写到了这个文件里面

3)标准错误------对应到显示器

配置环境变量去特定的系统路径里面找这个JAVAC命令对应的可执行文件

1)Process process= runtime.exec("javac"),当我们在执行这一段代码的时候,执行exec方法的时候,就相当于在cmd里面进行输入了一个对应的指令命令(javac)

Process类就是一个进程

1.1)但是咱们的操作系统并不会认识这个javac是啥,输入的命令,其实操作系统会去一些特定的目录中去找,看看是否存在上述这个对应的可执行文件,如果存在才可以执行,就会出现提示,如果命令不是内部或者外部命令,也不是可运行的程序,比如说你输入一个notePad,或者calc这样的命令计算机都可以找到对应的可执行文件,它会在特定的路径中找

1.2)找不到,就不会出现上面的提示,因为javac不是系统自带的命令,抛出一个IOException,因为程序路径有可能找不到,找不到javac这样的可执行文件

通过上面的语句块就可以进行创建出一个子进程,就可以执行我们的命令了

2)解决这个问题的方法(给定命令,系统找不到对应的可执行文件),就是将javac所在的目录进行配置到PATH环境变量里面

右键此电脑,属性,搜索环境变量,编辑系统环境变量直接就出现了下面的这样的界面:

点击上面环境变量:上面是用户变量,下面是系统变量

进行修改用户或者系统都是可以的,我们就直接修改上面的path,这里面的path就代表操作系统到哪些目录中查找命令对应的可执行文件

1)我们进行点击path之后,就可以直接点击新建,添加目录,我们直接进行查找javac.exe这样的可执行程序,我们进行复制路径,直接粘贴到新建里面去,就可以点击确定就行

2)像javac这样的命令,他是一个控制台程序,他的输出,是输出到标准输出和标准错误这两个特殊的文件里面的,我们要想看到这个程序的运行效果,就必须要获取到标准输出和标准错误里面的内容,比如说在exec里面放一个nodepad啥的,这样的程序是一个图形化界面的程序,点击运行之后,我们就可以看到一个图形化界面的窗口,就可以直接看到结果

我们直接通过Process对象就可以拿到标准输出和标准错误里面的内容了

3)虽然子进程在启动之后同样打开了三个文件,是有操作系统进行控制的,但是由于子进程没有和IDEA的终端进行关联,因此在IDEA中是看不到子进程的输出的,我们要想获取到输出,就只有通过代码(Process)调用方法加上读取文件来手动获取到;

javac是一个控制台程序,这个javac进程所输出的结果,是输出到标准输出和标准错误两个文件里面的

执行一个javac进程的时候,希望把标准输出和标准错误分别写到两个文件里面

先进行读取标准输入,标准输出,然后再写到我们电脑上面的指定文件

   //1.runtima在JVM里面是一个单例模式
    Runtime runtime=Runtime.getRuntime();
    //2.process表示是一个进程
   Process process= runtime.exec("javac");
   //3.获取到子进程的标准输出和标准错误,把这里面的内容写到两个文件里面,运行子进程我们就可以看到程序最终的一个执行效果了
    //3.1先获取到标准输出,从这个文件对象里面读,就可以把子进程的标准输出读出来
    InputStream readinputstdout= process.getInputStream();
    FileOutputStream writeinputstdout=new FileOutputStream("stdoutinput.txt");
    while(true)
    {
        byte[] arr1=new byte[4096];
        int len= readinputstdout.read(arr1);
        if(len==-1)
        {
            break;
        }
        writeinputstdout.write(arr1);
    }
    readinputstdout.close();
    writeinputstdout.close();
    //3.2获取到标准错误
    InputStream readerrorstdout=process.getErrorStream();
    FileOutputStream writeerrorstdout=new FileOutputStream("stdouterror.txt");
    while(true)
    {
        byte[] arr2=new byte[4096];
        int len= readerrorstdout.read(arr2);
        if(len==-1)
        {
            break;
        }
        writeerrorstdout.write(arr2);
    }
    readinputstdout.close();
    writeinputstdout.close();
}

进程等待:通过上述方式上面代码确实可以创建出子进程,但是父子进程执行的过程中(我们的main方法和Runtime.ecec()执行逻辑),是并发执行的关系

另一方面也需要父进程知道子进程的执行状态

一)在我们当前的场景中,我们希望父进程在等待子进程执行完毕之后,再进行执行后续代码,像咱们的在线OJ系统,我们需要让用户提交代码,编译执行代码,肯定是需要编译执行完毕之后,再把响应返回给用户,只有编译运行之后,才可以返回结果;

二)不希望刚刚执行到运行我们所写的代码的时候,我们的main方法,服务器就返回结果给前端了

1)先进行创建Javac子进程,我们要让javac子进程执行完了,得到最终的编译结果

2)当我们的Javac子进程执行完了之后,得到编译的结果,如果Javac子进程的编译结果出错(标准错误文件里面有代码,那么运行模块不用去执行,直接返回结果给前端);

3)Javac命令进程执行完毕之后,我们要创建Java进程执行运行模块,我们也是让Java子进程执行完毕之后,在返回结果给前端,我们一定是说等到编译运行模块都执行完毕了,再让我们的服务器返回响应,让我们的父亲进程知道子进程的一个执行状态,不能说编译运行执行一半,结果就返回了,所以说编译执行的状态必须约定好;

4)一方面要知道子进程的执行状态,看到子进程执行之后的效果,另一方面又要知道编译运行的结果

1)我们可以通过process类的waitFor方法来进行实现进程等待,当我们的父亲进程执行到waitFor的时候,就会出现阻塞,一直执行到子进程执行完为止,和Thread.join()类似,控制线程谁先结束,谁后结束,返回值是一个整数,这个退出码就表示子进程的执行结果是否OK
2)子进程是代码执行完了正常退出,那么返回的退出码就是0,如果说子进程执行了一半就异常退出,那么此时返回的退出码就是非0的
int exitCode= process.waitFor();
System.out.println(exitCode);
现在代码的结构是这样的:

会出现Process finished with exist code=0;

这个子进程是通过Idea的父进程来进行创建的,idea会获取到这个进程的退出码,然后就在终端中显示

在线OJ项目的开始编写:

1)进行封装上面的代码,创建CommitUtil类,里面只有一个方法,期望执行某一个命令,创建子进程来完成具体的工作;

我们在实现编译运行模块的时候,就可以调用这样的一个类实现javac编译,通过java运行这样的一个过程了

我们进行创建一个类,叫做CommitUtil类,实现run方法,我们希望这个run方法可以干一些事情,注意我们的正式的编译操作过程要执行一次run方法,咱们的正式的运行操作过程要执行一次run方法,每一次run方法,都要传递第三个参数

我们最终返回的是一个状态码,如果说程序正常执行完,就返回0

程序异常退出那么就返回1

一:我们要执行的命令到底是什么,是javac还是java?

二:我们把进程运行得到的标准错误结果要写到哪一个文件路径路径里面,是一个String类型,它的值可以是空,如果用户传递的是空,表名我们不需要关注标准错误里面的内容

三:我们把进程运行得到的标准输出结果要写道哪一个文件路径里面,是一个String类型

执行步骤:

1)通过Runtime类进行获取到Runtime实例,并执行exec方法,执行指定的命令

2)获取到标准输入,并写入到指定文件里面

3)获取到标准错误,并写入到指定的文件里面

4)等待子进程结束,并拿到子进程的状态码,并进行返回;

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

public class CommitUtil {
    static int run(String command,String stdoutFile,String stderrorFile) throws IOException, InterruptedException {
//1.通过Runtime类得到Runtime实例,并执行exec方法
        Runtime runtime=Runtime.getRuntime();
        Process process=runtime.exec(command);
//2获取到标准输出,并写入到指定文件里面,如果说不进行指定具体的标准输出指定的文件里面,我们就不会关心了
        if(!stdoutFile.equals("")&&stdoutFile!=null)
        {
            InputStream inputStream= process.getInputStream();
            FileOutputStream outputStream=new FileOutputStream(stdoutFile);
            while(true)
            {
                byte[] arr1=new byte[4096];
                int len=inputStream.read(arr1);
                if(len==-1)
                {
                    System.out.println("标准输出文件已经读完");
                    break;
                }
                outputStream.write(arr1);
            }
            inputStream.close();
            outputStream.close();
        }
//3.获取到标准错误,并写到指定的文件路径
        if(!stderrorFile.equals("")&&stderrorFile!=null)
        {
            InputStream inputStream=process.getErrorStream();
             FileOutputStream outputStream=new FileOutputStream(stderrorFile);
             while(true)
             {
                 byte[] arr2=new byte[4096];
                 int len=inputStream.read(arr2);
                 if(len==-1)
                 {
                     System.out.println("标准错误文件已经读取完毕");
                     break;
                 }
                 outputStream.write(arr2);
             }
             inputStream.close();
             outputStream.close();
        }
//3.等待子进程结束,拿到子进程的退出码,并进行返回
        int index=process.waitFor();
        System.out.println(index);
        return index;

    }

    public static void main(String[] args) throws IOException, InterruptedException {
CommitUtil.run("javac","stdoutinput.txt","stdouterror.txt");
    }
}
注意:写代码的时候这个类只有一个方法

2)代码框架:

来进行创建代码框架的时候,用户提交了一份代码,我们最后要返回最终的编译运行结果

1)我们的参数是要提交的代码,专门进行创建一个类,叫做Question类,里面只有一个参数叫做JavaCode;

2)我们的返回值我们也要专门创建一个类,叫做Answer类;

3)针对我们的编译执行流程,我们也要创建一个类,创建一个方法来完成整个编译运行过程

在Answer类里面,主要有四个参数字段:

1)int error,这个error字段表示错误码,当error字段表示0的时候为编译运行都OK,为1的时候表示编译出错,为2的时候表示运行出异常

2)我们可以根据上面的error值来填写原因,这个字段表示出错的提示信息

如果说error的值是1,那么就表示编译出错了,在reason里面就存放编译的出错信息
如果说error是2,运行异常了,reason里面就进行存放运行时的错误信息

如果说error是3,表示代码中存在一些不安全的东西

3) private String stdout,这个表示运行程序得到标准错误的结果
4) private String stderr,表示运行程序得到标准输出的结果

1)接下来基于刚才准备好的CommandUtil来实现一个完整的编译运行这样的模块

输入:用户提交的代码

输出:返回程序的编译结果和运行结果 

实现:通过Task类里面的complieAndRun方法来实现代码的编译运行功能

2)我们需要在java目录下在进行创建一个类,叫做Task,每一次的编译+运行,我们就称之为是一个Task,Task类里面只有一个方法;

3)我们在Task类里面进行创建一个方法,叫做compileAndRun叫做编译并且运行;

参数:要进行编译运行的java源代码

返回值:表示编译运行的结果,比如说编译出错信息,运行出错信息,运行正确信息

分为上述三种情况

我们针对参数要再进行创建一个类,专门表示要包含编译的代码,叫做Question类,里面主要表示用户提交的代码;

//我们用这个类来进行表示一个Task的输入内容
//表示Task里面的参数,里面包含了要进行编译的代码,用String类型来进行标识
//要进行编译的JAVA源代码
public class Question {
    private String JavaCode;
    public String getJavaCode() {
        return JavaCode;
    }
    public void setJavaCode(String javaCode) {
        JavaCode = javaCode;
    }
}

我们还要针对代码运行的返回值专门进行创建一个类,叫做Answer类,表示代码进行编译运行的结果

//我们用这个类来进行表示Task类的一个返回值
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;
    }
}

执行Task类中的CompileAndRun方法的时候的程序的一个执行过程:

1)因为我们传过来的类的Java源代码是String类型的,我们即将进行编译的时候,要有一个.java文件,要把Question类中的Java代码写入到.java文件里面,但是我们在这里面需要注意我们在Java中,类名是要和文件名是一致的,我们可以进行约定,类名和文件名咱们都叫做Solution,写道Solution.java文件里面;

2.创建子进程,调用javac来进行编译,注意我们进行编译的时候,要有一个.java文件,会生成一个.class文件,注意:我们如果程序编译出错javac就会把我们的编译错误信息写到

stderror里面,我们就可以用一个专门的文件来进行保存,就叫做compileErrorr.txt;

3.创建子进程,调用java命令来进行执行我们的.class文件,这个.class文件是我们上述编译模块自动生成的,注意:当我们在运行程序的时候,我们会把java子进程的标准输出和标准错误获取到,stdout.txt和stderror.txt;

4.咱们的父亲进程会进行获取到刚才编译运行的结果的,会打包成Answer对象,我们进行封装answer对象的时候;

临时文件命名:为了方便我们后续代码的一个维护,我们来进行约定一组常量,来约定临时文件的名字,类名目录名

我们创建的常量:因为.class文件是自动生成的,所以我们不需要手动创建

1)所有的文件的一个根路径:content
2)我们的类名:className
3)我们的.java文件的路径:根路径+类名+".java";
4)我们编译生成的标准错误的路径:
5)我们运行生成的标准错误的路径:
6)我们运行过程中生成的标准输出的路径:

我们要进行创建这么多临时文件的信息,最主要的目的就是为了进程之间通信,进程与进程之间,是存在独立性的,一个进程是很难影响到其他进程的;

服务器进程要拿到用户编写的代码,执行逻辑之后,就要返回最终的执行结果,进程之间要知道进程间的彼此的执行结果,要知道彼此之间的信息;

1)只要某一个东西可以被多个进程之间同时访问到,就可以进行进程之间通信

常用的进程之间通信的方式有管道,消息队列,信号量,信号,socket,文件

2)但是由于javac的代码和java这两个进程之间的代码,都是别人写好的,咱们自己也进行控制不了,咱们也就只能通过文件的方式来进行进程间通信了 

3)虽然在实际开发中最常用的进程间通信的手段是socket(网络编程)

4)服务器进程拿到Java代码,把这个代码写到一个.java文件里面,这样Javac子进程,来就可以读到这个文件,进行编译,生成.class文件,咱们最后Java子进程再去拿到这个.class文件,进行运行,然后咱们的服务器进程读取运行过后的标准输出或者标准错误,封装成Answer类返回给前端,前一个进程写文件,后一个进程读,文件后续进行测试的时候,这些临时文件也很重要;

封装文件操作:

我们要进行大量的文件读写操作,所谓我们将它进行封装成一个类:

这个类主要实现两个方法:

1)一个负责读取文件内容,返回一个字符串,里面的参数是我们要进行读取的文件路径

2)一个负责把字符串写入到整个文件里面,里面的参数是一个字符串,一个是文件路径

//3.这个表示我们要进行编译的代码的文件名:
private static final String FileJava=content+"Solution.java";
//4.约定存放编译时错误信息的文件名
private static final String ErrorClass=content+"CompilerError.txt";
//5.约定好运行时标准输出的文件名
private static final String RunTrue=content+"RunTrueFile.txt";
//6.约定好运行时错误的时候标准错误的文件名
private static final String RunFalse=content+"RunFalseFile.txt";

1)我们后续要进行读写的文件都是这几个,这几个文件都是文本文件,因此我们使用字符流要更好一些

2)我们此时一定要注意:我们在使用文本文件的时候,我们在使用字符流和字节流是都可以进行读写的,但是字符流要比较省事一些,字节流要比较麻烦,因为我们需要手动的进行处理编码格式,尤其是当遇到中文的时候;

import java.io.*;
import java.util.Scanner;

public class OperateFile {
    //1.将文件内容读取成为字符串并返回
    public static String readFile(String FilePath) throws FileNotFoundException {
         StringBuilder stringBuilder=new StringBuilder();
         InputStream inputStream=new FileInputStream(new File(FilePath));
          Scanner scanner=new Scanner(inputStream);
        while(scanner.hasNext())
         {
             String readline= scanner.nextLine();
             stringBuilder.append(readline);
         }
        return stringBuilder.toString();
        StringBuilder stringBuilder=new StringBuilder();
        try(FileReader fileReader=new FileReader(FilePath)){
            while(true) {
                int len = fileReader.read();//这表示一次读一个字符
                if(len==-1)
                {
                    break;
                }
           stringBuilder.append((char)len);

            }

        }catch(FileNotFoundException e)
        {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return stringBuilder.toString();
    }
    //2负责将content写入到filepath对应的文件里里面
    public static void writeFile(String FilePath,String content)
    {
           try(FileWriter fileWriter=new FileWriter(FilePath)) {
               fileWriter.write(content);
           } catch (IOException e) {
               e.printStackTrace();
           }
    }
    public static void main(String[] args) throws FileNotFoundException {
        OperateFile.writeFile("d:/test2.txt","HelloWORD");
    }
}

在上述过程中用StringBuider和StringBuffer都是可以的,因为多个线程同时修改同一个临时变量才会触发线程安全问题,像咱们刚才写的函数里面的局部变量,由于局部变量是在栈上面,而每一个线程又有自己的栈,现在只是修改局部变量;

实现编译功能:

1)编译命令:javac -encoding utf-8 要进行编译的文件所在的路径(包含.java文件所在的路径和Java文件的名字) -d 文件所在的路径(我们要生成的.class文件所在的父亲目录)

javac -encoding utf-8 ./temp/Solution.java -d ./temp/我们此时就拿这个命令来说,第一个路径就表示咱们的即将要进行编译的.java文件所在的目录,-d后面我们执行的路径,就是为了指定我们进行生成的.class文件的位置在哪里,此时我们是要求生成的.class文件和我们的.java文件还有标准错误,标准输出是在同一级目录的;

2)运行命令:java -classpath 目录(.class文件所在的父亲目录)+类名(public)

1)真正的实现编译功能 -d ./temp/这个命令是进行指定生成的class文件在哪里,如果说我们如果不指定好,那么生成的.class文件就跑到其他地方去了,后面我们执行运行操作的时候,可能就是说找不到.class文件的

2)对于javac进程来说,我们只需要关注标准错误,而不进行关注他的标准输出,一旦我们的代码编译错误,内容就会通过标准错误来进行反馈出来

3)Windows简体中文版,默认的字符编码方式是GBK,包括javac命令输出的错误信息,默认也是和系统编码一致,也是GBK,但是IDEA的终端默认编码是UTF-8,此处的乱码我们不关心,linux编码是utf-8

 

4)正常情况下,我们认为用户给系统提供的代码里面,是不应该给标准错误进行打印的,我们可以通过这个标准错误的文件内容,来进行判断文件内容是否出错了,如果说运行出错了,此时的异常信息和调用栈就会在标准错误文件里面

import java.io.File;
import java.io.IOException;
public class Task {
//我们可以通过约定一组常量来进行约定临时文件的名字
//1.这个标识所有临时文件的目录
private static final String content="./temp/";
//2.我们进行约定要进行编译的的代码的类名
private static final String ClassName="Solution";
//3.这个表示我们要进行编译的代码的文件名:
private static final String FileJava=content+"Solution.java";
//4.约定存放编译时错误信息的文件名
private static final String ErrorClass=content+"CompilerError.txt";
//5.约定好运行时标准输出的文件名
private static final String RunTrue=content+"RunTrueFile.txt";
//6.约定好运行时错误的时候标准错误的文件名
private static final String RunFalse=content+"RunFalseFile.txt";
    public Answer compileAndrun(Question question) throws IOException, InterruptedException {
       
        //1.把question的叫做JavaCode的字符串写入到一个叫做Solution.java文件里面,并先创建一个普通目录
        Answer answer=new Answer();
        File file=new File(content);
        if(!file.exists())
        {
            file.mkdirs();
        }
        //2.创建javac子进程,调用javac来进行编译
        OperateFile.writeFile(FileJava,question.getJavaCode());
        //3.创建javac子进程,调用java命令并进行执行
        //此处我们需要先进行构造编译命令
        String command=String.format("javac -encoding utf-8 %s -d %s",FileJava,content);
        System.out.println(command);
        //里面要进行指定标准输出在那个文件,标准错误在哪一个文件
        CommitUtil.run(command,null,ErrorClass);
        //读出javac进行编译结果的标准错误信息,此时有会涉及到读文件操作,如果说我们的编译过程出错了,那么错误信息就会被自动的记录到ErrorClass里面
        //也就是说如果编译没有出错,那么这个文件就是空的
        String str=OperateFile.readFile(ErrorClass);
        if(!str.equals("")||str!=null)
        {
            //如果说文件不为空,那么说明在编译过程中就出现了错误,我们就直接返回一个answer对象来进行返回报错信息
            answer.setError(1);
            answer.setReason("编译出错");
            answer.setStderr(str);
            return answer;
        }else{
           // String
            //4.父进程获取到刚才的编译执行的结果,并将其打包成Answer对象
            String command1=String.format("java -classpath %s %s",content,ClassName);
            System.out.println(command1);
            CommitUtil.run(command1,RunTrue,RunFalse);
            //读取到运行的时候标准错误的文件内容,来进行判断运行是否出错
            String str1=OperateFile.readFile(RunFalse);
            if(!str1.equals(""))
            {
                //运行结果正确
                System.out.println("运行出错");
                answer.setReason("运行出错");
                answer.setError(2);
                answer.setStderr(RunTrue);
                answer.setStderr(RunFalse);
                return answer;
            }else{
                //运行结果错误
                answer.setError(0);
                answer.setStderr(RunTrue);
                return answer;
            }
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        Task task=new Task();
        Question question=new Question();
        question.setJavaCode("public class Solution {\n" +
                "    public static void main(String[] args) {\n" +
                "        System.out.println(\"HelloWorld\");\n" +
                "    }\n" +
                "}\n");
        task.compileAndrun(question);
    }
}
//每一次编译和运行的过程就是一次Task任务
public class Task {
        //1.文件的根目录
        private static final String FatherPath="./temp/";
        //2.javac文件的类名
        private static final String BaseName="Solution";
        //3.原JAVA的文件
        private static final String JavaFile=FatherPath+BaseName+".java";
        //4.生成的编译的标准错误的文件
        private static final String ErrorFile=FatherPath+"ComplieError.txt";
        //5.生成的运行成功的文件
        private static final String RunTrueFile=FatherPath+"RunTrue.txt";
        //6.生成的运行错误的文件
        private static final String RunErrorFile=FatherPath+"RunError.txt";
    public static Response ComplieAndRun(Paramter paramter) throws IOException, InterruptedException {
        //1.判断临时目录是否存在
        Response response=new Response();
        File file=new File(FatherPath);
        if(!file.exists()){
            file.mkdir();
        }
        //2.把用户传递过来的JAVA原字符串写道一个.java文件里面,方便后续进行编译
        OperateFile.WriteFile(paramter.getJavaCode(),JavaFile);
        //3.进行编译命令的编写
        String javac=String.format("javac -encoding utf-8 %s -d %s",JavaFile,FatherPath);
        System.out.println(javac);
        //4.创建JavaC子进程,执行编译命令
        CommitUtil.run(null,ErrorFile,javac);
        //5.读取编译的标准错误的文件,如果文件内容为空,说明编译成功
        String str=OperateFile.ReadFile(ErrorFile);
        //6.如果发现编译的标准错误文件是空,那么继续运行
        if(str==null||str.equals("")){
        String runJava=String.format("java -classpath %s %s",FatherPath,BaseName);
        //7,创建Java子进程,运行JAVA子进程开始执行运行命令
            CommitUtil.run(RunTrueFile,RunErrorFile,runJava);
        //8.开始读取运行成功生成的文件,运行时的标准错误和标准输出
        String RunTrue=OperateFile.ReadFile(RunTrueFile);
        String RunFalse=OperateFile.ReadFile(RunErrorFile);
        //9.进行返回结果,最终返回的是一个Response对象
            if(RunTrue==null||RunTrue.equals("")){
               response.setError(2);
                response.setReason("运行错误");
               response.setStderror(RunFalse);
               return response;
            }else{
                response.setError(0);
                response.setReason("编译运行成功");
                response.setStdout(RunTrue);
                return response;
            }
        }else{
            response.setError(1);
            response.setReason("编译错误");
            response.setStderror("当前编译错误"+str);
            return response;
        }
    }
}

 

所犯的错误:

1)把标准错误读出来的内容直接用字符串来进行接收,最后字符串的引用是str,我们只用if(str!=null)来进行性判断独处的标准错误文件是否为空,这是错误的,应该用str.equals("");

2)Java中建立文件输出流,当文件不存在时会新建一个文件:
如果有同名文件,自动覆盖。不存在时自动建立,FileOutputStream的默认构造方法是直接覆盖掉原来的文件,而FileOutputStream(File file, boolean append) 的构造方法如果后面的append为true的时候就是追加到尾部而不是直接覆盖了

3)现在我们完成的代码结构画一个图:

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值