AcCode核心思路

文章目录


在线OJ项目核心思路

1. 项目介绍

该项目是一个类似于力扣的在线OJ平台,可以进行题目的编写和提交编译运行以及结果展示,使用的技术栈有:Java、MySQL、SpringBoot、MyBatis、Redis、Nginx、Docker

主要功能如下:

  1. 登录和注册(Session持久化+密码加盐)
  2. 图形验证码验证登录
  3. 题目管理(题目的添加和修改)
  4. 题目提交(编译+运行)
  5. 题目编译/运行结果展示
  6. Nginx+Docker实现负载均衡

2.预备知识

理解多进程编程

什么是进程?

进程可以看做操作系统中一个正在运行的程序的一个抽象,也可以把进程看做是程序的一次运行过程。在操作系统内部,进程是操作系统进行资源分配的基本单位

  • 使用 PCB(进程控制块) 描述进程

  • 组织:使用一定的数据结构来组织,常见做法就是使用双向链表

  • 进程之间是相互独立的

什么是多进程?

一个CPU运行多个进程

由于CPU的运行速度极快,虽然CPU在一直进行切换,但是咱们坐在电脑前的用户,是感知不到这个切换过程的

进程和线程的关系

  1. 进程是包含线程的,一个进程里可以有一个线程,也可以有多个线程
  2. 每个进程都有独立的内存空间(虚拟地址空间),同一个进程的多个线程之间,共用这个虚拟地址空间
  3. 进程是操作系统分配资源的基本单位,线程是操作系统调度执行的基本单位
  4. 如果一个进程挂了, 不会影响到其他进程. 如果一个线程挂了, 则整个进程都要异常终止.
  5. 进程更重量, 线程更轻量. 创建/销毁/调度线程比进程更高效.

Java中的多进程编程

Java中中对系统提供的进程创建、进程终止、进程程序替换、进程间通信进程了限制,最终只给用户提供了两个操作

进程的创建

创建出一个新的进程,让这个新的进程来执行一系列任务,被创建出来的进程,称为"子进程",创建子进程的进程,称为"父进程",服务器的进程就相当于一个父进程

根据收到的用户发送过来的代码再 创建出一个子进程,一个父进程,可以有多个子进程,但是一个子进程,只能有一个父进程

为啥采用多进程而不使用多线程?

一个操作系统上是运行了很多进程的,因为进程之间是相互隔离的,一个进程挂了是不会影响到其它进程的。如果使用多线程,我们并不知道用户提交的会提交什么样的代码,很可能提交一些恶意代码导致线程崩溃,而线程挂了很有可能就影响到了我们的整个服务进程。所以一定要采用多进程而不是多线程。

标准输入&标准输出&标准错误

java和javac是一个控制台程序,它的输出,是输出到"标准输出"和"标准错误"这两个特殊的文件当中的,一个进程启动的时候,就会自动打开三个文件:

  1. 标准输入,对应到键盘
  2. 标准输出,对应到显示器
  3. 标椎错误,对应到显示器

Runtime是Java中内置的一个单例类

  • 通过runtime.exec方法参数是一个字符串,表示一个可执行程序的路径,执行这个方法就会把指定路径的可执行程序,创建出一个子进程并执行。
  • runtime.exec()方法返回的是一个Process类,表示的就是一个子进程,后续通过这个子进程来进行操作
    • 获取标准输入:process.getInputStream():该方法能把process这个子进程的标准输出给读取出来
    • 获取标准错误:process.getErrorStream():该方法能把process这个子进程的标准错误给读取出来
    • 进程等待: process.waitFor():该方法能能让主进程进行阻塞等待,等待子进程process执行完毕。

3.项目实现

题目API实现

相关实体类定义

题目实体类

java 复制代码
public class Problem {
    private Integer id;
    private String title;
    private String levels;
    private String description;
    private String templateCode;
    private String testCode;
    private Date createTime;
    private Date updateTime;
}
新增/修改题目

新增修改题目通过判断url中的querystr里是否存在题目Id,来判断是修改题目还是新增题目

约定请求:

json 复制代码
post
{
    "id" : "",
    "title" : "题目标题",
    "levels" : "题目难度",
    "description" : "题干",
    "templateCode" : "题目代码模板",
    "testCode" :  "题目测试用例"
}

响应:

json 复制代码
{
    code : 200,
    message : ""
    data: 
}
java 复制代码
@PostMapping("/add")
public Response add(@RequestBody Problem problem) {
    if (problem == null || problem.getTitle() == null || "".equals(problem.getTitle().trim()) || problem.getLevels() == null ||
        "".equals(problem.getLevels().trim()) || problem.getTestCode() == null || "".equals(problem.getTestCode().trim()) ||
        problem.getTemplateCode() == null || "".equals(problem.getTemplateCode().trim())) {
        return Response.fail("题目参数不完整");
    }
    int ret = problemService.add(problem);
    if (ret == 1) {
        return Response.success(200,"添加成功");
    }
    return Response.fail("添加失败");
}
获取题目列表

请求:

json 复制代码
post
{
    /problem/all
}

响应:

json 复制代码
{
    code : 200,
    message:"",
    data:
    [
        {
            id : 1,
            title: "两数之和",
            levels: "简单",
            description: "题干",
            template: "题目模板"
        }
    ]
}

编译运行

通过Answer表示编译运行结果,约定:

  • 错误码为0表示运行成功
  • 错误码为1表示编译错误
  • 错误码为2表示运行错误
  • 错误码为1表示提交了违规代码
java 复制代码
public class Answer {
    // 错误码 0表示运行成功,1表示编译错误,2表示运行错误,-1表示违规代码
    private Integer errorCode;
    // 标准输出
    private String stdout;
    // 错误信息
    private String errorInfo;
}

Task类描述的是每一次代码的提交:

通过UUID生成唯一的目录,保证每个用户提交的代码相互隔离

java 复制代码
public class Task {

    // 存放临时文件目录
    private String workDir;
    // 运行文件路径
    private String className;
    // 编译文件路径
    private String classFile;
    // 存放编译错误信息文件
    private String compileErrorFile;
    // 标准输出文件
    private String stdoutFile;
    // 标准错误文件
    private String stderrFile;

    public Task() {
        this.workDir = "./tmp/"+UUID.randomUUID().toString()+"/";
        this.className = "Solution";
        this.classFile = workDir+ "Solution.java";
        this.compileErrorFile = workDir+"compileErrInfo.txt";
        this.stdoutFile = workDir+"stdout.txt";
        this.stderrFile = workDir+"stderr.txt";
    }
}
编译运行流程

请求:

json 复制代码
{
    problemId : "题目id",
    code : "提交的代码"
}

响应:

json 复制代码
{
    code : 200,
    message : "信息",
    data:{
        errorCode : "错误码",
        stdout: "标准输出",
        derrorInfo, "出错信息"
    }
}

编译运行流程:

  1. 对用户提交代码进行判空
  2. 从数据库中查询出测试用例进和提交代码进行拼接,形成完整代码。
  3. 对用户提交代码进行安全校验,判断其是否提交操作系统命令、文件网络等危险操作代码
  4. 把拼接好的代码写入到对应文件
  5. 进行编译和运行。

如下方法表示一次编译或者运行:

  • 通过判断stdoutFile是否为空来判断是编译还是运行
  • 从子进程process中的标准错误流中读取数据写入到task类的唯一的编译错误信息文件中,再判断文件内容是否为空
  • 如果编译错误信息文件不为空,说明编译出错直接返回
  • 如果编译错误信息文件为空,说明编译正确,再对编译后的字节码进行运行
  • 运行后再进行判断标准错误信息文件是否为空,如果为空说明运行正常,读取到标准输入文件里的信息返回给用户
java 复制代码
/**
     * 编译运行
     * @param cmd 执行的命令
     * @param stdoutFile
     * @param stderrFile
     * @return
     */
public static int run(String cmd,String stdoutFile,String stderrFile) {
    Runtime runtime =  Runtime.getRuntime();
    int exitCode = -1;
    try {
        // 执行命令获得子进程
        Process process = runtime.exec(cmd);
        // 编译
        if (stdoutFile == null) {
            try (InputStream stderrInoutStream = process.getErrorStream();OutputStream stderrOutputSteam = new FileOutputStream(stderrFile);){

                int ch;
                // 将错误信息读入到错误日志文件
                while ((ch = stderrInoutStream.read()) != -1) {
                    stderrOutputSteam.write(ch);
                }
            }

        }
        // 说明是运行
        if (stdoutFile != null) {
            try (InputStream stderrInoutStream = process.getErrorStream();
                 OutputStream stderrOutputSteam = new FileOutputStream(stderrFile);
                 InputStream stdoutInputStream = process.getInputStream();
                 OutputStream stdOutputStream = new FileOutputStream(stdoutFile)){
                // 获取标准错误输入流

                int ch;
                // 将错误信息读入到错误日志文件
                while ((ch = stderrInoutStream.read()) != -1) {
                    stderrOutputSteam.write(ch);
                }
                // 将子进程标准输出写入到指定文件
                while ((ch = stdoutInputStream.read()) != -1) {
                    stdOutputStream.write(ch);

                }
            }
        }
        // 进程等待
        exitCode = process.waitFor();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    return exitCode;
}

拼接编译命令时通过 -d指定编译后的文件存放到指定位置,不然找不到字节码文件位置。

java 复制代码
// 2.拼接编译命令
String compileCmd = String.format("javac -encoding utf8 %s -d %s",classFile,workDir);
//4.运行代码
String runCmd = String.format("java -classpath %s %s",workDir,className);

4.统一功能处理

统一登录拦截

定义拦截器:

  1. 创建自定义拦截器,实现Handlerlnterceptor 接口的preHandle(执行具体方法之前的预处理)方法
  2. 将自定义拦截器加入WebMvcConfigureraddInterceptors

提供一个管理员页面来对题目进行添加和修改。管理员页面使用拦截器对普通用户进行拦截.

java 复制代码
@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/user/login")
                .excludePathPatterns("/user/reg")
                .excludePathPatterns("/user/verificationCode")
                .excludePathPatterns("/login.html")
                .excludePathPatterns("/reg.html")
                .excludePathPatterns("/css/**")
                .excludePathPatterns("/js/**")
                .excludePathPatterns("/img/**");
        registry.addInterceptor(new AdminInterceptor())
                .addPathPatterns("/admin.html")
                .addPathPatterns("/addProblem.html")
                .addPathPatterns("/problem/update")
                .addPathPatterns("/problem/add");
    }
}

统一格式返回

统一的数据返回格式使用@ControllerAdvice+ResponseBodyAdvice实现

java 复制代码
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    @Resource
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof Response) {
            return body;
        }
        if (body instanceof  String) {
            try {
                return objectMapper.writeValueAsString(body);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        return Response.success(body);
    }
}

统一异常处理

java 复制代码
@ControllerAdvice
public class ExceptionAdvice {

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Response exceptionAdvice(Exception e) {
        return Response.fail("服务器异常");
    }
}

相关推荐
only-qi3 小时前
146. LRU 缓存
java·算法·缓存
且行志悠4 小时前
Mysql的使用
mysql
xuxie134 小时前
SpringBoot文件下载(多文件以zip形式,单文件格式不变)
java·spring boot·后端
白鹭4 小时前
MySQL源码部署(rhel7)
数据库·mysql
重生成为编程大王5 小时前
Java中的多态有什么用?
java·后端
666和7775 小时前
Struts2 工作总结
java·数据库
中草药z5 小时前
【Stream API】高效简化集合处理
java·前端·javascript·stream·parallelstream·并行流
野犬寒鸦5 小时前
力扣hot100:搜索二维矩阵 II(常见误区与高效解法详解)(240)
java·数据结构·算法·leetcode·面试
zru_96025 小时前
centos 系统如何安装open jdk 8
java·linux·centos