【在线OJ项目】核心技术之用户提交代码的编译运行

news2024/11/17 1:53:48

目录

一、认识Java进程编程

二、在线OJ核心思路

三、封装进程的执行

四、封装文件读写

五、封装用户提交代码的编译运行


一、认识Java进程编程

在之前的文章里提到了Java进程编程的相关API【JavaEE】Java中进程编程_1373i的博客-CSDN博客https://blog.csdn.net/qq_61903414/article/details/130497143?spm=1001.2014.3001.5501

二、在线OJ核心思路

在线OJ项目重点在于用户完成题目后,服务器获取到代码后,如何  验证代码的正确性。我们可以开启一个进程或线程去执行用户提交的这段代码,但是为什么不选择线程而选择进程呢,因为进程是相互独立的,如果用户提交恶意代码,我们开启一个进程去执行,此时该进程挂掉不会终止服务器进程。如果我们使用线程去执行这段恶意代码,该线程挂掉会导致整个服务器进程挂掉。所以我们选择使用进程去执行用户提交的代码。一个进程在开始执行时,会自动打开3个属于该进程的文件,分别是标准错误文件,标准输入文件,标准输出文件。当我们开启进程去执行用户提交的代码时,该代码执行时的结果(错误或sout的结果,类似在idea终端打印的错误消息相同)就会保存到对应的文件里

 那么相应的用户提交的代码执行的结果(错误、结果)都会在相对应的文件里,我们只需要去读取相对应的文件里的信息然后返回给前端展示给用户即可。所以在线OJ的核心是用户提交代码的编译与运行,核心步骤是:

获取到前端传来的用户提交代码

--》将用户提交的代码保存到一个.java文件

-》然后开启一个进程通过javac命令将该java文件编译为.class文件

-》然后读取该进程的标准错误文件看是否编译出错,如果编译出错则将编译出错的信息(第几行……错误)-

》然后再开启一个进程通过java命令去运行该代码

-》此时我们就可以读取该进程的标准输出文件与标准错误文件对用户提交代码是否满足题意进行校验

三、封装进程的执行

在前面的文章中我们了解了Java中如何创建进程以及如何让进程等待,现在我们需要对创建进程以及对获取进程结束后的三个文件操作进行封装,执行子进程后将该进程执行的结果即(标准输出、标准输入读取到指定的文件里面)在Java中可以用Process类表示进程,后续封装我们需要使用基于该类进行封装



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

/**
 * 执行对应的java进程,将执行结果写入文件
 */
public class ProcessUtil {
    /**
     * 将创建进程执行相应的代码进程封装
     * @param cmd  指令
     * @param stdoutFile 标准输入的文件复制路径
     * @param stderrFile 标准错误的文件复制路径
     * @return 进程执行的状态码
     */
    public static int run(String cmd, String stdoutFile, String stderrFile) {
        try {
            /* 1 通过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();
            return exitCode;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return 1;
    }
}

四、封装文件读写

封装完了进程创建以及获取进程的结果(标准输出标准错误)后,此时我们需要将从前端获取到的代码保存到一个指定的.java文件里,所以我们对文件的读写进行封装,将文件内容读取到字符串里,以及将字符串内容写入文件

package com.example.demo.common;

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

/**
 * 文件读写封装
 */
public class FileUtil {
    /**
     * 读取文件内容到String中
     * @param path
     * @return
     */
    public static String readFile(String path) {
        // StringBuilder相比String来说更高效,String它追加的底层是StringBuilder.append.toString
        StringBuilder result = new StringBuilder();

        // 字符流读取
        try (FileReader fileReader = new FileReader(path)) {
            while (true) {
                int ch = fileReader.read();
                if (ch == -1) {
                    break;
                }

                result.append((char) ch);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result.toString();
    }

    /**
     * 将内容写入对应文件
     * @param path
     * @param content
     */
    public static void writeFile(String path,String content) {
        try(FileWriter fileWriter = new FileWriter(path)) {
            fileWriter.write(content);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

五、封装用户提交代码的编译运行

通过上述封装之后,我们可以将用户提交的代码写入到指定的文件,此时我们就需对该代码进行编译运行,在此之间我们先对临时文件与代码的类名进行约定,我们把这些临时文件都放在一个文件夹里

// 所有临时文件的目录
private static final String WORD_DIR = "./tmp/";
// 所有代码的类名
private static final String CLASS = "Main";
// 用户上传代保存码文件
private static final String CODE = WORD_DIR + CLASS + ".java";
// 用户上传代码进程的编译时标准错误文件
private static final String COMPILE_ERROR = WORD_DIR + "compileError.txt";
// 用户上传代码进程的标准输出文件
private static final String STDOUT = WORD_DIR + "stdout.txt";
// 用户上传代码进程的运行时标准错误文件
private static final String STDERR = WORD_DIR + "stderr.txt";

此时我们需要两个对象,一个对象用户表示从前端获取的代码信息

package com.example.demo.model;

import lombok.Data;

/**
 * 表示:用户提交的代码
 */
@Data
public class Question {
    private String code;  // 代码
}

 一个表示用户提交代码执行的结果

package com.example.demo.model;

import lombok.Data;

/**
 *执行结果
 */
@Data
public class Answer {
    private int error;      // 0--ok  1--error 2--throw
    private String reason;
    private String stdout;  // 标准输入
    private String stderr;  // 标准错误
}

此时我们就可以对编译运行进行封装:

思路

首先我们需要将用户提交的代码写入一个java文件,然后我们需要创建一个进程进行编译,然后我们需要查看编译是否出错,也就是查看编译进程的标准错误文件,看是否为空,空则表示无错误,继续进行,如果不为空则表示存在错误,我们则读取该文件将错误信息进行封装后返回。在编译完成后,我们需要创建另一个进程进行运行编译的class文件,运行完成后,我们依旧需要读取运行进程的标准错误文件看是否为空,不空则说明可能存在运行时异常,空则读取标准输出文件将内容封装后返回。

了解了思路后,我们开始编写代码

package com.example.demo.common;

import com.example.demo.model.Answer;
import com.example.demo.model.Question;

import java.io.*;

/**
 * 每次 ”编译+运行“  的一个过程就是一个Task
 */
public class Task {

    /**
     * 这些临时文件 服务器进程获取子进程编译运行代码的结果,也就是进程之间通信
     */
    // 所有临时文件的目录
    private static final String WORD_DIR = "./tmp/";
    // 所有代码的类名
    private static final String CLASS = "Main";
    // 用户上传代保存码文件
    private static final String CODE = WORD_DIR + CLASS + ".java";
    // 用户上传代码进程的编译时标准错误文件
    private static final String COMPILE_ERROR = WORD_DIR + "compileError.txt";
    // 用户上传代码进程的标准输出文件
    private static final String STDOUT = WORD_DIR + "stdout.txt";
    // 用户上传代码进程的运行时标准错误文件
    private static final String STDERR = WORD_DIR + "stderr.txt";

    /**
     * 编译运行代码
     * @param question
     * @return
     */
    public Answer compileAndRun(Question question)  {
        Answer answer = new Answer();

        // 0.创建临时目录
        File workDir = new File(WORD_DIR);
        if (!workDir.exists()) {
            // 目录不存在,创建目录
            workDir.mkdirs();
        }

        // 1.将question里的code(用户提交的代码)写入java文件  :类名与文件名必须相同,此处规定为Main.java
        FileUtil.writeFile(CODE,question.getCode());

        // 2.创建子进程,调用javac命令编译   如果编译出错就会写入标准错误文件
        String compileCmd = String.format("javac -encoding utf8 %s -d %s",CODE,WORD_DIR);
        System.out.println("编译命令生成:" + compileCmd);
        ProcessUtil.run(compileCmd,null,COMPILE_ERROR); // 开始编译

            // 读取编译错误文件:如果为空则编译正确,如果有内容则编译有错误
        String compileError = FileUtil.readFile(COMPILE_ERROR);
        if (!compileError.equals("")) {
            // 编译错误,构造错误信息返回
            System.out.println("编译出错:" + compileError);
            answer.setError(1);
            answer.setReason(compileError);
            return answer;
        }

        // 3.创建子进程,调用java命令执行    会把标准输入与标准输出获取到
        String runCmd = String.format("java -classpath %s %s",WORD_DIR,CLASS);
        System.out.println("运行命令生成:" + runCmd);
        ProcessUtil.run(runCmd,STDOUT,STDERR);

            // 读取运行时标准错误文件, 正常情况用户不可能存在标准错误。如果该文件空则正常,如果不为空则存在异常
        String runError = FileUtil.readFile(STDERR);
        if (!runError.equals("")) {
            // 运行时出错,存在异常
            System.out.println("运行出错:" + runError);
            answer.setError(2);
            answer.setStderr(runError);
            return answer;
        }

        // 4.父进程获取编译结果,打包为Answer对象进行返回
        String runOut = FileUtil.readFile(STDOUT);
        answer.setError(0);
        answer.setStdout(runOut);

        return answer;
    }

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        Task task = new Task();
        Question question = new Question();
        question.setCode("hello main");
        task.compileAndRun(question);
    }
}

要注意的是一个java的类名必须与文件名相同所以我们在约定文件名时必须规定前端用户输入代码创建类时需提示用户类名。

项目gitee地址1886i (PG1886) - Gitee.comhttps://gitee.com/PG1886

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/497777.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【代码随想录】刷题Day17

1.AVLTree判断 110. 平衡二叉树 后序遍历的强化理解: 所谓后续遍历,不仅仅是一种遍历,其实它是完成了所有左右子树的递归。后续遍历能将自己所求的值返回给上层节点。这在比较中很关键,举个例子,我们能得到下边节点返…

Makefile教程(Makefile的结构)

文章目录 前言一、Makefile的结构二、深入案例三、Makefile中的一些技巧总结 前言 一、Makefile的结构 Makefile 通常由一系列规则组成,每条规则定义了如何从源文件生成目标文件。每个规则又由目标、依赖和命令三部分组成。 下面是 Makefile 规则的基本结构&…

Matlab官方的两个配色colormap补充包

目录 一、othercolor 1、使用方法 2、图示 二、slanCM 1、使用方法 2、图示 三、从matlab上下的函数如何使用 一、othercolor 下载地址:matlab_othercolor.zip 1、使用方法 不指定获取颜色个数会默认256色,举例获取[163]号彩虹色(rainbow)&…

Java阶段二Day15

Java阶段二Day15 文章目录 Java阶段二Day15复习前日知识点对象数据类型注入数组类型注入集合类型的注入p命名空间引入外部属性文件 基于XML管理beanbean的作用域bean的生命周期代码演示生命周期后置处理器处理展示基于XML的自动装配 基于注解管理bean开启组件扫描使用注解定义B…

【A200】 TX1核心 JetPack4.6.2版本如何修改DTB文件测试全部SPI

大家好,我是虎哥,很长时间没有发布新内容,主要是这段时间集中精力,研究DTB设备树的修改,以适配不同载板,同时也是专门做了一个TX1&TX2核心,双网口,可以使用SPI 扩展CAN接口的载板…

java获取resources路径的方法

我们在写程序的时候,有时候会发现代码不能正常运行,出现提示异常的问题,这就说明我们的代码没有执行完,也就是没有 resource,其实遇到这种情况,我们只需要把代码重新执行一遍即可。 在 java中是可以实现 re…

【计算机组成原理笔记】计算机的基本组成

计算机的基本组成 文章目录 计算机的基本组成冯诺伊曼计算机的特点硬件框图以运算器为核心的计算机现代计算机系统复杂性管理的方法 计算机的工作步骤存储器运算器控制器I/0 脚注 冯诺伊曼计算机的特点 五大部件组成 运算器存储器控制器输入设备输出设备 指令和地址以同等地位…

基于CUDA的GPU计算PI值

访问【WRITE-BUG数字空间】_[内附完整源码和文档] 基于CUDA的GPU计算PI值。本项目使用CUDA编程模型并行计算PI值,研究GPU与CPU效率的比较,分析不同GPU线程分块对性能的影响。 异构计算试验报告 —实验1:基于CUDA的GPU计算PI值 第一部分&…

原型模式--深拷贝和浅拷贝

定义 Specify the kind of objects to create using a prototypical instance, and create new objects by copying this prototype. (使用原型实例指定将要创建的对象类型,通过复制这个实例创建新的对象。) 从定义中我们我们可以发现&#x…

2023年4月Web3行业月度发展报告区块链篇 | 陀螺科技会员专享

4月,以太坊上海升级与香港Web3动向成最大热点,上海升级的完成是转POS的重要里程碑,从市场而言,由于升级解锁质押ETH是否引发抛压备受关注,仅以交易表现来看,并未出现大范围的抛压与离场。另一方面&#xff…

算力提升+AIGC,是驱动元宇宙发展的核心引擎|数据猿直播干货分享

‍数据智能产业创新服务媒体 ——聚焦数智 改变商业 “元宇宙”是美国科幻小说家尼奥斯蒂文森1992年在《雪崩》中提出的概念,书中设定现实世界中的人在网络世界中都有一个分身,这个由分身组成的世界就是“元宇宙”。如今,随着虚拟现实技术的…

60+开箱即用的工具函数库xijs更新指南(v1.2.5)

xijs 是一款开箱即用的 js 业务工具库, 聚集于解决业务中遇到的常用函数逻辑问题, 帮助开发者更高效的开展业务开发. 接下来就和大家一起分享一下v1.2.5 版本的更新内容以及后续的更新方向. 贡献者列表: 1. 数据深拷贝cloneDeep 该模块主要由 20savage 贡献, 支持 symbol, map,…

BM58-字符串的排列

题目 输入一个长度为 n 字符串&#xff0c;打印出该字符串中字符的所有排列&#xff0c;你可以以任意顺序返回这个字符串数组。 例如输入字符串ABC,则输出由字符A,B,C所能排列出来的所有字符串ABC,ACB,BAC,BCA,CBA和CAB。 数据范围&#xff1a;n < 10。 要求&#xff1a;空…

[架构之路-191]-《软考-系统分析师》-8-软件工程 - 解答什么是面向功能的结构化程序设计:算法+数据结构 = 程序

目录 1. 什么是结构化程序设计 2. 结构化程序设计的局限性 3.程序设计的三种基本结构 (1) 顺序结构 (2) 选择结构 (3) 循环结构 1. 什么是结构化程序设计 功能 》 Function 》 函数 》 算法 数据流Data Flow 》 数据结构Data Strucuture 程序 算法 数据结构 》 数…

36. Kubernetes 网络原理——CNI 网络插件

本章讲解知识点 Flannel 原理概述直接路由的原理和部署示例Calico 插件原理概述1. Flannel 原理概述 Flannel 是一个用于容器网络的开源解决方案,它使用了虚拟网络接口技术(如 VXLAN)和 etcd 存储来提供网络服务。它的原理概述如下: Flannel 协助 Kubernetes,给每一个 No…

界面交互篇:答题页的答题逻辑交互开发

微信小程序云开发实战-答题积分赛小程序 界面交互篇:答题页的答题逻辑交互开发 前面的那一篇文章,我们已经完成了使用云开发的聚合能力实现从题库中随机抽取题目功能。 在页面加载时,实现从题库中随机抽取题目功能。那么,拿到数据后要干什么?如何做? 动态数据绑定 实…

c++练习题

1、默认参数练习 创建默认参数函数 void stars(int cols ,int rows ) 该函数默认缺省值cols是10 rows是1。该函数完成功能是根据行和列数显示一个由星号组成的矩形。在main函数仲按照默认值调用该函数。按照cols是5调用该函数。按照列数和行数是7&#xff0c;3 调用该函数 #…

【MMdetection训练及使用脚本系列】MMdetection训练1——如何保存最优的checkpoint文件

MMdetection如何保存最优的checkpoint文件 以目标检测为例&#xff0c;进入到 configs/_base_/datasets/coco_detection.py将evaluation dict(interval1, metricbbox)改为evaluation dict(interval1, metricbbox, save_bestauto)即可。 但是不建议这样做&#xff0c;防止以…

软件设计师笔记--数据结构

文章目录 前言学习资料数据结构大 O 表示法时间复杂度线性结构和线性表线性表的顺序存储线性表的链式存储栈的顺序存储栈的链式存储队列的顺序存储与循环队列 串KMP 数组矩阵树二叉树二叉树的顺序存储结构二叉树的链式存储结构二叉树的遍历平衡二叉树二叉排序树最优二叉树(哈夫…

C/C++每日一练(20230507) 数列第n项值I/II、简化路径

目录 1. 求数列的第n项的值 ※ 2. 求数列的第n项的值 II ※ 3. 简化路径 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 1. 求数列的第n项的值 已知数列…