【我的OJ平台】

本文介绍了基于Java Servlet构建一个在线编程挑战平台的过程,涉及项目创建、Java多进程编程、数据库操作和前端实现。通过创建Task、Question和Answer类来管理题目和答案,使用CommandUtil进行编译运行,ProblemServlet和CompileServlet处理API请求,前端页面通过AJAX与服务器交互。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

项目描述

模拟实现类似于牛客以及leedcode的在线做题,通过浏览器实现客户端与服务器端的交互,将数据在网络中传输。

项目创建

基于Java 实现一个web程序(servlet)
1、创建maven项目,引入相关依赖
MySQL -connector:

 <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.49</version>
        </dependency>

Servlet:`

<dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

2、创建目录
servlet项目基本要有的目录结构
在这里插入图片描述
web.xml

<!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>Archetype Created Web Application</display-name>
</web-app>

ps:前置知识
1、I/O流(Java中的文件操作主要是I/O读写)
Java标准库中,java.io这个包里提供了很多操作文件的类
操作字节(二进制数据)——字节流:InputStream,FileInputStream,OutputStream,FileOutputStream
和操作字符(文本文件)——字符流:Reader,Writer,FileReader,FileWriter
注意:读写文件之前先打开文件,读写文件之后必须关闭文件
示例:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class TestFile {
    public static void main(String[] args) throws IOException {
        //把一个文件的内容读出来写到另一个指定的文件中(相当于拷贝功能)
        String srcPath="e:/test1.txt";
        String destPath="e:/test2.txt";
        //打开test1文件
        FileInputStream fileInputStream=new FileInputStream(srcPath);
        //打开第二个文件test2
        FileOutputStream fileOutputStream=new FileOutputStream(destPath);
        //循环把第一个文件里的内容按照字节的方式读取到第二个文件
        while(true){
            //read方法依次返回一个字节(byte)但是用int接收的理由:
            //1、Java中没有无符号数byte的范围:-128~127,实际应用需要返回0~255的数,就不能用byte接受
            //2、read都到末尾返回EOF(可以用-1表示)
            int ch= fileInputStream.read();
            if (ch==-1){
                //文件读取结束
                break;
            }
            fileOutputStream.write(ch);
        }
        //关闭文件
        fileInputStream.close();
        fileOutputStream.close();

    }
}

2、进程和线程的相关知识
进程:任务,操作系统想要执行一个具体的动作就要创建出一个对应的进程
“可执行文件”运行起来就是一个“进程”
并发:多个进程同时运行
“多进程编程”用来解决“并发编程”(将任务拆分成多个进程)
“线程”为了解决“多进程创建销毁的低效”,每个线程是独立的执行流,一个进程包含多个线程
虽然二者都能解决并发的问题,但是进程的独立性强,相互不影响(各有各的地址空间),多线程共用同一进程的地址空间
OJ项目:
服务器进程:运行Servlet,接受用户请求做出响应……
用户提交的代码也是一个独立的逻辑(使用多进程) 避免错误代码影响服务器进程

Java中的多进程编程

进程创建及进程等待

创建新进程,让新进程执行任务
服务器进程“父进程”,用户发来的进程“子进程”
Runtime.exec方法(属于Java中的内置类Runtime的一个方法):参数是一个字符串,表示一个可执行程序的路径;执行这个方法,就会把指定路径的可执行程序创建出进程并执行
一个进程在启动的时候,会自动打开三个文件;
1、标准输入——键盘
2、标准输出——显示器
3、标准错误——显示器
由于子进程未与idea关联,需手动获取

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

public class TestExec {
    public static void main(String[] args) throws IOException, InterruptedException {
        //Runtime在Java中是一个单例
        Runtime runtime=Runtime.getRuntime();
        //可创建子进程,但是父子进程并发执行
        Process process=runtime.exec("javac");//相当于在cmd中输入的命令
        //获取子进程标准输出
        InputStream stdoutFrom= process.getInputStream();
        //写入到另外一个文件
        FileOutputStream stdoutTo=new FileOutputStream("stdout.txt");
        while(true){
            int ch=stdoutFrom.read();
            if(ch==-1){
                break;
            }
            stdoutTo.write(ch);
        }
        stdoutFrom.close();
        stdoutTo.close();
        //获取标准错误,从这个文件对象中读,把子进程的标准错误读出来
        InputStream stderrFrom=process.getErrorStream();
        FileOutputStream stderrTo=new FileOutputStream("stderr.txt");
        while(true){
            int ch=stderrFrom.read();
            if(ch==-1){
                break;
            }
            stderrTo.write(ch);
        }
        stderrFrom.close();
        stderrTo.close();
        //通过process类的waitFor方法实现进程等待(父进程执行到waitFor就会阻塞,直到子进程执行完毕为止)
        //进程码为0正常退出,非0异常退出
        int exitCode=process.waitFor();
        System.out.println(exitCode);
    }
}

创建CommandUtil类

实现一个完整的“编译运行”的模块
要做的事情:
输入:用户提交的代码
输出:程序的编译结果和运行结果
由于Java中的类名和文件名需要一致,code字符串中的类名需要和写入的文件名一致
因此约定:类名文件名都叫Solution(代码往Solution里面写)

package compile;

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

public class CommandUtil {
    //1、通过Runtime类得到Runtime实例,执行exec方法
    //2、获取到标准输出,并写入到指定文件中
    //3、获取到标准错误,并写入到指定文件中
    //4、等待子进程结束,拿到子进程的状态码并返回
    public static int run(String cmd,String stdoutFile,String stderrFile) throws InterruptedException {
        try {
            //1、通过Runtime类得到Runtime实例,执行exec方法
            Process process=Runtime.getRuntime().exec(cmd);
            //2、获取到标准输出,并写入到指定文件中
            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();
            }
            //3、获取到标准错误,并写入到指定文件中
            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();
            }
            //4、等待子进程结束,拿到子进程的状态码并返回
            int exitCode=process.waitFor();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return 1;
    }

    public static void main(String[] args) throws InterruptedException {
        CommandUtil.run("javac","stdout.txt","stderr.txt");
    }
}

编译运行模块

创建Task类

package compile;

import com.sun.org.apache.xpath.internal.patterns.ContextMatchStepPattern;
import common.FileUtil;

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

//每次“编译+运行”的过程称为一个Task
public class Task {
    //通过一组常量约定临时文件名字
    //所有文件所在目录WORK_DIR
    private  String WORK_DIR=null;
    //约定代码类名
    private  String CLASS =null;
    //要编译的代码文件名
    private  String CODE=null;
    //存放编译错误信息的文件名
    private  String COMPILE_ERROR=null;
    //存放运行时标准输出的文件名
    private  String STDOUT=null;
    //存放运行时标准错误的文件名
    private  String STDERR=null;

    public Task(){
        //在Java中使用UUID这个类就能生成一个UUID
        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";
    }



    //这个Task类提供的核心方法,叫做compileAndRun
    //参数:需要编译运行的Java源代码
    //返回值:编译运行的结果,编译出错/运行出错/运行正确...
    public Answer compileAndRun(Question question) throws InterruptedException {
        Answer answer = new Answer();
        //准备用来存放临时文件的目录
        File workDir =new File(WORK_DIR);
        if(!workDir.exists()){
            //创建多级目录
            workDir.mkdirs();
        }

        //进行安全性判定
        if(!checkCodeSafe(question.getCode())){
            System.out.println("用户提交了不安全的代码");
            answer.setError(3);
            answer.setReason("提交的代码可能危害到服务器,禁止运行!");
            return answer;
        }
        //1、创建子进程,调用javac进行编译,编译需要.java文件,当前是通过String的方法提供的代码
        FileUtil.writeFile(CODE,question.getCode());
        //    如果编译出错,javac就会把错误信息写到stderr里面,可以用一个专门的文件进行保存(compileError.txt)
        //2、将Question中的code写入到一个Solution.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("")){
            System.out.println("编译错误!");
            //编译出错,直接返回Answer,让Answer里面记录编译的错误信息
            answer.setError(1);
            answer.setReason(compileError);
            return answer;
        }
        //3、创建子进程,调用java命令执行
        //    运行程序时,会把java子进程的标准输出和标准错误获取到(stdout.txt,stderr.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;
        }
        //4、让父进程获取到刚才编译执行的结果并打包成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 target :blackList){
            int pos = code.indexOf(target);
            if(pos >=0){
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) throws InterruptedException {
        Task task =new Task();
        Question question =new Question();
        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);
    }
}

为啥创建这么多临时文件?
目的:进行进程间通讯
进程和进程间存在独立性,一个进程很难影响到其他进程
在这里插入图片描述
实质上:某一个东西只要能被多个进程访问到,便可以用来进行进程间通讯。

创建Question类

package compile;

//用这个类表示一个task的输入内容
//会包含要编译的代码
public class Question {
    private String code;

    public String getCode() {
        return code;
    }

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

创建Answer类

package compile;//表示一个task的执行结果

public class Answer {
    //错误码,约定error为0表示编译运行都OK,为1表示编译出错,为2表示运行出错(抛异常)
    private int error;
    //设置出错原因,error为1,放编译错误信息,error为2,放运行错误信息
    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;
    }

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

封装文件

FileUtil类

package common;

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

public class FileUtil {
    //把filePath对应的文件内容读取出来,放到返回值中
    public static String readFile(String filePath){
        StringBuilder result = new StringBuilder();//String里字符串不能修改
        try( FileReader fileReader=new FileReader(filePath)) {
            while(true){
                int ch = fileReader.read();//每次读取一个字符
                if(ch==-1){
                    break;
                }
                result.append((char)ch);
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return result.toString();
    }
    //负责把content写入到filePath对应的文件中
    public static void writeFile(String filePath,String content){
        try(FileWriter fileWriter= new FileWriter(filePath)){
            fileWriter.write(content);

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

题目管理模块

数据库:保存当前题目信息
题目标题id、题目名称title、题目难度level、题目描述description、题目模板代码templateCode、测试用例代码testCode
在这里插入图片描述

create database if not exists oj_database;
use oj_database;
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)
);

在这里插入图片描述

DBUtil类

package common;

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

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

public class DBUtil {
    //需要封装和数据库之间的连接操作
    private static final String URL = "jdbc:mysql://127.0.0.1:3306/oj_database?characterEncoding=utf8&&useSSL=false";
    private static final String USERNAME = "root";
    private static final String PASSWORD =null;
    private static volatile MysqlDataSource dataSource = null;
    private static MysqlDataSource 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;//将数据类型迁移到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) {
                throw new RuntimeException(e);
            }
        }
        if(statement != null){
            try {
                statement.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        if(resultSet != null){
            try {
                resultSet.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

封装数据库操作

通过一个实体类对象对应表中的一条记录
创建dao包(数据访问对象)

Problem类(实体类),一个Problem对象对应表中的一条记录

package dao;

public class Problem {
    private int id;
    private String title;
    private String level;
    private String description;
    private String templateCode;
    private String testCode;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getLevel() {
        return level;
    }

    public void setLevel(String level) {
        this.level = level;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getTemplateCode() {
        return templateCode;
    }

    public void setTemplateCode(String templateCode) {
        this.templateCode = templateCode;
    }

    public String getTestCode() {
        return testCode;
    }

    public void setTestCode(String testCode) {
        this.testCode = testCode;
    }

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

对于表的增删改查操作,创建一个ProblemDAO来负责进行这些操作

ProblemDAO类

package dao;

import common.DBUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

//封装对problem的增删改查
//管理员:新增题目,删除题目
//普通用户:查询题目列表,查询题目详情
public class ProblemDAO {
    public void insert(Problem problem) {
        Connection connection =null;
        PreparedStatement statement =null;

        try{
            //1、和数据可建立连接
             connection = DBUtil.getConnection();
            //2、构造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());
            //3、执行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{
            //1、和数据库建立连接
            connection =DBUtil.getConnection();
            //2、拼装sql语句
            String sql = "delete from oj_table where id =?";
            statement =connection.prepareStatement(sql);
            statement.setInt(1,id);
            //3、执行sql
           int ret = statement.executeUpdate();
            if (ret !=1){
                System.out.println("删除题目失败!");
            }else{
                System.out.println("删除题目成功!");
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally{
            DBUtil.close(connection,statement,null);
        }

    }
    //将当前列表中的所有题目都查出来"分页查询“
    //前端实现分页器,后端用sql limit offset算
    public List<Problem> selectAll(){
        List<Problem> problems = new ArrayList<>();
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try{
            //1、和数据库建立连接
            connection = DBUtil.getConnection();
            //2、拼装sql
            String sql = "select id,title,level from oj_table ";
            statement = connection.prepareStatement(sql);
            //3、执行sql
            resultSet = statement.executeQuery();
            //4、遍历resultSet
            while(resultSet.next()){
                //每一行都是一个Problem对象
                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 e) {
            throw new RuntimeException(e);
        }finally{
            DBUtil.close(connection,statement,resultSet);
        }
       // return null;
    }
    //查找一道具体的题目
    public Problem selectOne(int id){
        Connection connection =null;
        PreparedStatement statement =null;
        ResultSet resultSet =null;
        try {
            //1、和数据库建立连接
            connection =DBUtil.getConnection();
            //2、拼接SQL语句
            String sql = "select * from oj_table where id=? ";
            statement = connection.prepareStatement(sql);
            statement.setInt(1,id);
            //3、执行SQL语句
            resultSet = statement.executeQuery();
            //4、遍历查询结果(由于id是主键,按照id查处的结果一定唯一)
            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) {
            throw new RuntimeException(e);
        }finally{
            DBUtil.close(connection,statement,resultSet);
        }
        return null;
    }

    private static void testInsert() throws SQLException {
        ProblemDAO problemDAO =new ProblemDAO();
        Problem problem =new Problem();
        problemDAO.insert(problem);
        problem.setTitle("两数之和");
        problem.setLevel("简单");
        problem.setDescription("给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。\n" +
                "\n" +
                "你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。\n" +
                "\n" +
                "你可以按任意顺序返回答案。\n" +
                "\n" +
                " \n" +
                "\n" +
                "示例 1:\n" +
                "\n" +
                "输入:nums = [2,7,11,15], target = 9\n" +
                "输出:[0,1]\n" +
                "解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。\n" +
                "示例 2:\n" +
                "\n" +
                "输入:nums = [3,2,4], target = 6\n" +
                "输出:[1,2]\n" +
                "示例 3:\n" +
                "\n" +
                "输入:nums = [3,3], target = 6\n" +
                "输出:[0,1]\n" +
                " \n" +
                "\n" +
                "提示:\n" +
                "\n" +
                "2 <= nums.length <= 104\n" +
                "-109 <= nums[i] <= 109\n" +
                "-109 <= target <= 109\n" +
                "只会存在一个有效答案\n" +
                "进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗?\n" +
                "\n" +
                "来源:力扣(LeetCode)\n" +
                "链接:https://leetcode.cn/problems/two-sum\n" +
                "著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。");
        problem.setTemplateCode("class Solution {\n" +
                "    public int[] twoSum(int[] nums, int target) {\n" +
                "\n" +
                "    }\n" +
                "}");
        problem.setTestCode(" public static void main(String[] args) {\n" +
                "        Solution solution =new Solution();\n" +
                "        //testcase1\n" +
                "        int[] nums ={2,7,11,15};\n" +
                "        int target = 9;\n" +
                "        int[] result = solution.twoSum(nums,target);\n" +
                "        if(result.length == 2 && result[0]==0 && result[1]==1){\n" +
                "            System.out.println(\"testcase1 successful\");\n" +
                "        }else{\n" +
                "            System.out.println(\"testcase1 failed\");\n" +
                "        }\n" +
                "        //testcase2\n" +
                "        int[] nums2={3,2,4};\n" +
                "        int target2 =6;\n" +
                "        int[] result2 =solution.twoSum(nums2,target2);\n" +
                "        if(result2.length==2 && result[0]==1 && result[1]==2){\n" +
                "            System.out.println(\"testcase2 successful\");\n" +
                "        }else{\n" +
                "            System.out.println(\"testcase2 failed\");\n" +
                "        }\n" +
                "    }");
        problemDAO.insert(problem);
    }

    private static void testSelectAll(){
        ProblemDAO problemDAO = new ProblemDAO();
        List<Problem> problem = problemDAO.selectAll();
        System.out.println(problem);
    }

    private static void testSelectOne(){
        ProblemDAO problemDAO =new ProblemDAO();
        Problem  problem = problemDAO.selectOne(1);
        System.out.println(problem);
    }

    private static void testDelete(){
        ProblemDAO problemDAO =new ProblemDAO();
        problemDAO.delete(8);
    }

    public static void main(String[] args) throws SQLException {
        testInsert();
        //testSelectAll();
        //testSelectOne();
        //testDelete();

    }

}

Solution类

public class Solution {
    public int[] twoSum(int[] nums, int target) {
    int [] a={0,1};
    return a;
    }
    //这个main方法就相当于测试用例的代码testCode
    public static void main(String[] args) {
        Solution solution =new Solution();
        //testcase1
        int[] nums ={2,7,11,15};
        int target = 9;
        int[] result = solution.twoSum(nums,target);
        if(result.length == 2 && result[0]==0 && result[1]==1){
            System.out.println("testcase1 successful");
        }else{
            System.out.println("testcase1 failed");
        }
        //testcase2
        int[] nums2={3,2,4};
        int target2 =6;
        int[] result2 =solution.twoSum(nums2,target2);
        if(result2.length==2 && result[0]==1 && result[1]==2){
            System.out.println("testcase2 successful");
        }else{
            System.out.println("testcase2 failed");
        }
    }
}

设计服务器提供的API

API(一些预先定义的接口,如函数、HTTP接口,通过这些接口可以和网页前端进行交互)
一、设计服务器需要考虑到有哪些页面
1、题目列表页:
功能是展示当前题目的列表→向服务器请求,题目的列表
2、题目详情页:
功能一:展示题目的详细要求→向服务器请求,获取指定题目的详细信息
功能二:有一个代码编辑框,让用户编写代码(纯前端,无需交互)
功能三:有一个提交按钮,点击提交按钮就能把用户的代码提交到服务器上,服务器进行编译和运行,并返回结果→向服务器发送用户当前编写得代码,并获取结果
二、具体设计这几根前后端交互的API
流行的前后端交互方式:通过JSON格式来组织,解析比较麻烦→引入第三方库Jackson
1、实现第一个API(向服务器请求,题目的列表)
在这里插入图片描述
2、实现第二个API(向服务器请求,获取指定题目的详细信息)
在这里插入图片描述
3、实现第三个API(向服务器发送用户当前编写得代码,并获取结果)

如何向服务器发送用户的代码
①GET:将代码放到url中,通过querystring来发,但需要对代码的字符串进行urlencode(比较麻烦)
②POST:将代码放到body中即可
在这里插入图片描述

实现题目管理页

创建api包
在这里插入图片描述

ProblemServlet类

在这里插入图片描述

package api;

import com.fasterxml.jackson.databind.ObjectMapper;
import dao.Problem;
import dao.ProblemDAO;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@WebServlet ("/problem")//便于服务器找到这个类
public class ProblemServlet extends HttpServlet {
    private ObjectMapper objectMapper = new ObjectMapper();//jason核心类
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setStatus(200);
        resp.setContentType("application/json;charset=utf8");
        ProblemDAO problemDAO =new ProblemDAO();
        //尝试获取id参数,如果能获取到,说明获取的是题目详情;如果不能获取到,说明书获取题目列表
        String idString = req.getParameter("id");
        if(idString == null || "".equals(idString)){
            //没有获取到id字段,查询题目列表
            List<Problem> problems =problemDAO.selectAll();
            //将problems转化为字符串
            String respString =objectMapper.writeValueAsString(problems);
            //设置HTTP响应的body部分
            resp.getWriter().write(respString);
        }else{
            //获取到题目id,查询题目详情
            Problem problem = problemDAO.selectOne(Integer.parseInt(idString));
            String respString = objectMapper.writeValueAsString(problem);
            resp.getWriter().write(respString);
        }
    }
}

CompileServlet类

在这里插入图片描述

package api;

import com.fasterxml.jackson.databind.ObjectMapper;
//import compile.Answer;
import common.CodeInvalidException;
import common.ProblemNotFoundException;
import compile.Answer;
import compile.Question;
import compile.Task;
import dao.Problem;
import dao.ProblemDAO;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;

@WebServlet("/compile")
public class CompileServlet extends HttpServlet {
    static class CompileRequest{
        public int id;
        public String code;
    }
    static class CompileResponse{
        //约定error为0表示编译运行都正常,1表示编译出错,2表示运行出错(用户代码异常),3表示其他错误
        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 {
        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();
            }
            //testCode是测试用例的代码
            String testCode = problem.getTestCode();
            //requestCode是用户提交的代码
            String requestCode = compileRequest.code;
            //3、把用户提交的代码和测试用例代码,拼接成一个完整的代码
            String finalCode = mergeCode(requestCode, testCode);
            System.out.println(finalCode);
            if(finalCode == null){
                throw new CodeInvalidException();
            }
            //4、创建一个Task实例,调用里面的compileAndRun进行编译运行
            Task task = new Task();
            Question question = new Question();
            question.setCode(finalCode);
            Answer answer;
            try {
                answer = task.compileAndRun(question);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            //5、根据Task运行的结果,包装成一个HTTP响应
            compileResponse.error = answer.getError();
            compileResponse.reason = answer.getReason();
            compileResponse.stdout = answer.getStdout();
            String respString = objectMapper.writeValueAsString(compileResponse);
            resp.getWriter().write(respString);
        } catch (ProblemNotFoundException e) {
            //处理题目没有找到的异常
            compileResponse.error=3;
            compileResponse.reason ="没有找到指定题目 id ="+ compileRequest.id;
        } catch (CodeInvalidException e) {
            compileResponse.error=3;
            compileResponse.reason ="提交的代码不符合要求!";
            String respString = objectMapper.writeValueAsString(compileResponse);
            resp.getWriter().write(respString);
        }

    }

    private String mergeCode(String requestCode, String testCode) {//合并代码的方法实现
        //1、查找requestCode中的最后一个 }
        int pos = requestCode.lastIndexOf("}");
        if(pos==-1){
            //说明提交的代码完全没有},代码必然是错误的
            return null;
        }
        //2、根据这个位置进行字符串截取,排除最后一个}
        String subStr = requestCode.substring(0,pos);
        //3、进行拼接
        return subStr + testCode + "\n";
    }

    private 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) {
            throw new RuntimeException(e);
        }

        //5、把这个byte[]的内容构造成一个String
        return new String (buffer,"utf8");//把二进制转成文本数据
    }
}

在这里插入图片描述

每次有一个请求过来,都需要生成一组临时文件,如果同一时刻,有N个请求一起过来了,这些请求的临时文件名和所在目录都是一样的,此时多个请求之间就会产生“相互干扰”的情况(类似于线程安全问题)
解决:让每一个请求创建的work-dir目录都不一样(可以使用“唯一ID”来作为目录名字→UUID“全球唯一”),每个请求生成一个唯一的UUID,进一步创建一个以UUID命名的临时目录,请求生成的临时文件就放在这个临时目录中。由于UUID唯一,请求也就不会互相影响。(磁盘上的临时目录在达到一定上限时,磁盘会定期清理)
在这里插入图片描述

前端的实现

题目列表页

在这里插入图片描述
页面不能写死,需要让页面通过ajax的方式从服务器获取

<script>
            //在页面加载的时候,尝试从服务器上获取题目列表,通过ajax的方式进行获取。
            function getProblems(){
                //1、先通过ajax从服务器获取到题目列表,$是jQuery中的特殊变量
                $.ajax({
                    url:"problem",
                    type:"GET",
                    success: function(data,status){
                        //data是响应的body,status是响应的状态码
                        //2、把得到的响应数据给构造成HTML片段
                        makeProblemTable(data);
                    }
                });
            }
            //通过这个函数把数据转化成HTML页面片段
            function makeProblemTable(data){
                let problemTable =document.querySelector("#problemTable");
                for (let problem of data){
                    let tr =document.createElement("tr");
                    let tdId = document.createElement("td");
                    tdId.innerHTML =problem.id;
                    tr.appendChild(tdId);

                    let tdTitle = document.createElement("td");
                    let a =document.createElement("a");
                    a.innerHTML = problem.title;
                    a.herf = 'problemDetail.html?id='+problem.id;
                    a.target = '_blank';
                    tdTitle.appendChild(a);
                    tr.appendChild(tdTitle);

                    let tdLevel = document.createElement("td");
                    tdLevel.innerHTML = problrm.level;
                    tr.appendChild(tdLevel);

                    problemTable.appendChild(tr);

                }
            }
            getProblems();
        </script>

题目详情页

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值