项目---在线OJ

1. 项目介绍

1.1项目背景

  • 基于经常在牛客网或leetcode 刷题,比较好奇这样的网站是如何形成呢?然后做一个;类似于leetcode这样的项目,那么这个项目我主要实现了哪些功能呢?

1.2项目实现的功能

  1. 能够保存题目;
  2. 展示题目列表;(id,标题,难度)
  3. 展示题目的详细信息(标题,难度,题目的描述,题目的代码模板)
  4. 能够在线编辑代码并且能够提交运行

2.项目具体实现

2.1 创建进程实现javac,java等命令

  • 创建该类的目的是执行javac,java等命令,首先,先创建一个新进程(创建好进程之后,新的进程和当前的进程是一个并列的关系,新的进程跑起来之后,就需要获取新进程的的输出结果,需要使用process.getInputStream();然后读取新进程的内容依次写入到stdout中,标准错误也是类似。之后使用waifFor()获得子进程的退出码,一直阻塞等待到子进程结束,父进程才能够结束。当前CommandUtil这个类,就能够创建子进程实现某个命令。
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) throws IOException, InterruptedException {
    	Process process = Runtime.getRuntime().exec(cmd);
    	if (stdoutFile != null) {
            //得到的是标准输出
            InputStream stdoutFrom = process.getInputStream();
            //通过这个对象就可以获取到当前新进程标准输出的内容
            FileOutputStream stdoutTo = new FileOutputStream(stdoutFile);
            //接下来就从新进程这边一次读取每个字节,写到stdoutTo这个文件中
            //把新进程本来要显示到显示器的数据,给写到一个指定的操作,称为重定向
            while (true){
                int ch = stdoutFrom.read();
                if(ch == -1){
                    break;
                }
                stdoutTo.write(ch);
            }
            //循环结束,文件读写完毕,关闭文件
            stdoutFrom.close();
            stdoutTo.close();
        }
        if(stderrFile != null){
            //getErrorStream 得到的是标准错误
            InputStream stderrFrom = process.getErrorStream();
            FileOutputStream stderrTo = new FileOutputStream(stderrFile);
            while(true){
                int ch = stderrFrom.read();
                if(ch == -1){
                    break;
                }
                stderrTo.write(ch);
            }
            //循环结束,关闭文件
            stderrFrom.close();
            stderrTo.close();
        }
        //注意这里的waitFor方法,父进程得等待子进程执行完毕之后父进程才能执行。
        int exitCode = process.waitFor();
        return exitCode;
    }
 }

2.2 创建类Question,Answer

  • 实现了CommandUtil这个类,这个类的作用是创建一个子进程,使子进程能够执行某个命令,比如说是javac ,或者java这样的命令。之后就需要创建两个类,Question和Answer,Question表示去编译运行的代码,Answer表示运行后的结果。设计Question类的时候,需要一个要编译运行的源代码,可以用code表示。设计Answer类的时候,Answer类的属性:errno0表示编译运行都ok,1表示编译出错,2表示运行抛异常,reason,errno1,reason包含了编译错误的信息,errno2,reason包含了异常的调用栈信息,程序的标准输出 stdout.程序的标准错误,stderr.

2.3 Task类

  • 创建好Answer,Question之后,创建Task类,主要实现的功能是实现完整的编译或者运行的功能。这个类主要实现的方法是compileAndRun,主要有四个部分:
    1. 先准备好需要用到的临时文件(在这之前需要给这些文件准备好一个目录)(使用UUID来辅助生成目录)
    2. 构造编译指令(javac),并进行执行,预期得到的结果
    3. 构造运行指令(java),并进行执行,预期得到的结果
    4. 把最终的结果构造成Answer对象。
import util.FileUtil;

import java.io.File;
import java.io.IOException;
import java.util.UUID;

//用这个类表示一个完整的编译运行的过程
public class Task {
    private  String WORK_DIR ;
    private  String CLASS = "Solution";
    private  String CODE ;
    private  String STDOUT ;
    private  String STDERR ;
    private  String COMPILE_ERROR ;

    public Task(){
        WORK_DIR = "./tmp/"+UUID.randomUUID().toString()+"/";
        CODE = WORK_DIR + CLASS +".java";
        STDOUT = WORK_DIR + "stdout.txt";
        STDERR = WORK_DIR + "stderr.txt";
        COMPILE_ERROR = WORK_DIR + "compile_error.txt";
    }
    public Answer compileAndRun(Question question) throws IOException, InterruptedException {
        Answer answer = new Answer();
        File file = new File(WORK_DIR);
        if(!file.exists()){
            //创建对应的目录
            file.mkdirs();
        }
        //1.先要准备好需要用到的临时文件
        //要编译的源代码的文件(首先搞这个)
        FileUtil.writeFile(CODE,question.getCode());
        String compileCmd = String.format("javac -encoding utf-8 %s -d %s",CODE,WORK_DIR);
        System.out.println("编译命令:"+compileCmd);
        CommandUtil.run(compileCmd,null,COMPILE_ERROR);
        //此处判断一下编译是否出错,看一下COMPILE_ERROR这个文件是不是空着就知道了
        String compileError = FileUtil.readFile(COMPILE_ERROR);
        if(!compileError.equals("")){
            //编译文件不为空,说明编译出错了
            answer.setErrno(1);
            answer.setReason(compileError);
            return answer;
        }
        String runCmd = String.format("java -classpath %s %s",WORK_DIR,CLASS);
        System.out.println("runCmd"+runCmd);
        CommandUtil.run(runCmd,STDOUT,STDERR);
        String runError = FileUtil.readFile(STDERR);
        if(!runError.equals("")){
            //运行出错
            answer.setErrno(2);
            answer.setReason(runError);
            return answer;
        }
        //4.把最终的结果构造程Answer对象,并且返回
        answer.setErrno(0);
        String runStdout = FileUtil.readFile(STDOUT);
        answer.setStdout(runStdout);
        return answer;
    }

2.4创建 FileUtil

  • 创建一个类FileUtil,使这个类封装一下Java的文件操作,在后面的代码中更方便使用。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

//使用这个类封装一下Java的文件操作
//让后面的代码能够更方便的读写整个文件
public class FileUtil {
    //从指定的文件中一次把所有的内容都读出来
  public static String readFile(String filePath){
      StringBuilder stringBuilder = new StringBuilder();
      FileInputStream fileInputStream = null;
      try {
           fileInputStream = new FileInputStream(filePath);
          while(true){
              int ch = fileInputStream.read();
              if(ch == -1){
                 break;
              }
              //每个read方法只是读到一个字节
              //read设计程返回int的原因只是为了能多表示一下-1这个情况
              //实际把读取的结果往stringBuilder里插入的时候,还得在转换成char字符
              //预期是一次往stringBuilder写一个字符,而不是一次写四个字节。
              stringBuilder.append((char)ch);
          }
      } catch (IOException e) {
          e.printStackTrace();
      }finally {
          try {
              if (fileInputStream != null) {
                  fileInputStream.close();
              }
          } catch (IOException e) {
              e.printStackTrace();
          }
      }
      return stringBuilder.toString();
  }

  //把content中的内容一次写入到filePath对应的文件中
  public static void writeFile(String filePath,String content){
    try(FileOutputStream fileOutputStream = new FileOutputStream(filePath)){
        //进行写文件操作
        fileOutputStream.write(content.getBytes());
    }catch (IOException e){
        e.printStackTrace();
    }
  }
}

2.5 题目存储

  • 题目存储这个模块我通过用数据库来进行存储。我实现的OJ项目有题目的列表和题目的详情页,因此题目的列表包括id ,标题,还有难度,题目的详情页包括题目表述和模板代码,还有测试代码。因此我在设计库中的表的时候就必须要包含着几个字段。
create database if not exists oj;

use oj;
drop table if exists  oj_table;
create table oj_table(
    id int primary key auto_increment,
    title varchar(50),
    level varchar(50),
    description varchar(4096),
    templateCode varchar(4096),
    testCode varchar(4096)
);
  • 建立完数据库以及表之后,就需要创建一个实体类problem,这个类的每一个实体对应数据库中的一条记录。problem类中包含的属性和数据库表中的子段是相匹配的。
  • 设计完数据库之后,需要通过JDBC来完成针对数据库的操作。
  • 首先和数据库建立连接,使用双重校验锁可以保证线程的安全。
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

//方便其他的代码随时能和数据库建立连接
public class DBUtil {
    //DateSource这个东西一般在一个程序里只有一个实例就够了
    //单例模式
    private static final String URL = "jdbc:mysql://127.0.0.1:3306/oj?Character=UTF8&&useSSL=false";
    private static final String USERNAME = "root";
    private static final String PASSWORD ="12345678";

    private static  volatile DataSource dataSource = null;

    //目的是只创建出一个DataSource实例
    public static DataSource getDataSource(){
        if(dataSource == null){
            //没有被实例化过,就创建一个实例
            synchronized (DBUtil.class){
                dataSource = new MysqlDataSource();
                ((MysqlDataSource)dataSource).setURL(URL);
                ((MysqlDataSource)dataSource).setUser(USERNAME);
                ((MysqlDataSource)dataSource).setPassword(PASSWORD);
            }  
        }
        //如果已经被实例化过了,就直接返回现有的实例
        return dataSource;
    }

    public static Connection getConnection(){
        try {
            return getDataSource().getConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    //断开连接的逻辑(税收资源的逻辑)
    public static void close(Connection connection, PreparedStatement  preparedStatement, ResultSet resultSet){
        if(resultSet != null){
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if(preparedStatement != null){
            try {
                preparedStatement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if(connection != null){
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
     }
}
  • 和数据库建立连接之后,写一个类,problemDAO主要包含一些增删改查的方法,主要用来操作数据库。
public class problemDAO{
	public List<Problem> selectAll(){
        List<Problem> problems = new ArrayList<>();
        //1.建立连接
        Connection connection = DBUtil.getConnection();
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        //2.拼装SQL
        String sql = "select id,title,level from oj_table";
        try {
            statement = connection.prepareStatement(sql);
            //3.执行SQL
             resultSet = statement.executeQuery();
            //4.遍历结果集
            while(resultSet.next()){
               //每次从数据库中读取出一条记录就对应出一个Problem的对象
                Problem problem = new Problem();
                problem.setId(resultSet.getInt("id"));
                problem.setTitle(resultSet.getString("title"));
                problem.setLevel(resultSet.getString("level"));
//                problem.setDescription(resultSet.getString("description"));
//                problem.setTemplateCode(resultSet.getString("templateCode"));
//                problem.setTestCode(resultSet.getString("testCode"));
                problems.add(problem);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally{
            DBUtil.close(connection,statement,resultSet);
        }
        return problems;
    }

    //查找指定题目(用来实现题目的详情页)
    //需要把problem中的每个字段都查询出来
    public static Problem selectOne(int problemId){
        //1.建立连接
        Connection connection = DBUtil.getConnection();
        //2.拼装SQL
        String sql = "select * from oj_table where id=?";
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            statement = connection.prepareStatement(sql);
            statement.setInt(1,problemId);
            //3.执行SQL
            resultSet = statement.executeQuery();
            //4.遍历结果集,由于查询结果要么是0个,要么是1个的查询结果
            //直接使用if判断即可,不用使用while
            if(resultSet.next()){
                Problem problem = new Problem();
                problem.setId(resultSet.getInt("id"));
                problem.setTitle(resultSet.getString("title"));
                problem.setLevel(resultSet.getString("level"));
                problem.setDescription(resultSet.getString("description"));
                problem.setTemplateCode(resultSet.getString("templateCode"));
                problem.setTestCode(resultSet.getString("testCode"));
                return problem;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            DBUtil.close(connection,statement,resultSet);
        }
        return null;
    }
}

2.6 实现前后的交互

  • 实现一下服务器的逻辑,让服务器能够跑起来,能够接受请求,再给服务器的逻辑中调用上面的编译模块和数据库操作模块。要想实现前后端的交互操作,就需要约定前后端接口。
  • 实现前后端的交互接下来就要约定好,服务器都能够接受怎么样的HTTP请求以及每一个请求都返回啥样的结果数据。
  • 前后端交互的API的设计风格我用的是Restful风格:
    1. 使用不同的HTTP方法表示不同的操作类型(GET表示获取数据,POST表示新增数据,PUT表示修改数据,DELETE表示删除数据);
    2. 使用URL中的路径表示要操作的对象,比如说:POST/problem 新增一个题目,GET/problem获取题目信息,PUT/problem 修改题目 POST/user新增一个用户信息;GET/user获取用户信息,PUT/user修改用户信息
    3. 使用JSON格式来表示HTTP body中的数据。(JSON最大的优势在于可读性较好,JSON最大的劣势是效率比较低)
    4. 使用HTTP的状态码表示执行结果。
  • 那么知道了如何设计前后端的交互接口,那么我这个项目的前后交互接口有哪些呢?
    1. 请求获取题目列表 —— GET/problem
    2. 请求获取 题目的详情信息——GET/problem?id = 2
    3. 提交代码并且运行——POST/compile
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值