java 代码沙箱实现

java 代码沙箱的实现篇

在线判题系统的核心功能模块



前言

代码沙箱其实就是java的编译执行过程,我们只需要完成这些过程即可,需要注意的是,防止写入的代码带病毒,乱写入,死循环之类的问题,我们可以用黑白名单来解决,比如把File加入黑名单

代码沙箱实现逻辑

1、编译前的准备

1.传入的参数

public class ExecuteCodeRequest {

    private List<String> inputList;

    private String code;

    private String language;
}
  • 题目需要输入的参数集合
  • 代码内容
  • 使用编译语言

2.传出结果参数类

public class ExecuteCodeResponse {

    private List<String> outputList;

    /**
     * 接口信息
     */
    private String message;

    /**
     * 执行状态
     */
    private Integer status;

    /**
     * 判题信息
     */
    private JudgeInfo judgeInfo;
}
  • 我们应该都知道,每一个需要判题程序可以用很多不同的类名,所以我们固定类名,这样我们的java代码在的时候执行的时候就不会那么的麻烦,只需要写好一个字符串为javac (文件路径),就能够生成一个Main的class类,然后执行即可。
  private static final String GLOBAL_JAVA_CLASS_NAME = "Main.java";

2、存放代码及编译代码

将用户代码保存为文件

   /**
     * 1. 把用户的代码保存为文件
     * @param code 用户代码
     * @return
     */
    public File saveCodeToFile(String code) {
        String userDir = System.getProperty("user.dir");
        String globalCodePathName = userDir + File.separator + GLOBAL_CODE_DIR_NAME;
        // 判断全局代码目录是否存在,没有则新建
        if (!FileUtil.exist(globalCodePathName)) {
            FileUtil.mkdir(globalCodePathName);
        }

        // 把用户的代码隔离存放
        String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID();
        String userCodePath = userCodeParentPath + File.separator + GLOBAL_JAVA_CLASS_NAME;
        File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);
        return userCodeFile;
    }

我们固定一个位置,来存放我们的代码,后续我们执行完成后将其删除

Process类

Process 类通常用于表示和管理操作系统中的进程。在多任务操作系统中,进程是执行中的程序实例,它包含程序计数器、寄存器、堆栈和程序执行的内存区域等。不同的编程语言或框架可能有不同的 Process 类实现,但通常它们都提供了一些共同的功能。
- 方法

  • Start()
    描述:启动进程。
    参数:可能包括命令行参数、工作目录等。
    返回值:通常无返回值,但可能抛出异常。

  • Stop() 或 Kill()
    描述:停止或终止进程。
    参数:可能包括终止信号等。
    返回值:通常无返回值,但可能抛出异常。

  • WaitForExit()
    描述:等待进程退出。
    参数:可能包括超时时间。
    返回值:通常返回进程的退出代码。

  • Read() 或 GetOutput()
    描述:读取进程的输出。
    参数:可能包括读取的缓冲区大小等。
    返回值:进程的输出内容。

  • Write() 或 Input()
    描述:向进程发送输入。
    参数:要发送的输入内容。
    返回值:通常无返回值,但可能抛出异常。

编译代码

 /**
     * 2、编译代码
     * @param userCodeFile
     * @return
     */
    public ExecuteMessage compileFile(File userCodeFile) {
        String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());
        try {
            Process compileProcess = Runtime.getRuntime().exec(compileCmd);
            ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
            log.info("{},{}",executeMessage.getExitValue(),executeMessage.getErrorMessage());
            if (executeMessage.getExitValue() != 0) {
                executeMessage.setErrorMessage("编译错误");
            }
            return executeMessage;
        } catch (Exception e) {
//            return getErrorResponse(e);
            throw new RuntimeException(e);
        }
    }
  • 固定好我们的执行代码,确定编码格式,防止乱码
  • 使用Process类,执行代码
  • 构造Process的工具类,来进行编译
public static ExecuteMessage runProcessAndGetMessage(Process runProcess, String opName) {
        ExecuteMessage executeMessage = new ExecuteMessage();

        try {
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();//开始计时间
            // 等待程序执行,获取错误码
            int exitValue = runProcess.waitFor();
            executeMessage.setExitValue(exitValue);
            // 正常退出
            if (exitValue == 0) {
                System.out.println(opName + "成功");
                // 分批获取进程的正常输出
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(runProcess.getInputStream()));
                List<String> outputStrList = new ArrayList<>();
                // 逐行读取
                String compileOutputLine;
                while ((compileOutputLine = bufferedReader.readLine()) != null) {
                    outputStrList.add(compileOutputLine);
                }
                executeMessage.setMessage(StringUtils.join(outputStrList, "\n"));
            } else {
                // 异常退出
                System.out.println(opName + "失败,错误码: " + exitValue);
                // 分批获取进程的正常输出
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(runProcess.getInputStream()));
                List<String> outputStrList = new ArrayList<>();
                // 逐行读取
                String compileOutputLine;
                while ((compileOutputLine = bufferedReader.readLine()) != null) {
                    outputStrList.add(compileOutputLine);
                }
                executeMessage.setMessage(StringUtils.join(outputStrList, "\n"));

                // 分批获取进程的错误输出
                BufferedReader errorBufferedReader = new BufferedReader(new InputStreamReader(runProcess.getErrorStream()));
                // 逐行读取
                List<String> errorOutputStrList = new ArrayList<>();
                // 逐行读取
                String errorCompileOutputLine;
                while ((errorCompileOutputLine = errorBufferedReader.readLine()) != null) {
                    errorOutputStrList.add(errorCompileOutputLine);
                }
                executeMessage.setErrorMessage(StringUtils.join(errorOutputStrList, "\n"));
            }
            stopWatch.stop();//停止计时
            executeMessage.setTime(stopWatch.getLastTaskTimeMillis());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return executeMessage;
    }

在这个工具类中,我们主要做的几件事,计算时间,判断编译的退出码,然后进行比对是否正确,如果不正确,逐行读取错误信息然后设置,然后返回

3、执行代码,得到输出结果

  • 执行.class代码
    public List<ExecuteMessage> runFile(File userCodeFile, List<String> inputList) {
        String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();

        List<ExecuteMessage> executeMessageList = new ArrayList<>();
        for (String inputArgs : inputList) {
//            String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeParentPath, inputArgs);
            String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeParentPath, inputArgs);
            try {
                Process runProcess = Runtime.getRuntime().exec(runCmd);//开始运行命令
                // 超时控制
                new Thread(() -> {//防止死锁
                    try {
                        Thread.sleep(TIME_OUT);
                        System.out.println("超时了,中断");
                        runProcess.destroy();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }).start();
                ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(runProcess, "运行");
                System.out.println(executeMessage);
                executeMessageList.add(executeMessage);
            } catch (Exception e) {
                ExecuteMessage executeMessage=new ExecuteMessage();
                executeMessage.setMessage(e.getMessage());
                executeMessageList.add(executeMessage);
                return executeMessageList;
            }
        }
        return executeMessageList;
    }

这里运行了一些我们java虚拟机的只是,我们在执行的代码中加入了内存的限制-Xmx256m,限制为256M,如果内存大于该限制我们就会返回该错误信息,并且为了防止用户写一个长时间无法执行完成的程序,我们新建一个线程,让其睡眠一段时间,醒来后直接结束执行的线程(防止死锁)。

  • 获取输出结果
    public ExecuteCodeResponse getOutputResponse(List<ExecuteMessage> executeMessageList) {
        ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
        List<String> outputList = new ArrayList<>();
        // 取用时最大值,便于判断是否超时
        long maxTime = 0;
        for (ExecuteMessage executeMessage : executeMessageList) {
            String errorMessage = executeMessage.getErrorMessage();
            if (StrUtil.isNotBlank(errorMessage)) {
                executeCodeResponse.setMessage(errorMessage);
                // 用户提交的代码执行中存在错误
                executeCodeResponse.setStatus(3);
                break;
            }
            outputList.add(executeMessage.getMessage());
            Long time = executeMessage.getTime();
            if (time != null) {
                maxTime = Math.max(maxTime, time);
            }
        }
        // 正常运行完成
        if (outputList.size() == executeMessageList.size()) {
            executeCodeResponse.setStatus(1);
        }
        executeCodeResponse.setOutputList(outputList);
        JudgeInfo judgeInfo = new JudgeInfo();
        judgeInfo.setTime(maxTime);
        executeCodeResponse.setJudgeInfo(judgeInfo);
        return executeCodeResponse;
    }

当我们执行代码的时候,输出会在控制台中,这个时候我们读取控制台中的信息,然后拿到一个一个的输出结果,并且我们可以通过executeMessage.getErrorMessage得到是否超时的信息。

  • 24
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
华为java培训讲义 第一天 配置java环境变量: JAVA_HOME:配置JDK的目录 CLASSPATH:指定到哪里去找运行时需要用到的类代码(字节码) PATH:指定可执行程序的位置 LINUX系统(在" .bash_profile "下的环境变量设置) JAVA_HOME=/opt/jdk1.5.0_06 CLASSPATH=.:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar PATH=$PATH:$JAVA_HOME/bin:. export JAVA_HOME CLASSPATH PATH (将指定的环境变量声明为全局的) windows系统: 右击我的电脑-->属性-->高级-->环境变量 Java的运行过程: 编译:生成可执行文件,如C++中利用g++生成a.out,效率高,但不跨平台 解释:解释器把源文件逐行解释,跨平台但效率不高 在java中:先编译后解释,把.java文件编译成.class字节码文件 Java代码文件(.java文件)---> Java编译器(javac)---> Java字节码文件(.class文件,平台无关的)---> Java解释器(java),执行Java字节码 Java的垃圾回收: 由一个后台线程gc进行垃圾回收 虚拟机判定内存不够的时候会中断代码的运行,这时候gc才进行垃圾回收 缺点:不能够精确的去回收内存 java.lang.System.gc(); 建议回收内存,但系统不一定回应,他会先去看内存是否够用,够用则不予理睬,不够用才会去进行垃圾回收 内存中什么算是垃圾: 不再被引用的对象(局部变量,没有指针指向的) java的安全性: 沙箱机制:只能做沙箱允许的操作 通过下面环节,实现安全 加载有用的类文件,不需要的不加载 校验字节码,查看允许的操作 查看代码和虚拟机的特性是否相符 查看代码是否有破坏性 查看是否有违规操作,如越界 查看类型是否匹配,类型转换是否能正确执行 源程序: package mypack; //相当于一个目录 public class HelloWorld{ public static void main(String[] args){ System.out.println(“Hello World”); } } 注: 1、文件名必须和public修饰的类名一致,以.java作为文件后缀,如果定义的类不是public的,则文件名与类名可以不同。 2、一个.java文件中可以有多个class,但是只有一个public修饰的类。 3、java代码文件编译后,一个类对应生成一个.class文件 4、一个java应用程序应该包含一个main()方法,而且其签名是固定的,它是应用程序的入口方法,可以定义在任意一个类中,不一定是public修饰的类

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值