项目2:在线OJ

目录

一、项目目标

二、实现思路和前期准备

2.1 实现思路

2.2 前期准备

问题1:此处为什么创建的是进程不是线程?

问题2:如何创建进程?

问题3:如何获取标准输出和标准错误?

三、实现CommandUtil类(利用cmd执行命令)

四、实现FileUtil类(封装读写文件的操作)

4.1 代码实现

4.2 注意事项

①对于文本文件来说,字节流和字符流都可以进行读写,为什么选择字符流?

②返回的类型是一个字节byte,为什么使用int来接收?

③文件读写完需要关闭,否则会导致文件资源泄露

④为什么不使用StringBuffer

五、创建Task类,实现编译运行的全过程

④检查用户提交代码安全性的补充(使用docker)

六、设计数据库,利用MyBatis实现对数据库的操作

6.1 设计数据库

6.2 引入MyBatis框架,配置MySql的服务器地址、用户名和密码、xml保存路径

6.2.1 引入框架

6.2.2 配置信息

6.3  写代码,执行业务处理

七、实现QuestionController(前后端的交互)

7.1 明确需要设计的网页

7.2 约定前后端交互的API

7.2.1 请求题目列表

7.2.2 请求指定的题目的详细信息(测试代码不需要展示!!!)

7.2.3 向服务器发送当前用户编写的代码,并且获取运行结果

7.2.4 请求删除题目

7.2.5 插入一个题目

7.3 具体实现

7.3.1 准备工作

7.3.2 向服务器发送当前用户编写的代码,并且获取运行结果

7.3.3 请求题目列表和请求指定的题目的详细信息

7.3.4 请求删除题目

7.3.5 插入一个题目

八、前端部分

8.1 题目列表页

8.1.1 页面核心代码

8.1.2 和后端交互的代码

8.2 问题详情页

8.2.1 页面核心代码

8.2.2 和后端交互的代码:

8.2.3 引入代码编辑器组件ace.js

8.3 新增问题页面

8.3.1 页面核心代码

8.3.2 和后端交互代码

九、项目总结


一、项目目标

牛客网、力扣等平台就是一个在线OJ平台,基于该平台,可以实现在线做题,提交后能够立刻看到运行结果是否通过。我们此次要实现的项目,就是一个功能简化版的在线OJ项目。该项目的核心功能为:

题目列表页:可以以表格的形式展示题目列表

题目详情页:可以查看某个题目的详细信息,并有代码编辑框。

提交并运行题目,展示运行结果:在代码编辑框提交代码后,点击题目详情页的提交按钮。网页就会将代码提交到服务器上,服务器执行代码后,返回用例是否通过的结果。

二、实现思路和前期准备

2.1 实现思路

有一个服务器的进程一直运行着,用户的代码从前端传来。服务器的进程先创建一个子进程对该代码进行编译。随后,再创建一个子进程对编译好的代码进行运行。随后,服务器返回运行结果。

2.2 前期准备

问题1:此处为什么创建的是进程不是线程?

因为每个用户提供的代码是一个独立的逻辑,同时,我们无法控制用户输入的代码,很可能代码运行会导致系统崩溃。考虑到进程的稳定性,所以采用多进程编程的思路。

问题2:如何创建进程?

首先,被创建的进程称为子进程,创建子进程的进程称为父进程。在该项目中,服务器进程相当于父进程,根据用户的代码创建的进程称为子进程。一个父进程可以有多个子进程。(谈到多进程,会经常涉及到父进程和子进程,但是线程没有父线程和子线程的说法)。

其次,关于多进程编程,JAVA提供了两个操作。第一个是进程创建,第二个是进程等待。

Process process = Runtime.getRuntime().exec(cmd);

①通过Runtime获取到Runtime对象,此对象是一个单例对象。然后exec得到Process对象。通过这个代码,可以创建出子进程。此时父子进程之间,是并发执行的关系。但是,在该项目中,当用户提交的代码编译运行完毕了后,需要将结果返回给用户。因此,我们需要让父进程知道子进程的执行状态。因此,需要加上进程等待的语句

int exitCode = process.waitFor();

②执行Runtime.getRuntime().exec(cmd)这行代码,就相当于在cmd输入一条命令。当在cmd输入一条命令。操作系统会去一些特定的目录中查找这个可执行文件,找到才能执行。怎么确保能够找到?将javac所在的目录加到PATH环境变量中。

③Javac是一个控制台程序,他的输出是存放在“标准输出”和“标准错误”这两个特殊的文件中。要想看到这个程序的运行效果,就需要获得标准输出和标准错误的内容。

问题3:如何获取标准输出和标准错误?

当一个进程在启动时,会自动打开三个文件。标准输入(对应到键盘)、标准输出(对应到显示器)、标准错误(对应到显示器)。虽然子进程启动后同样也打开这三个文件,但是由于子进程没有和IDEA的终端关联,因此在IDEA中看不到子进程的输出的。要想获得输出,就需要在代码中手动获取到。

获取标准输出:

InputStream stdoutFrom = process.getInputStream();

获取标准错误:

InputStream stderrFrom = process.getErrorStream();

获取到标准输出和标准错误后,我们需要将其写入到文件中。随后通过读文件,就可以看到程序的执行效果。

三、实现CommandUtil类(利用cmd执行命令)

根据传入的语句创建进程——》获取进程的标准输出,并将其写入到特定文件中——》获取进程的标准错误,并将其写入到特定文件中——》返回进程的状态码。

读写文件的操作可以参考这篇博客:Lesson3:文件操作和IO_刘减减的博客-CSDN博客

package com.example.demo.compile;

import org.springframework.stereotype.Component;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@Component
public class CommandUtil {
    public static int run(String cmd,String stdoutFilePath,String stderrFilePath){
        try {
            Process process = Runtime.getRuntime().exec(cmd);
            if(stdoutFilePath != null){
                // 获取运行的标准输出
                InputStream stdoutFrom = process.getInputStream();
                // 将标准输出写入到文件中
                OutputStream stdoutTo = new FileOutputStream(stdoutFilePath);
                while (true){
                    int ch = stdoutFrom.read();
                    if(ch == -1){
                        break;
                    }
                    stdoutTo.write(ch);
                }
                stdoutFrom.close();
                stdoutTo.close();
            }
            if(stderrFilePath != null){
                // 获取运行的标准错误
                InputStream stderrFrom = process.getErrorStream();
                // 将标准错误写入到文件中
                OutputStream stderrTo = new FileOutputStream(stderrFilePath);
                while (true){
                    int ch = stderrFrom.read();
                    if(ch == -1){
                        break;
                    }
                    stderrTo.write(ch);
                }
                stderrFrom.close();
                stderrTo.close();
            }
            // 等到子进程结束,拿到子进程的状态码,并返回
            int exitCode = process.exitValue();
            return exitCode;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return 1;
    }

    public static void main(String[] args) {
        run("javac","E://stdout.txt","E://stderr.txt");
    }
}

四、实现FileUtil类(封装读写文件的操作)

4.1 代码实现

在第三小结,将进程的标准输出和标准错误写到文件中,在QuestionController模块,我们需要将 编译子进程、运行子进程的标准输入和标准输入从文件中读取出来。虽然Java本身已经提供了很多关于文件读写操作的方法,但是使用稍微麻烦一点。为了便于使用,对这些操作进一步封装为一个类FileUtil,创建两个方法,第一个方法是将文件的内容读出来,第二个方法是将String写入文件中。

package com.example.demo.compile;

import org.springframework.stereotype.Component;

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

@Component
public class FileUtil {
    // 将文件的值读到一个String里面
    public static String readFile(String path){
        StringBuilder content = new StringBuilder();
        try(FileReader fileReader = new FileReader(path)){
            while (true){
                int ch = fileReader.read();
                if(ch == -1){
                    break;
                }
                content.append((char)ch);
            }

        }catch (IOException e){
            e.printStackTrace();
        }
        return content.toString();
    }
    // 将String写入文件中
    public static void writeFile(String content,String path){
        try(FileWriter fileWriter = new FileWriter(path)){
            fileWriter.write(content);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        String path = "E:\\hello.txt";
        String content = readFile(path);
        System.out.println(content);
        writeFile("mybatis","E:\\hello.txt");
    }
}

4.2 注意事项

①对于文本文件来说,字节流和字符流都可以进行读写,为什么选择字符流?

与字符流相比,字节流会更麻烦一点,体现为:当文件中包含中文的时候,需要手动的处理编码格式。

②返回的类型是一个字节byte,为什么使用int来接收?

Java中不存在无符号数,byte也是有符号的,范围是[-128,127]。byte占一个字节空间,最高位是符号位,剩余7位能表示0-127,加上符号位的正负,就是-127至+127,但负0没必要,为充分利用,就用负零表示-128(即原码1000,0000)。(计算机转补码后存储)

但是在实际中,因为读出来的只是表示一个单纯的字符,并不是要进行加减运算,因此期望读到的是一个无符号的数字,将范围改为(0,255)使用int表示,文件还没读完时,返回一个int。如果读到末尾返回EOF,end of file,用-1表示,证明文件已经读完。

③文件读写完需要关闭,否则会导致文件资源泄露

受限于操作系统内核里面的实现,一个进程能够同时打开的文件个数是存在上限的。对于Linux来说,进程PCB中的文件操作符表属性,大小是存在上限的。可以通过ulimit命令来查看和修改进程能够支持的最大文件个数。

④为什么不使用StringBuffer

因为String是不可变对象,为了方便对String的修改。可以使用StringBuilder(线程不安全)和StringBuffer(线程安全)。当多个线程同时修改同一个变量,会触发线程安全问题。刚刚创建的StringBuilder是函数里面的局部变量在栈上,由于每个线程都有自己的栈。所以不会引起线程安全问题。没必要使用StringBuffer。

五、创建Task类,实现编译运行的全过程

实现一个Task类。基于CommandUtil,Task类主要是实现一个完整的编译和运行这样的模块。该模块的输入是用户提交的代码,输出是代码的编译结果和运行结果。该模块的执行逻为:

前置条件:服务器进程一直在运行

第一步:服务器收到用户发送的代码,判断其安全性。主要检查代码中是否包含Runtime、exec(这两个防止提交的代码运行恶意程序)、java.io(禁止提交的代码读写文件)、java.net(禁止提交的代码访问网络)这三个关键字。

第二步:服务器进程创建子进程,执行javac命令进行编译。需要指定.class文件的位置,免得运行时找不到。

第三步:服务器进程读取编译生成的标准错误的内容,判断是否为空。不为空则证明编译不通过,返回编译出错结果。如果为空,证明编译成功,进入下一步运行。

第四步:服务器进程创建子进程,执行java命令进行运行。.class文件的位置与第二步保存的位置一致。

第五步:服务器进程读取运行的标准错误的内容,如果不为空证明运行错误,返回运行出错信息。

第六步:服务器进程读取运行的标准输出的内容,返回运行结果。

注意:

①javac进程和Java这两个进程需要通信。Linux系统提供管道、消息队列、信号量、信号、socket、文件等通信方式,在该项目中,使用文件来进行通信。因此,我们需要约定一个临时文件。

②因为在JAVA中,类名和文件名需要一致,我们约定好类名和文件名都是Solution

③约定

error = 0,编译运行都通过

error = 1,编译错误

error = 2,运行错误

reason:存放错误信息

stdout:存放标准输出

stderr:存放标准错误

④检查用户提交代码安全性的补充(使用docker)

目前使用的这个黑名单的方式,只能简单粗暴的处理掉一批明显的安全漏洞,但是还是存在安全隐患。有一种更彻底的解决这个问题的方式:

docker相当于一个轻量级虚拟机,每次用户提交的代码,都会给这个代码分配一个docker容器,让用户提交的代码在容器中执行,如果这个容器中有恶意代码,最多也就是将docker容器搞坏了,对物理机没有任何影响。同时docker的设计天然就是很轻量的,一个容器可以随时创建随时删除。

该容器和tomcat、servlet容器没有关系,和Spring中的bean容器也没有关系,也和c++STL里的容器没有关系。

package com.example.demo.compile;

import com.example.demo.model.Answer;
import com.example.demo.model.UserCode;
import org.springframework.stereotype.Component;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Component
public class Task {
    // 用一组常量来约定临时变量的名字
    private String WORK_DIR = null;  // 目录
    private String CLASSNAME = null; // 类名
    private String CODE = null; //代码的文件名
    private String COMPILE_ERROR = null; // 编译错误
    private String STDOUT = null; //  标准输出
    private String STDERR = null;  // 标准输入

    public Task(){
        WORK_DIR = "E://OJ//"+ UUID.randomUUID()+"//";
        CLASSNAME = "Solution";
        CODE = WORK_DIR+"Solution.java";
        COMPILE_ERROR = WORK_DIR + "compileError.txt";
        STDOUT = WORK_DIR + "stdout.txt";
        STDERR = WORK_DIR + "stderr.txt";
    }

    public Answer compileAndRun(UserCode userCode){
        Answer answer = new Answer();
        // 准备用来存放临时文件的目录
        File workFile = new File(WORK_DIR);
        if(!workFile.exists()){
            workFile.mkdirs();
        }

        // 对代码的安全性进行判定
        if(!checkCodeSafe(userCode.getUserCode())){
            System.out.println("用户提交了不安全的代码");
            answer.setError(3);
            answer.setReason("\"您提交的代码可能会危害到服务器, 禁止运行!\"");
            return answer;
        }
        // 将question中的code写入到CODE中
        FileUtil.writeFile(userCode.getUserCode(),CODE);

        // 进行编译,javac -d 用来指定生成的 .class 文件的位置
        String compileCmd = String.format("javac -encoding utf8 %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("")){
            System.out.println("编译出错");
            answer.setError(1);
            answer.setReason(compileError);
            return answer;
        }
        //进行运行
        String runCmd = String.format("java -classpath %s %s",WORK_DIR,CLASSNAME);
        System.out.println("编译命令"+runCmd);
        CommandUtil.run(runCmd,STDOUT,STDERR);
        String stdErr = FileUtil.readFile(STDERR);
        if(!stdErr.equals("")){
            System.out.println("运行出错");
            answer.setError(2);
            answer.setReason(stdErr);
            return answer;
        }
        answer.setError(0);
        answer.setStdout(FileUtil.readFile(STDOUT));
        return answer;
    }

    private boolean checkCodeSafe(String code) {
        List<String> blackList = new ArrayList<>();
        // 防止提交的代码运行恶意程序
        blackList.add("Runtime");
        blackList.add("exec");
        // 禁止提交的代码读写文件
        blackList.add("java.io");
        // 禁止提交的代码访问网络
        blackList.add("java.net");
        for(String str:blackList){
            int pos = code.indexOf(str);
            if(pos >= 0){
                return false; // 找到任意的恶意代码特征, 返回 false 表示不安全
            }
        }
        return true;

    }

    public static void main(String[] args) {
        Task task = new Task();
        UserCode userCode = new UserCode();
        userCode.setUserCode("public class Solution {\n" +
                "    public static void main(String[] args) {\n" +
                "        System.out.println(\"hello\");\n" +
                "    }\n" +
                "}");
        Answer answer = task.compileAndRun(userCode);
        System.out.println(answer.toString());
    }
}

六、设计数据库,利用MyBatis实现对数据库的操作

6.1 设计数据库

id:题目id

title:题目标题

level:难度等级

description:题目详细介绍

templateCode:模板代码

testCode:测试代码

6.2 引入MyBatis框架,配置MySql的服务器地址、用户名和密码、xml保存路径

6.2.1 引入框架

主要引入MyBatis Framework和MySQL Driver这两个框架。具体操作步骤见Lesson 8: MyBatis_刘减减的博客-CSDN博客的2.1小结。

6.2.2 配置信息

创建application.yml文件。代码具体含义见Lesson 8: MyBatis_刘减减的博客-CSDN博客2.2小结。

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/oj_database?characterEncoding=utf8&useSSL=false
    username: root
    password: 9898
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
  mapper-locations: classpath:mapper/**Mapper.xml

6.3  写代码,执行业务处理

①创建一个model包,创建QuestionInfo类。类中的属性和数据库中user表的信息一致。

package com.example.demo.model;

import lombok.Data;
import org.springframework.stereotype.Component;

@Component //注入到Spring中
@Data
public class QuestionInfo {
    private int id;
    private String title;
    private String description;
    private String level;
    private String testCode;
    private String templateCode;
}

创建一个包mapper,创建一个接口QuestionMapper,在接口中定义往数据库中插入数据、根据id删除问题、根据id查看问题和查看所有问题四个方法。这里只是定义,具体的实现在xml中。

package com.example.demo.mapper;

import com.example.demo.model.QuestionInfo;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface QuestionMapper {
    public void insert(QuestionInfo questionInfo);
    public void deleteById(int id);
    public QuestionInfo selectQuestionById(int id);
    public List<QuestionInfo> selectAllQuestion();
}

在resources下建立一个文件夹mapper,里面创建一个QuestionMapper的xml文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace=你要实现的接口的完整包名+接口名 -->
<mapper namespace="com.example.demo.mapper.QuestionMapper">
    <!--    id = 实现接口中的方法名    resultType = 返回的类型-->
    <insert id="insert">
        insert into question(title,description,level,testCode,templateCode) values(#{title},#{description},#{level},#{testCode},#{templateCode})
    </insert>
    <delete id="deleteById">
        delete from question where id =#{id}
    </delete>
    <select id="selectQuestionById" resultType="com.example.demo.model.QuestionInfo">
        select * from question where id = #{id}
    </select>
    <select id="selectAllQuestion" resultType="com.example.demo.model.QuestionInfo">
        select * from question
    </select>
</mapper>

④单元测试

step1:添加单元测试类。在QuestionMapper页面的空白处,右击Generate—Test—勾选需要进行测试的方法。

step2:在单元测试类中:

        加注解@SpringBootTest,表明当前单元测试的框架是SpringBoot,不是其他

        将需要测试的类引入。

        进行测试。

package com.example.demo.mapper;

import com.example.demo.model.QuestionInfo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

import java.util.List;
// 表示当前单元测试的框架是SpringBoot,不是其他
@SpringBootTest
class ProblemInfoMapperTest {
    // 将需要测试的类注入
    @Resource
    private QuestionMapper questionMapper;
    //这个就相当于一个main方法,点击这个就可以进行测试了。由于测试的是QuestionMapper的方法,因此,需要先将该类引入
    @Test
    void insert() {
        QuestionInfo questionInfo = new QuestionInfo();
        questionInfo.setTemplateCode("tem");
        questionInfo.setTitle("title");
        questionInfo.setTestCode("testcode");
        questionInfo.setLevel("level");
        questionInfo.setDescription("description");
        questionMapper.insert(questionInfo);
    }

    @Test
    void deleteById() {
        questionMapper.deleteById(5);
    }

    @Test
    void selectProblemById() {
        QuestionInfo questionInfo = questionMapper.selectQuestionById(6);
        System.out.println(questionInfo);
    }

    @Test
    void selectAllProblem() {
        List<QuestionInfo> questionInfos = questionMapper.selectAllQuestion();
        for(QuestionInfo questionInfo : questionInfos){
            System.out.println(questionInfo);
        }
    }
}

详细操作见Lesson 8: MyBatis_刘减减的博客-CSDN博客的2.3.1和2.3.2小结。

七、实现QuestionController(前后端的交互)

7.1 明确需要设计的网页

题目列表页:向服务器发送请求,获取题目列表

题目详情页:

        展示题目信息:向服务器发送请求,获取指定题目的完整信息。将templateCode渲染到代码编辑框中。

        有一个代码编辑框,让用户来编写代码。这个不需要和服务器进行交互。

        有一个提交按钮,点击提价,将用户输入的代码发送到服务器,服务器编译和运行后,返回结果。

7.2 约定前后端交互的API

前后端交互的方式使用Json格式来完成。

7.2.1 请求题目列表

请求:GET/question
响应:
[
    {
        "id": 1,
        "title": "两数之和",
        "description": "a+b",
        "level": "简单"
    }
    {
        "id": 2,
        "title": "两数相减",
        "description": "a-b",
        "level": "简单"
    }
]

7.2.2 请求指定的题目的详细信息(测试代码不需要展示!!!)

请求:GET/question?questionID=1
响应:
{
    "id": 1,
    "title": "两数之和",
    "description": "a+b",
    "level": "简单",
    "templateCode": "templateCodslsjjs"
}

7.2.3 向服务器发送当前用户编写的代码,并且获取运行结果

请求:POST/compile
{
    "questionId": 1,  (id不能少,服务器需要根据id从数据库中拿到测试代码,和用户的代码组成一个完成代码,再去进行编译运行)
    "code": "用户提交的代码",
}
响应:
{
    "error": 1, (0表示编译运行都ok,1表示编译出错,2表示运行出错)
    "reason": "出错的详细信息",
    "stdout": "测试用例的输出情况,包含了通过几个用例的信息",
}

7.2.4 请求删除题目

请求:GET/delete?questionID=1
响应:重定向到题目列表页

7.2.5 插入一个题目

请求:POST/insert
{
    "title": "用户输入的title",
    "description": "用户输入的详情",
    "level": "用户输入的等级",
    "templateCode": "用户输入的模板代码",
    "testCode": "用户输入的测试代码",    
}
响应:重定向到题目列表页

7.3 具体实现

7.3.1 准备工作

针对编译运行请求来说,请求和响应都是JSON格式的数据。为了方便解析和构造,创建CompileResponse和RequestCode这两个类,来对应这两个JSON结构。

由于涉及到对数据库的一些操作,需要将QuestionMapper注入进来。

    @Resource
    @Component
    static class CompileResponse{
        public int error;
        public String reason;
        public String stdout;
    }
    @Resource
    @Component
    static class RequestCode{
        public int questionId;
        public String code;
    }
    @Resource
    private QuestionMapper questionMapper;
    @Resource
    private CompileResponse compileResponse = new CompileResponse();

7.3.2 向服务器发送当前用户编写的代码,并且获取运行结果

思路为:从请求中获取题目id—》根据id去数据库查题目——》获取该题目的测试代码——》从请求中获取用户提交的代码——》将两个代码组合,并判断是否为空——》实例化一个Task,进行编译运行该代码——》构造响应,并返回。

注意:@RequestBody注解指明返回的数据类型是json。

    @RequestMapping("/compile")
    public CompileResponse solve(@RequestBody RequestCode requestCode){
        try{
            // 获取题目id
            int questionId = requestCode.questionId;
            // 去数据库查询题目
            QuestionInfo questionInfo = questionMapper.selectQuestionById(questionId);
            // 没找到,报异常
            if(questionInfo == null){
                throw new QuestionNotFountException();
            }
            // 获取用户提交的代码
            String userCode = requestCode.code;
//            String userCode = "public class Solution {\n" +
//                    "    public static int sum(int a,int b){\n" +
//                    "        return a+b;\n" +
//                    "    }\n" +
//                    "}";
            // 获取题目的测试代码
            String testCode = questionInfo.getTestCode();
//            String testCode = "    public static void main(String[] args) {\n" +
//                    "        int a = 1;\n" +
//                    "        int b = 2;\n" +
//                    "        int c = sum(a,b);\n" +
//                    "        System.out.println(c);\n" +
//                    "    }";
            // 将两个代码组合
            String finalCode = merge(userCode,testCode);
            System.out.println(finalCode);
            // 判断代码是否合法
            if(finalCode == null){
                throw new CodeInvalidException();
            }
            // 实例化一个Task,编译运行代码
            Task task = new Task();
            UserCode userCode1 = new UserCode();
            userCode1.setUserCode(finalCode);
            Answer answer = task.compileAndRun(userCode1);
            System.out.println(answer);

            // 构造响应
            compileResponse.error = answer.getError();
            compileResponse.reason = answer.getReason();
            compileResponse.stdout = answer.getStdout();

        }catch (QuestionNotFountException e){
            compileResponse.error = 3;
            compileResponse.reason = "没有找到指定的题目! id="+requestCode.questionId;
            e.printStackTrace();
        }catch (CodeInvalidException e){
            compileResponse.error = 3;
            compileResponse.reason = "提交的代码不符合要求!"+requestCode.questionId;
            e.printStackTrace();
        }finally {
            return compileResponse;
        }
    }

7.3.3 请求题目列表和请求指定的题目的详细信息

思路为:从请求中读取questionID,如果读取到就返回特定题目信息。如果没有读取到,就返回所有题目。

    @RequestMapping("/question")
    public List<QuestionInfo> showQuestionInfo(String questionID){
        List<QuestionInfo> questionInfos = new ArrayList<>();
        if(questionID == null || "".equals(questionID)){
            questionInfos= questionMapper.selectAllQuestion();
            return questionInfos;
        }else{
            QuestionInfo questionInfo = questionMapper.selectQuestionById(Integer.parseInt(questionID));
            questionInfos.add(questionInfo);
        }
        System.out.println(questionInfos);
        return questionInfos;
    }

7.3.4 请求删除题目

    @RequestMapping("/delete")
    public void deleteQuestion(String questionID){

        questionMapper.deleteById(Integer.parseInt(questionID));
    }

7.3.5 插入一个题目

    @RequestMapping("/insert")
    public void insert(@RequestBody QuestionInfo questionInfo){
//        System.out.println(questionInfo);
        questionMapper.insert(questionInfo);
    }

八、前端部分

8.1 题目列表页

8.1.1 页面核心代码

                            <h3>题目列表</h3>
                            <table class="table table-striped">
                                <thead>
                                    <tr>
                                        <th>编号</th>
                                        <th>标题</th>
                                        <th>难度</th>
                                    </tr>
                                </thead>
                                <tbody id="questionTable">
<!--                                    <tr>-->
<!--                                        <td>1</td>-->
<!--                                        <td>-->
<!--                                            <a href="#">两数之和</a>-->
<!--                                        </td>-->
<!--                                        <td>简单</td>-->
<!--                                    </tr>-->
                                </tbody>
                            </table>
                    

8.1.2 和后端交互的代码

        <script>
            // 在页面加载的时候, 尝试从服务器获取题目列表. 通过 ajax 的方式来进行获取
            function getQuestions(){
                $.ajax({
                    url:"question",
                    type:"get",
                    success:function(data,status){
                        createTable(data);
                    }
                })
            }

            // 通过这个函数来把数据转换成 HTML 页面片段
            function createTable(data){
                let questionTable = document.querySelector("#questionTable");
                for(let questionInfo of data){
                    let tr = document.createElement("tr");
                    // 添加id
                    let tdId = document.createElement("td");
                    tdId.innerHTML = questionInfo.id;
                    tr.appendChild(tdId);
                    // 添加题目标题
                    let tdTitle = document.createElement("td");
                    let a = document.createElement("a");
                    a.innerHTML = questionInfo.title;
                    a.href = 'questionDetail.html?questionID=' + questionInfo.id;
                    a.target = '_blank';
                    tdTitle.appendChild(a);
                    tr.appendChild(tdTitle);
                    // 添加等级
                    
                    let tdLevel = document.createElement("td");
                    tdLevel.innerHTML = questionInfo.level;
                    tr.appendChild(tdLevel);
                    questionTable.appendChild(tr);
                }
            }
            getQuestions();
        </script>

8.2 问题详情页

8.2.1 页面核心代码

<div class="row mt-4">
            <div class="col-sm-12 pb-4" style="margin-bottom: 0;padding-bottom: 0;">
                <label for="codeEditor" style="margin-bottom: 20px;">问题描述</label>
                <div class="jumbotron jumbotron-fluid" style="margin-bottom: 20px; " >
                    <div class="container" id="questionDesc" >
                    </div>
                </div>
                
            </div>
        </div>

        <div class="row mt-4" style="margin-bottom: 0;padding-bottom: 0;">
            <div class="col-sm-12 pb-4">
                <div class="form-group" >
                    <label for="codeEditor" style="margin-bottom: 20px;">代码编辑框</label>
                    <div id="editor" style="min-height:400px">
                        <textarea class="form-control" id="codeEditor" style="width: 100%; height: 400px;"></textarea>
                    </div>
                </div>
            </div>
        </div>
        
        
        <button type="button" class="btn btn-primary" id="submitButton" style="margin-bottom: 20px;">提交</button>
        <br>
        <label for="codeEditor" style="margin-bottom: 20px;">运行结果</label>
        <div class="row mt-4">
            <div class="col-sm-12 pb-4">
                <div class="jumbotron jumbotron-fluid">
                    <div class="container">
                            <pre id="questionResult">

                            </pre>
                    </div>
                </div>
            </div>
        </div>

8.2.2 和后端交互的代码:

script>
            console.log(location.search);

            function initAce() {
                // 参数 editor 就对应到刚才在 html 里加的那个 div 的 id
                let editor = ace.edit("editor");
                editor.setOptions({
                    enableBasicAutocompletion: true,
                    enableSnippets: true,
                    enableLiveAutocompletion: true
                });
                editor.setTheme("ace/theme/twilight");
                editor.session.setMode("ace/mode/java");
                editor.resize();
                document.getElementById('editor').style.fontSize = '20px';

                return editor;
            }

            let editor = initAce();

            // 通过 ajax 从服务器获取到题目的详情
            function getQuestion() {
            // 1. 通过 ajax 给服务器发送一个请求
                $.ajax({
                    url: "question" + location.search,
                    type: "GET",
                    success: function(data, status) {
                        for(let question of data){
                            makeProblemDetail(question);
                            console.log(data);
                        }
                    }
                });
            }

            function makeProblemDetail(question){
                // 1. 获取到 problemDesc, 把题目详情填写进去
                let questionDesc = document.querySelector("#questionDesc");
                let h3 = document.createElement("h3");
                h3.innerHTML = question.id + "." + question.title + "_" + question.level;
                console.log(h3)
                questionDesc.appendChild(h3);

                let pre = document.createElement("pre");
                let p = document.createElement("p");
                p.innerHTML = question.description;
                pre.appendChild(p);
                questionDesc.appendChild(pre);

                // 2. 把代码的模板填写到编辑框中.
                editor.setValue(question.templateCode);
                // 3. 给提交按钮注册一个点击事件
                let submitButton = document.querySelector("#submitButton");
                submitButton.onclick = function () {
                    // 点击这个按钮, 就要进行提交. (把编辑框的内容给提交到服务器上)
                    let body = {
                        questionId: question.id,
                        // code: codeEditor.value,
                        code: editor.getValue(),
                    };
                    $.ajax({
                        type: "POST",
                        url: "compile",
                        data: JSON.stringify(body),
                        contentType: "application/json",
                        success: function (data, status) {
                            let problemResult = document.querySelector("#questionResult");
                            if (data.error == 0) {
                                // 编译运行没有问题, 把 stdout 显示到页面中
                                problemResult.innerHTML = data.stdout;
                            } else {
                                // 编译运行没有问题, 把 reason 显示到页面中
                                problemResult.innerHTML = data.reason;
                            }
                        }
                    });
                }
            }
            getQuestion();
        </script>

8.2.3 引入代码编辑器组件ace.js

// 引入ace.js
<script src="https://cdn.bootcss.com/ace/1.2.9/ace.js"></script>
<script src="https://cdn.bootcss.com/ace/1.2.9/ext-language_tools.js"></script>

// 将页面编辑框外面套一层 div, id 设为 editor, 并且一定要设置 min-height 属性.
<div id="editor" style="min-height:400px">
    <textarea style="width: 100%; height: 200px"></textarea>
</div>

// 对编译器进行初始化
function initAce() {
    // 参数 editor 就对应到刚才在 html 里加的那个 div 的 id
    let editor = ace.edit("editor");
    editor.setOptions({
        enableBasicAutocompletion: true,
        enableSnippets: true,
        enableLiveAutocompletion: true
    });
    editor.setTheme("ace/theme/twilight");
    editor.session.setMode("ace/mode/java");
    editor.resize();
    document.getElementById('editor').style.fontSize = '20px';

    return editor;
}

//获取刚刚初始化好的编译器
let editor = initAce();

// 由于ace.js会重新绘制页面,导致之前弄得textarea没了。因此需要换种方式
// 将代码设置到编译器中
editor.setValue(question.templateCode);

// 获取编译器中得代码
editor.getValue()

8.3 新增问题页面

8.3.1 页面核心代码

<label for="codeEditor" style="margin: 10px;">问题题目</label>
<div class="inputblock">
    <input type="text" id="title" style="height: 64px; width:930px;display: block;background-color:#465966;">
</div>
<label for="codeEditor" style="margin: 10px;">问题描述</label>
<div class="inputblock">
    <input type="text" id="desc" style="height: 128px; width:930px;display: block;background-color:#465966;">
</div>
<label for="codeEditor" style="margin: 10px;">问题等级</label>
<div class="inputblock">
    <select id="level" style="height: 40px; width:930px;display: block;background-color:#465966;color:white ;">
        <option selected = "selected" style="color: white;">---请选择难度等级---</option>
        <option style="color: white;">简单</option>
        <option style="color: white;">一般</option>
        <option style="color: white;">困难</option>
    </select>
</div>

<div class="form-group">
    <label for="codeEditor" >模板代码输入框</label>
    <div id="editor1" style="min-height:400px">
        <textarea class="form-control" id="codeEditor1" style="width: 100%; height: 400px;"></textarea>
    </div>
</div>

<div class="form-group">
    <label for="codeEditor">测试代码输入框</label>
    <div id="editor2" style="min-height:400px">
        <textarea class="form-control" id="codeEditor" style="width: 100%; height: 400px;"></textarea>
    </div>
</div>

<button type="button" class="btn btn-primary" id="submitButton">提交</button>


8.3.2 和后端交互代码

    <script>
        function initAce(id) {
            // 参数 editor1 就对应到刚才在 html 里加的那个 div 的 id
            let editor = ace.edit(id);
            editor.setOptions({
                enableBasicAutocompletion: true,
                enableSnippets: true,
                enableLiveAutocompletion: true
            });
            editor.setTheme("ace/theme/twilight");
            editor.session.setMode("ace/mode/java");
            editor.resize();
            document.getElementById(id).style.fontSize = '20px';
            return editor;
        }

        let editor1 = initAce("editor1");
        let editor2 = initAce("editor2");

        // 3. 给提交按钮注册一个点击事件
        let submitButton = document.querySelector("#submitButton");
            submitButton.onclick = function () {
                let titleInput = document.getElementById("title");
                let title = titleInput.value;

                let descInput = document.getElementById("desc");
                let description = descInput.value;

                let levelSelect = $("#level option:selected"); // 选取选中项
                let level = levelSelect.text(); //拿到选中项的文本

                let templateCode = editor1.getValue();
                
                let testCode = editor2.getValue();

                let body = {
                    title:title,
                    description:description,
                    level:level,
                    templateCode:templateCode,
                    testCode:testCode
                };
                $.ajax({
                    type: "POST",
                    url: "insert",
                    data: JSON.stringify(body),
                    contentType: "application/json",
                });
            }
    </script>

九、项目总结

本项目基于SpringMVC和MyBatis框架,实现了一个在线OJ项目。在该项目中,主要包含四个模块。

编译运行模块:创建了CommandUtil类、FileUtil类和Task类。在CommandUtil类中实现了创建进程并将进程的标准输出和标准错误写入到文件中的方法。在FileUtil类中封装了读写文件的操作。在Task类,基于多进程编程,实现编译运行的全过程。

题目管理模块:基于MyBatis,创建了QuestionMapper接口,在该接口中实现了增删查操作。

API模块:首先设计了前后端交互的接口,创建QuestionController类,实现了获取题目列表、获取题目详情、编译运行、增加题目信息、删除题目这5个api接口。

前端模块:创建了题目列表页、题目详情页、题目信息在线录入页面。引入代码编辑器组件ace.js使得编辑框变得更加好用。通过js代码,实现了调用后端HTTP API的过程。

  • 3
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘减减

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值