基于java开发的在线OJ系统

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


项目背景

在线OJ系统实现像LeetCode,牛客网等在线oj网站所提供的的核心功能,可以实现在线做题,在线判题等功能。


一、项目说明

1.项目平台与技术栈

  • 技术栈:Servlet、Mysql、Runtime,多进程
  • 平台与环境:Windows、IDEA、Maven、Tomcat

2.项目功能

  • 题目列表页:展示题目id,题目标题,题目难易程度
  • 题目详情页:展示某个题的详细信息+代码编辑框
  • 提交并运行题目
  • 查看运行结果

二、项目演示

1.进入题目列表页

在这里插入图片描述

在这里插入图片描述

2.进入题目详情页

在这里插入图片描述

3.代码编辑框

在这里插入图片描述

4.提交代码并运行

在这里插入图片描述

三、项目模块划分

1.编译运行模块(compile)

此模块主要用来能让用户进行题目的编译和运行的操作。本模块里边有大致划分为四个模块。

1)执行命令模块(CommandUitl):

这个类里边完成的功能就是能进行编译和运行的指令。要进行编译和运行我们就要创建一个新的进程(子进程),用到的技术有单例设计模式、多进程。

  • 里面用run方法来实现这个模块,传入三个参数:指令,将标准输入,输出写入到哪个文件中。
    想实现编译运行,就首先需要传入一个指令,其次指令编译运行后,有三种状态:编译运行都成功,编译成功运行失败,编译失败。因此需要获取到标准输出,将其写入到一个文件中,表示编译运行都成功;获取标准错误,将其写入到一个文件,表示编译运行时的错误。

  • 单例设计模式和多进程:使用Runtime.exec方法,创建子进程。其参数表示一个可执行程序的路径,执行这个方法,就会把指定的路径的可执行程序,创建出进程并执行。同时Runtime在JVM中是一个单例模式。

  • 子进程创建后,父进程要结束的话,要等待子进程结束后才能最终结束所有进程。所以要进行等待子进程的结束,用的是 .waitFor()方法。

  • 不管是标准输出还是标准错误里边的内容,我们都采用重定向,即将两个里边的内容放在我们重新创建的文件中,方便后边对文件的读写操作。

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.getErrorStream();
                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();
            return exitCode;
        } catch (IOException |InterruptedException e){
            e.printStackTrace();
        }
        return 1;
    }

2)用户编译模块(Question):

当点进具体的题目时,用户编辑代码。因此这个模块完成的功能是用户代码的输入。只需要定义code即可。

public class Question {
    private String code;

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }
}

3)运行结果模块(Answer):

这个模块用来反馈运行结果给用户,结果包含错误码error,出错原因reason,标准输出信息stdout,标准错误信息stderr。

  • 约定error为0表示编译运行都ok,为1编译出错,为2运行出错。reason为出错的提示信息。
public class Answer {
   private int error;

   public String getReason() {
       return reason;
   }

   public void setReason(String reason) {
       this.reason = reason;
   }

   private String reason;

   public String getStderr() {
       return stderr;
   }

   public void setStderr(String stderr) {
       this.stderr = stderr;
   }

   public String getStdout() {
       return stdout;
   }

   public void setStdout(String stdout) {
       this.stdout = stdout;
   }

   private  String stdout;
   private  String stderr;

   public int getError() {
       return error;
   }

   public void setError(int error) {
       this.error = error;
   }

   @Override
   public String toString() {
       return "Answer{" +
               "error=" + error +
               ", reason='" + reason + '\'' +
               ", stdout='" + stdout + '\'' +
               ", stderr='" + stderr + '\'' +
               '}';
   }
}

4)编译运行整个过程(Task):

每次的编译运行,都用Task类来完成,里面提供的核心方法为compileAndRun。参数为要编译运行的java源代码。返回值表示编译运行的结果。

  • 一次编译运行过程中,会产生很多临时文件,要编译运行的临时文件,编译出错的临时文件、运行出错的标准错误文件、标准输出的临时文件等等。我们将这些临时的文件放在一个目录下,方便后边进程间通信。
    考虑到会有多个人进行此系统的操作。因此要保证每一个用户生成的临时文件都有唯一的目录。用UUID来生成唯一目录。
  • java中类名和文件名要一致,因此code字符串的类名字,就要和写入的文件名一致。约定:类名和文件名都叫做Solution
  • 创建子进程,调用javac进行编译,编译时需要有一个.java文件,因此把question中的code写入到一个Solution.java文件中。编译完成后产生一个.class文件。
    创建子进程,执行java运行命令,也就是运行刚编译好的.class文件。父进程获取到刚才编译执行的结果,并打包成Answer对象。
    父进程获取到刚才编译执行的结果,并打包成Answer对象。
  • 写一个FileUtil类,提供两个方法,一个负责读取整个文件内容,返回一个字符串readFile,另一个方法负责写入整个字符串到文件中writeFile
public class Task {
   //通过一组常量来约定临时文件的名字
   //这个表示所有临时文件所在的目录
   private  String WORK_DIR="./tmp/";
   //约定代码的类名
   private   String CLASS=null;
   //要编译的代码文件
   private   String CODE=null;
   //存放编译错误信息的文件
   private   String COMPILE_ERROR=null;
   //存放运行时的标准输出
   private   String STDOUT=null;
   //存放运行时标准错误
   private   String STDERR=null;


   public Task(){
       WORK_DIR="./tmp/"+ UUID.randomUUID().toString()+"/";
       CLASS="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(Question question){
       Answer answer=new Answer();
       File workDir=new File(WORK_DIR);
       if (!workDir.exists()){
           workDir.mkdirs();
       }
       //1.把question中的code写入到一个Solution.java文件中
       FileUtil.writeFile(CODE,question.getCode());
       //2.创建子进程,调用javac进行编译,编译时候需要有一个.java文件
       //    如果编译出错,javac就会把错误信息给写入到stderr里,就可以用一个专门的文件来保存compileError.txt
       //需要先把编译命令构造出来
       String compileCmd=String.format("javac -encoding utf8 %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
           System.out.println("编译出错");
           answer.setError(1);
           answer.setReason(compileError);
           return answer;
       }
       //编译正确
       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;

   }

2.题目管理模块(problem):

这个模块设计了数据库,封装了数据库操作。此模块实现的功能进行题目的插入、删除、题目列表页的显示和题目详情页的显示。

1)与数据库建立连接

写一个类DBUtil:封装和数据库之间的连接操作。

public class DBUtil {
    private static final String URL="";
    private static final String USERNAME="";
    private static final String PASSWORD="";

    private static  volatile DataSource dataSource=null;

    private  static DataSource getDataSource(){
        if (dataSource==null){
            synchronized (DBUtil.class){
                if (dataSource==null){
                    MysqlDataSource mysqlDataSource=new MysqlDataSource();
                    mysqlDataSource.setURL(URL);
                    mysqlDataSource.setUser(USERNAME);
                    mysqlDataSource.setPassword(PASSWORD);
                    dataSource=mysqlDataSource;
                }
            }
        }
        return dataSource;
    }
    public static Connection getConnection() throws SQLException{
        return getDataSource().getConnection();
    }

    public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {
        if (resultSet!=null){
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (statement!=null){
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (connection!=null){
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

2)题目实体类Problem:

这个类是一个实体类,表示的是数据库中的一条记录就对应的是网页上边显示的每一个实例。里边的属性有:题目id,标题,难度,描述,模板代码,测试代码。

public class Problem {
    private int id;
@Override
    public String toString() {
        return "Problem{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", level='" + level + '\'' +
                ", description='" + description + '\'' +
                ", templateCode='" + templateCode + '\'' +
                ", testCode='" + testCode + '\'' +
                '}';
    }

    private String title;
    private String level;
    private String description;
    private String templateCode;
    private String testCode;
}

3)数据访问类PboblemDAO:

一个Problem对象,就对应着表中的一条记录,还需要针对这个表进行“增删查改”,创建一个ProblemDAO来负责进行增删查改。

  • 增 (insert):主要用的是JDBC来操作数据库,基本步骤是:和数据库建立连接、拼装sql、执行sql、断开连接。

  • 删(delete):同上,还是JDBC操作数据库,基本步骤:和数据库建立连接、拼装sql、执行sql、断开连接。

  • 题目列表页的展示(selectAll):展示题目列表页,题目id,标题,难易程度等。其JDBC操作基本步骤是:建立连接、拼装sql、执行sql、遍历结果集、断开连接。

  • 题目详情展示页(selectOne):展示一个题目的所有信息(题目序号、标题、难度级别、题目描述、模板代码、测试代码)。还是要给用户打开页面进行结果集的展示JDBC操作步骤:建立连接、拼装sql、执行sql、遍历结果集、断开连接。

public class ProblemDAO {
    public void insert(Problem problem){
        Connection connection= null;
        PreparedStatement statement=null;
        try {
            //与数据库建立连接
            connection = DBUtil.getConnection();
            //构造SQL语句
            String sql="insert into oj_table values(null,?,?,?,?,?)";
            statement=connection.prepareStatement(sql);
            statement.setString(1,problem.getTitle());
            statement.setString(2,problem.getLevel());
            statement.setString(3,problem.getDescription());
            statement.setString(4,problem.getTemplateCode());
            statement.setString(5,problem.getTestCode());
            //执行SQL
            int ret=statement.executeUpdate();
            if (ret!=1){
                System.out.println("题目新增失败");
            }else {
                System.out.println("题目新增成功");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            DBUtil.close(connection,statement,null);
        }
    }
    public void delete(int id){
        Connection connection= null;
        PreparedStatement statement=null;
        try {
            //和数据库建立连接
            connection=DBUtil.getConnection();
            //拼装sql语句
            String sql="delete from oj_table where id = ?";
            statement=connection.prepareStatement(sql);
            statement.setInt(1,id);
            //执行SQL
            int ret=statement.executeUpdate();
            if (ret!=1){
                System.out.println("删除题目失败");
            }else {
                System.out.println("删除题目成功");
            }
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }finally {
            DBUtil.close(connection,statement,null);
        }
    }
    public List<Problem> selectAll(){
        List<Problem> problems=new ArrayList<>();
        Connection connection=null;
        PreparedStatement statement=null;
        ResultSet resultSet=null;
        try {
            //和数据库建立连接
            connection=DBUtil.getConnection();
            //拼装SQL
            String sql="select id,title,level from oj_table";
            statement=connection.prepareStatement(sql);
            //执行SQL
            resultSet=statement.executeQuery();
            //遍历resultSet
            while (resultSet.next()){
                Problem problem=new Problem();
                problem.setId(resultSet.getInt("id"));
                problem.setTitle(resultSet.getString("title"));
                problem.setLevel(resultSet.getString("level"));
                problems.add(problem);
            }
            return problems;
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }finally {
            DBUtil.close(connection,statement,resultSet);
        }
        return null;
    }
    public Problem selectOne(int id){
        Connection connection=null;
        PreparedStatement statement=null;
        ResultSet resultSet=null;
        try {
            connection=DBUtil.getConnection();
            String sql="select * from oj_table where id = ?";
            statement=connection.prepareStatement(sql);
            statement.setInt(1,id);
            resultSet=statement.executeQuery();
            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 throwables) {
            throwables.printStackTrace();
        }finally {
            DBUtil.close(connection,statement,resultSet);
        }
        return null;
    }

3. API模块:

此模块用来实现前后端的交互。主要由两个类构成,CompileServlet,ProblemServlet

  • 要实现交互的页面:
    题目列表页:向服务器请求题目列表
    题目详情页:展示题目详细要求,向服务器请求,获取指定题目的详细信息。
    题目详情页:向服务器发送用户当前编写的代码,并获取到结果。
  • 具体设计前后端交互的API:通过JSON格式来组织,引入第三方库Jackson

1)CompileServlet类(获取用户提交代码后的结果)

  • 编译请求CompileRequest:向服务器发送用户编写的代码,服务器只需要获取题目id对应到题目和用户提交的代码code即可。
  • 编译响应CompileResponse:在给服务端发送请求之后,返回响应,响应里边包含返回码error(0编译运行都ok,1表示编译出错,2表示运行出错)、错误原因reason、标准输出(测试用例的输出情况,包含了通过几个测试用例这样的信息)。
  • 编译Servlet:CompileServlet
    这个模块主要是要完成数据格式之间的转换,完成客户端和服务端之间的相互解析响应全过程。
    (1)先读取请求的正文,按照JSON格式进行解析
    (2)根据id从数据库中查找到题目的详情,得到测试用例代码
    (3)把用户提交的代码和测试用例的代码,给拼接为一个完整代码
    (4)创建一个Task实例,调用里面的compileAndRun,来进行编译运行。
    (5)根据Task运行的结果,包装成一个HTTP响应
@WebServlet("/compile")
public class CompileServlet extends HttpServlet {
    static class CompileRequest {
        public int id;
        public String code;
    }

     private class CompileResponse {
        public  int error;
        public  String reason;
        public  String stdout;
    }
    private ObjectMapper objectMapper=new ObjectMapper();

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("用户的当前工作目录:"+System.getProperty("use.dir"));
        CompileRequest compileRequest=null;
        CompileResponse compileResponse=new CompileResponse();
        try {
            resp.setStatus(200);
            resp.setContentType("application/json;charset=utf8");
            // 1.先读取请求的正文,并按照JSON 格式进行解析
            String body = readBody(req);
            compileRequest=objectMapper.readValue(body,CompileRequest.class);
            // 2.根据id从数据库中查找到题目的详情=> 得到测试用例代码
            ProblemDAO problemDAO=new ProblemDAO();
            Problem problem=problemDAO.selectOne(compileRequest.id);
            if (problem==null){
                throw new ProblemNotFoundException();
            }
            String testCode=problem.getTestCode();
            String requestCode=compileRequest.code;
            // 3.把用户提交的代码和测试用例代码,给拼接成个完整的代码.
            String finalCode=mergeCode(requestCode,testCode);
            if (finalCode==null){
                throw new CodeInValidException();
            }
            //System.out.println(finalCode);
            // 4.创建一个Task 实例,调用里面的compileAndRun 来进行编译运行
            Task task=new Task();
            Question question=new Question();
            question.setCode(finalCode);
            Answer answer=task.compileAndRun(question);
            // 5.根据Task运行的结果,包装成一个HTTP响应
            compileResponse.error=answer.getError();
            compileResponse.reason=answer.getReason();
            compileResponse.stdout=answer.getStdout();
        } catch (ProblemNotFoundException e) {
            compileResponse.error=3;
            compileResponse.reason="确认没有找到指定题目 id="+compileRequest.id;
        } catch (CodeInValidException e) {
            compileResponse.error=3;
            compileResponse.reason="提交的代码不符合要求";
        }finally {
            String respString=objectMapper.writeValueAsString(compileResponse);
            resp.getWriter().write(respString);
        }


    }
    private static String mergeCode(String requestCode,String testCode){
        int pos=requestCode.lastIndexOf("}");
        if (pos==-1){
            return null;
        }
        String subStr=requestCode.substring(0,pos);
        return subStr+testCode+"\n}";
    }

    private static String readBody(HttpServletRequest req) throws UnsupportedEncodingException {
        // 1.先根据请求头里面的ContentLength 获取到body 的长度(单位是字节)
        int contentLength=req.getContentLength();
        // 2.按照这个长度准备好一个byte[].
        byte[] buffer=new byte[contentLength];
        // 3.通过req 里面的getInputStream 方法,获取到body 的流对象。
        try(InputStream inputStream=req.getInputStream()){
            // 4.基于这个流对象,读取内容,然后把内容放到byte[] 数组中即可.
            inputStream.read(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 5.把这个byte[] 的内容构造成个String
        return new String(buffer,"utf8");
    }
}

2)ProblemServlet类

这个模块是用来展示题目列表页和题目详情页的。

  • doGet方法:通过创建problemDao请求对象,向服务端发起请求,在数据库中查找结果并转化成Json格式最终返回给客户端界面。完成的功能是展示题目列表页和题目详情页。
@WebServlet("/problem")
public class ProblemServlet extends HttpServlet {
    private ObjectMapper objectMapper=new ObjectMapper();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setStatus(200);
        resp.setContentType("application/json;charset=utf8");
        ProblemDAO problemDAO=new ProblemDAO();

         String idString=req.getParameter("id");
         if (idString==null||"".equals(idString)){
             List<Problem> problems=problemDAO.selectAll();
             String respString=objectMapper.writeValueAsString(problems);
             resp.getWriter().write(respString);
         }else{
             Problem problem=problemDAO.selectOne(Integer.parseInt(idString));
             String respString=objectMapper.writeValueAsString(problem);
             resp.getWriter().write(respString);
         }
    }
}

总结

以上就是今天要讲的内容,本文介绍了在线oj系统的核心功能:展示题目列表页,题目详情页,提交代码和显示结果如何实现。至于前端页面,小伙伴们可以自行实现想要的页面。欢迎大家讨论。

  • 5
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值