• 一、处理异常
  • 二、区分不同请求的工作目录
    • UUID
    • 对 Task 类进行重构
  • 三、校验代码的安全性
  • 四、阶段性总结

书接上回,我们自己测试没问题,是因为使用了正常数据;万一用户输入的是非法的请求,该咋办?

我们需要处理异常请求,修改整个代码框架。

一、处理异常

为了防止用户输入异常 ID,我们创建 ProblemNotFoundException 异常类来处理。

为了防止用户提交有问题的代码,我们创建 CodeInValidException 异常类来处理。

统一在 catch 处理异常代码。

整理整体代码结构,去除冗余代码,最后 CompileServlet 类代码如下:

@WebServlet("/compile")
public class CompileServlet extends HttpServlet {static class CompileRequest {public int id;public String code;}static class CompileResponse {// 0 表示没问题,1 表示编译出错,2 表示运行异常,3 表示其它错误public int error;public String reason;public String stdout;}private ObjectMapper objectMapper = new ObjectMapper();@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {CompileRequest compileRequest = new CompileRequest();CompileResponse compileResponse = new CompileResponse();try {resp.setStatus(200);resp.setContentType("application/json;charset=utf8");// 1. 读取请求的正文String body = readBody(req);// 类对象,获取类的信息compileRequest = objectMapper.readValue(body, CompileRequest.class);// 2. 根据 id 从数据库中查找到题目的详情 - 得到测试用例代码ProblemDAO problemDAO = new ProblemDAO();Problem problem = problemDAO.selectOne(compileRequest.id);// 处理用户输入异常 id,导致查不到题目if (problem == null) {// 为了统一处理错误,在这个地方抛出一个异常throw new ProblemNotFoundException();}// testCode 是测试用例的代码String testCode = problem.getTestCode();// requestCode 是用户提交的代码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) {// 1. 查找 requestCode 最后一个 }int pos = requestCode.lastIndexOf("}");if (pos == -1) {return null;}// 2. 截取字符串String substring = requestCode.substring(0, pos);// 3. 拼接字符串并返回return substring + testCode + "\n}";}// 通过请求头获取数据,转换成String 返回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");}
}

测试一波~

输入错误 id 能够捕捉异常。

二、区分不同请求的工作目录

问题引入

每次有一个请求过来,都需要生成一组临时文件。
如果同一时刻,有 N 个请求一起过来,这些临时文件和目录都是一样的。
此时多个请求之间就会出现 “相互干扰” 的情况(非常类似于线程安全问题)。

这三个请求,里面的题目和提交的代码都是一样的吗?都是不一样的!
因为这是来自三个不同用户的请求。
如果我们使用同一份目录里面的同一份文件,就会出现这种相互干扰的情况!

解决方法

我们需要让每个请求,都有一个自己的目录来存放这些临时文件,不会导致相互干扰。

因此,我们需要让每个请求创建的 WORK_DIR 目录都不相同!这时候就可以使用 “唯一 ID” 来作为目录的名字~

UUID

UUID 是计算机中非常常用的一个概念,表示一个 “全世界都唯一的 id”。每次生成的一个 UUID,会根据一系列算法,来保证这个 UUID 是唯一的。

每个请求,都生成一个唯一的 UUID,进一步创建一个以 UUID 命名的临时目录。最后把生成的临时文件都放在这个临时目录中即可。

对 Task 类进行重构

把开头的一组常量修改成变量。
然后创建一个构造方法,在里面生成 UUID 即可。

完整的 Task 类

// 编译运行
public class Task {// 通过一组常量来约定临时文件的名字// 表示所有临时文件所在的目录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 这个类,就能够生成一个 UUIDWORK_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";}// 此类的核心方法。// 参数:要编译运行的 Java 源代码;// 返回值:表示编译运行结果。public Answer compileAndRun(Question question) {Answer answer = new Answer();// 0. 准备好用来存放临时文件的目录File workDir = new File(WORK_DIR);// 判断是否存在该目录if (!workDir.exists()) {// 不存在则创建多级目录.workDir.mkdirs();}// 1. 把 question 中的 code 写入到一个 Solution.java 文件中FileUtil.writeFile(question.getCode(), CODE);// 2. 创建子进程,调用 javac 进行编译。编译的时候,需要有一个 .java 文件//      如果编译出错,javac 就会把错误信息写入到 stderr 里,使用专门的文件来保存:compileError.txtString 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.setError(1);answer.setReason(compileError);return answer;}// 3. 创建子进程,调用 java 命令执行//      运行程序的时候,也会把 java 子进程的标准输出和标准错误获取到. stdout.txt, stderr.txtString 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. 父进程获取到刚才的编译执行结果,并打包成 compile.Answer 对象//      正常编译运行的结果,就通过刚才约定的文件来进行获取answer.setError(0);answer.setStdout(FileUtil.readFile(STDOUT));return answer;}public static void main(String[] args) {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" +"}\n");// 编译运行后的结果Answer answer = task.compileAndRun(question);System.out.println(answer);}
}

单独编译运行 Task 类,我们可以从项目目录的 tmp 文件中,发现已经生成了 UUID 命名的文件。

启动 Tomcat,发现没有生成目录

是因为相对路径的原因。
IDEA 中直接运行 Task 类,这时候的工作目录就是当前 Java 项目所在的目录。
IDEA 通过 SmartTomcat 来运行 Servlet 程序,此时的工作目录就是由 SmartTomcat 控制的。不想由 SmartTomcat 控制,就可以写绝对路径。

所以,当我们使用相对路径指定文件的时候,发现文件找不到,主要是工作目录是啥我们不知道。

我们为代码添加一端监控,查看 SmartTomcat 的工作目录。

        // 查看 SmartTomcat 的工作目录System.out.println("用户工作目录:" + System.getProperty("user.dir"));

重新运行 Tomcat,通过 Postman 发送请求,控制台就会输出工作目录,最后能够在 tmp 文件中找到生成的 UUID 目录。

三、校验代码的安全性

当前代码还存在一个严重的安全性问题。

在线 OJ 系统需要执行一段用户提交的代码,用户提交的代码,可能是存在安全隐患的。

大家可以试试,这段代码在 leetcode 上执行看看什么结果。

有诸多问题需要防范,目前能注意的到有这些:

  1. Runtime 能够执行一个程序指令,这个比较危险。
  2. 代码中可能存在一些 “读写操作”,黑客可能直接把一个病毒程序写到你的机器上。
  3. 代码中如果存在一些 “网络” 操作,也是比较危险的。

解决方法

一个简单粗暴的方法,就是使用一个黑名单,把有危险的代码特性,都放在黑名单中。
在获取到用户提交代码的时候,就查询一个当前是否命中黑名单,如果命中黑名单就直接报错,不去编译执行。

// 黑名单
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;
}

四、阶段性总结

  1. 基于多进程编程的方式,创建了一个 CommandUtil 类,来封装创建进程完成任务的工作。
  2. 创建了 Task 类,把整个编译运行过程进行了封装。
  3. 创建了数据库和数据表,设计了题目的存储方式。
  4. 封装了数据库操作(Problem 和 ProblemDAO)。
  5. 设计了前后端交互的 API。
  6. 实现了这些前后端交互的 API。

到这里,我们 online-OJ 项目的服务器后台实现的差不多了。

我们继续实现前端部分,实现 online-OJ 项目的界面。

在线 OJ 项目(三) · 处理项目异常 · UUID · 校验代码的安全性 · 阶段性总结相关推荐

  1. JavaWeb 项目 --- 在线 OJ 平台 (三)

    文章目录 1. 设计网页页面 1.1 列表页 1.2 详情页 2. 设计网页的前后端交互接口 约定交互1: 获取题目的列表 约定交互2: 获取指定题目的详情信息 约定交互3: 向服务器提交编写的代码 ...

  2. 学习java第四天,自己做的尚硅谷项目三开发人员调度系统,代码很丑陋,等后面有时间再优化一下。

    1.定义公司成员作为父类 package tEAM;public class Person {private String ID;private String age;private String w ...

  3. 2021-11-29 vue移动端卖座电影项目(三) vue项目中使用Swiper插件,Film页面设置轮拨图,nowPlaying页面设置样式

    文章目录 1.引入swiper.vue组件 目的 步骤 结果 2.把swiper-slide做成匿名插槽 3.Film.vue中通过axios请求获取后台轮播图片 目的 步骤 因为现在原网站已取消轮拨 ...

  4. JavaWeb项目——基于Servlet实现的在线OJ平台 (项目问答+代码详解)

    文章目录 项目演示 预先知识 请问 在处理用户同时提交代码时是 多进程处理还是 多线程处理? 你是如何创建多进程的逻辑的 如何获取到编译与运行后的结果? 编译运行模块 子进程之间如何并发? 文件读写操 ...

  5. 毕设项目:基于BS模型的在线OJ系统

    系列文章目录 文章目录 系列文章目录 前言 一.在线OJ系统描述 二.在线编译模块 1.搭建一个HTTP服务器完成在线编译 2.收到HTTP请求,进行数据格式转化(HTTP中body的内容转换为JSO ...

  6. 在线OJ项目(3)------实现接口与网页前端进行交互

    我们先想一下:我们要具体进行设计那些网页呢?有几个页面?都是干啥的?如何设计前后端交互的接口? 当前我们已经把数据库的相关操作给封装好了,接下来我们可以进行设计一些API,也就是HTTP风格的接口,通 ...

  7. JavaWeb 项目 --- 在线 OJ 平台 (一)

    文章目录 1. 项目设计 2. 项目效果图 3. 创建项目 ① 创建一个 maven 项目 ② 创建 webapp/WEB-INF/web.xml ③ 写入 web.xml ④ 导入依赖 ⑤ 验证 创 ...

  8. 试着模仿LeetCode做一个在线OJ系统(超级阉割版)(附项目测试)

    文章目录 引言--痛苦的刷题 1.简单的需求 2.读写文件模块 (1)读文件readFile() 输入:文件路径 返回值:String (2)写文件writeFile() 输入:文件路径,文件 3.创 ...

  9. 在线 OJ 项目(四) · 前端设计与项目总结

    一.页面设计 题目列表页 题目详情页 二.获取到后台数据 实现思路 遇到换行问题 小结 引入 ace.js 三.项目总结 接下来将实现 online-oj 项目的前端界面. 先随便从各大网站上下载网页 ...

最新文章

  1. Numpy中的meshgrid()函数
  2. 2015/6/2站立会议(补发)
  3. phpstorm常用设置
  4. 汇编语言--串处理指令
  5. 第四周实践项目4 建立算法库——双链表
  6. Airtest自动化测试工具介绍
  7. 排错-tcpreplay回放错误:send() [218] Message too long (errno = 90)
  8. 第十六周项目3-有相同数字?
  9. 进阶攻略|前端最全的框架总结
  10. Python基础——zip、lambda、map
  11. 如何去掉 Visual Studio源代码 出现 对齐的点点
  12. 分治法-求最大最小元素
  13. 面对面的办公室——纪念艾伦•图灵百年诞辰
  14. 彭斌_无人机的发展与未来
  15. 独家|数据造假、爬虫与反爬虫战争暴露出哪些行业现状?
  16. 重新编译CDH版本hadoop报错:Non-resolvable parent POM: Could not transfer artifact com.
  17. linux-rm -f如何恢复
  18. linux7.4离线内核升级,CentOS 7.4升级Linux内核
  19. 带你读AI论文丨SP21 Survivalism: Living-Off-The-Land 经典离地攻击
  20. 考试/答题系统的设计思路

热门文章

  1. Java通过-jni调用c语言
  2. 医学图像处理——入门篇(二)
  3. HTC美国区总裁称iPhone没那么酷了
  4. 分享了一篇文章:《张烊:户外广告设计赏析-2》
  5. 分布式存储原理:TiDB
  6. 在WIN7系统中安装Ubuntu系统
  7. centos防火墙问题
  8. IDEA Git 使用,annotate显示代码编写者及时间
  9. 16QAM + OFDM + Rayleigh fading系统
  10. 使用ASP.NET的外观文件(skin)与css样式表