0


快乐人的web项目(Servlet)----在线OJ(上)后端部分(不含数据库)

在线OJ

一、准备工作

这篇博客我们分三部分来讲解如何实现一个在线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>ArchetypeCreatedWebApplication</display-name></web-app>

二、编辑模块设计

1.封装CommandUtil类

在这里插入图片描述
如图在java下面创建一个名为CommandUtil的类在这个类中我们放入如下代码
这里我们会用到文件io的知识和线程等待还有异常处理的知识。

简单提一下字节流和字符流(帮助大家理解)
如果数据所在的文件通过windows自带的记事本打开并能读懂里面的内容,就用字符流,其他用字节流。
如果你什么都不知道,就用字节流。
InputStream & FileInputStream
InputStream字节输入流,用来将文件中的数据读取到java程序
FileInputStream就是他的子类
OutputStream & FileOutputStream
字节输出流,将数据输出到指定文件中,
通过这套组合我们可以把文件A的内容读取出来写入文件B

多进程编程
进程 == “任务”. 是一个 "动作"就是我们打开任务管理器出来的内一堆玩意,多进程是实现并发编程的一种重要实现方式
为什么是进程不是线程?
如果一个进程挂了, 不会影响到其他进程. 如果一个线程挂了, 则整个进程都要异常终止.

importjava.io.FileOutputStream;importjava.io.IOException;importjava.io.InputStream;publicclassCommandUtil{//1.通过Runtime类得到实例,执行exec方法//2.希望获取到标准输出,并写入到指定的文件中//3.获取到标准的错误,并写入到指定文件中//4.等待子进程结束,拿到状态码,并返回。publicstaticintrun(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=newFileOutputStream(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.getInputStream();FileOutputStream stderrTO=newFileOutputStream(stderrFile);while(true){int ch=stderrFrom.read();if(ch==-1){break;}
                    stderrTO.write(ch);}
                stderrFrom.close();
                stderrTO.close();}//进程等待父进程执行到waitfor的时候就会阻塞,直到子进程完毕int exitCode=process.waitFor();System.out.println(exitCode);}catch(IOException|InterruptedException e){
            e.printStackTrace();}return1;}}
理解 "标准输入","标准输出","标准错误" 这几个重要概念.
需要手动实现重定向的过程.
exec 执行过程是异步的. 可以使用 waitFor 方法阻塞等待命令执行结束.

接下来,基于刚刚准备好的CommandUtil,我们来实现一个完整的“编译运行”这样的模块。
要做的就是,用户输入,程序相应做错出反应,来判断这个oj结果是否正确。因此我们创建如下四类。

基于刚刚准备好的CommandUtil,实现一个完整的编译运行这样的模块。

2. 创建Question类

用这个类来表示要编译代码,一个task的编译代码。我们直接用 String然后直接用get和set方法。

publicclassQuestion{privateString code;// 其实这个 stdin 没有用上privateString stdin;publicStringgetCode(){return code;}publicvoidsetCode(String code){this.code=code;}}

3.创建Answer类(编译的结果)

编译的结果总共有三种编译出错/运行出错/运行正确.

publicclassAnswer{//错误码如果error为0表示运行ok,1为编译错误,2为运行出错。privateint error;//出错的提示信息,如果error为1,出错了放错误信息,如果为0,放运行出错的信息。privateString reason;//运行程序得到的便准输出privateString stdout;//运行程序得到的标准错误privateString stderr;publicintgetError(){return error;}publicvoidsetError(int error){this.error = error;}publicStringgetReason(){return reason;}publicvoidsetReason(String reason){this.reason = reason;}publicStringgetStdout(){return stdout;}publicvoidsetStdout(String stdout){this.stdout = stdout;}publicStringgetStderr(){return stderr;}publicvoidsetStderr(String stderr){this.stderr = stderr;}}

4.创建Task类,表示一次编译的过程(最重要的一部)

每次的“编译”加“运行”,被称为Task。
这里需要理解

javac 是java语言编程编译器。全称java compiler。javac工具读由java语言编写的类和接口的定义,并将它们编译成字节代码的class文件。javac 可以隐式编译一些没有在命令行中提及的源文件。用 -verbose 选项可跟踪自动编译。当编译源文件时,编译器常常需要它还没有识别出的类型的有关信息。对于源文件中使用、扩展或实现的每个类或接口,编译器都需要其类型信息。这包括在源文件中没有明确提及、但通过继承提供信息的类和接口。

这里就不得不提我们需要打开cmd看看输入cmd有反应,如果没有我们需要在环境变量里面引入jdks的环境变量。
java中的文件名和类名是一样的
我们就把question的文件写入,java的solution中去。
publicclassTask{// 通过这个方法封装编译命令, 并得到编译运行结果.//compileAndRun的意思是编译加运行.//返回值就是编译的结果publicAnswercompileAndRun(Question question){// 0. 把question中的文件写入到.java文件中去。// 1. 根据 Question 创建.java临时文件,创建子进程,用javac进行编译。需要先将Question中间的文件写入到一个。java的文件中去。// 2. 创建子进程,调用javac把错误信息写入到stdout.txt文件stderr.txt文件// 3. 创建子进程,调用java并执行,读stdout.txt文件stderr.txt文件// 4. 父进程获取到刚刚的结果并包装到最终 Answer 对象中//编辑结果通过刚刚的文件获取即可。}}

在编译运行过程中可能会生成一些临时文件. 这里统一用临时文件的方式表示. 并约定命名. 这些临时文件放到一个统一的目录中.
这些属性都是 Task 类的成员因此我们将他放入Task类中

为什么搞这么多临时文件,最主要目的是为了进程间通信
进程和进程之间,是独立存在的,一个进程很难影响到其它进程。
我们这里用的简单粗暴的方法,临时文件。
只要某个东西可以被多个进程同时访问到,就可以用来进行进程间通信。
在这里插入图片描述

// 存放临时文件的目录.(进程间通信)privatefinalString WORK_DIR ="./tmp/";// 编译代码的类名privatefinalString CLASS ="Solution";// 编译代码的文件名privatefinalString CODE = WORK_DIR +"Solution.java";privatefinalString STDIN = WORK_DIR +"stdin.txt";//标准输出privatefinalString STDOUT = WORK_DIR +"stdout.txt";//标准错误privatefinalString STDERR = WORK_DIR +"stderr.txt";//错误信息privatefinalString COMPILE_ERROR = WORK_DIR +"compile_error.txt";

5.创建 FileUtil

对于文本来说字符流会很省事。

importjava.io.*;publicclassFileUtil{//负责把文件读取出来publicstaticStringreadFile(String filePath){//多个线程修改同一个变量,才会触发线程安全问题StringBuilder result =newStringBuilder();try(FileReader fileReader =newFileReader(filePath)){while(true){int ch = fileReader.read();// fileReader.read();if(ch ==-1){break;}
                result.append((char) ch);}}catch(IOException e){
            e.printStackTrace();}return result.toString();}//负责把content写入到filePath对应的文件中publicstaticvoidwriteFile(String filePath,String content){try(FileWriter fileWriter =newFileWriter(filePath)){
            fileWriter.write(content);}catch(IOException e){
            e.printStackTrace();}}}

有了以上模块我们就可以编写task代码了,就和在上面的task上提到一样。这就是类的方法。
分析一下步骤。先实例化一个Answer然后创建一个文件用来放入我们写入的代码,然后通过cmd进行编程,判断然后将不同情况返回到不同的文件中去。

publicAnswercompileAndRun(Question question){Answer answer=newAnswer();File workDir=newFile(WORK_DIR);//如果不存在就创建一个if(!workDir.exists()){
            workDir.mkdir();}//1.把question中的code写入到一个Solution.java文件中。FileUtil.writeFile(CODE,question.getCode());//2.创建子进程,调用javac编译。String compileCmd=String.format("javac -encoding uft8 %s -d %s",CODE,WORK_DIR);System.out.println(compileCmd);CommandUtil.run(compileCmd,null,COMPILE_ERROR);//3.创建子进程,调用java命令并执行String compileError=FileUtil.readFile(COMPILE_ERROR);if(!compileError.equals("")){
            answer.setError(1);
            answer.setReason(compileError);return  answer;}//4.父进程获取到刚才的编译执行结果,并打包成Answer对象returnnull;}publicstaticvoidmain(String[] args){Task task=newTask();Question question=newQuestion();
        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模块就做好了。这就是我们的后端不含数据库部分。

标签: 后端 数据库 前端

本文转载自: https://blog.csdn.net/m0_57315623/article/details/122993417
版权归原作者 爱编程的快乐人 所有, 如有侵权,请联系我们删除。

“快乐人的web项目(Servlet)----在线OJ(上)后端部分(不含数据库)”的评论:

还没有评论