­

基於SpringBoot的代碼在線運行的簡單實現

  • 2020 年 3 月 14 日
  • 筆記

說明

由於沒有實現沙盒,所以這個運行只適合提交自己寫的代碼到服務器,不適合像 菜鳥工具 那樣可以讓人公開提交代碼並訪問。

基本思路

前端提交代碼,後端運行並返回結果。

後端實現

為了方便實現後端採用到了SpringBoot

我們需要先完成代碼運行所需要的配置

@ConfigurationProperties(prefix = "run.script")  @Component  public class Config {      private String cpp;      private String c;      private String python;        public void setCpp(String cpp) {          this.cpp = cpp;      }        public void setC(String c) {          this.c = c;      }        public void setPython(String python) {          this.python = python;      }        public String getCpp() {          return cpp;      }        public String getC() {          return c;      }        public String getPython() {          return python;      }    }

配置yml文件

此處的cpp和c應為需要編譯運行,所以需要根據不同的操作系統寫運行腳本

所有的路徑都必須是絕對路徑

run:    script:      cpp: F:SpringrunCodesrcmainresourcesrunCpp.bat      c: F:SpringrunCodesrcmainresourcesrunC.bat      python: C:UserspuzhiweiAppDataLocalProgramsPythonPython38python.exe

然後我們需要將前端提交的代碼保存到文件

        // 獲取系統緩存文件的位置          String tmpDir = System.getProperty("java.io.tmpdir");          // 隨機文件夾的名字          File pwd = Paths.get(tmpDir, String.format("%016x", nextLong.incrementAndGet())).toFile();          // 新建文件夾          pwd.mkdirs();          ProcessBuilder pb = null;          switch (type) {              case "C":                  try (Writer writer = new BufferedWriter(new FileWriter(new File(pwd, "Main.c"), Charset.defaultCharset()))) {                      writer.write(code);                  }                  pb = new ProcessBuilder().command(config.getC()).directory(pwd);                  break;              case "CPP":                  try (Writer writer = new BufferedWriter(new FileWriter(new File(pwd, "Main.cpp"), Charset.defaultCharset()))) {                      writer.write(code);                  }                  pb = new ProcessBuilder().command(config.getCpp()).directory(pwd);                  break;              case "JAVA":                  try (Writer writer = new BufferedWriter(new FileWriter(new File(pwd, "Main.java"), Charset.defaultCharset()))) {                      writer.write(code);                  }                  String[] command = new String[]{getJavaExecutePath(), "-Dfile.encoding=" + Charset.defaultCharset(), "--source", "11", "--enable-preview", "Main.java"};                  pb = new ProcessBuilder().command(command).directory(pwd);                  break;              case "PYTHON":                  try (Writer writer = new BufferedWriter(new FileWriter(new File(pwd, "Main.py"), Charset.defaultCharset()))) {                      writer.write(code);                  }                  pb = new ProcessBuilder().command(config.getPython(), "Main.py").directory(pwd);                  break;              default:                  break;          }

這段代碼主要實現了將代碼保存到系統的緩存文件夾中,

pb為要在終端中執行的編譯運行命令

由於C和C++需要編譯才能執行,所以執行的是運行腳本,需要根據自己的系統進行修改

在windows下如下

@echo off  clang -std=c11 main.c && a.exe
@echo off  clang++ -std=c++17 main.cpp && a.exe

獲取Java執行路徑的的代碼如下

    private String getJavaExecutePath() {          if (javaExec == null) {              String javaHome = System.getProperty("java.home");              String os = System.getProperty("os.name");              boolean isWindows = os.toLowerCase().startsWith("windows");              Path javaPath = Paths.get(javaHome, "bin", isWindows ? "java.exe" : "java");              javaExec = javaPath.toString();          }          return javaExec;      }

之後就是使用 ProcessBuilder 執行腳本,並讀取運行結果了

pb.redirectErrorStream(true);          Process p = pb.start();          if (p.waitFor(5, TimeUnit.SECONDS)) {              String result = null;              try (InputStream input = p.getInputStream()) {                  result = readAsString(input, Charset.defaultCharset());              }              return new ProcessResult(p.exitValue(), result);          } else {              System.err.println(String.format("Error: process %s timeout. destroy forcibly.", p.pid()));              p.destroyForcibly();              return new ProcessResult(p.exitValue(), "運行超時");          }

最後,這個類的完整代碼如下

import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.stereotype.Component;    import java.io.*;  import java.nio.charset.Charset;  import java.nio.file.Path;  import java.nio.file.Paths;  import java.util.concurrent.TimeUnit;  import java.util.concurrent.atomic.AtomicLong;    /**   * @author Pu Zhiwei {@literal puzhiweipuzhiwei@foxmail.com}   * create          2020-03-13 18:22   */  @Component  public class RunCode {      private final Config config;        private static String javaExec = null;          private static AtomicLong nextLong = new AtomicLong(System.currentTimeMillis());        @Autowired      public RunCode(Config config) {          this.config = config;      }          public ProcessResult runCode(String type, String code) throws IOException, InterruptedException {          // 獲取系統緩存文件的位置          String tmpDir = System.getProperty("java.io.tmpdir");          // 隨機文件夾的名字          File pwd = Paths.get(tmpDir, String.format("%016x", nextLong.incrementAndGet())).toFile();          // 新建文件夾          pwd.mkdirs();          ProcessBuilder pb = null;          switch (type) {              case "C":                  try (Writer writer = new BufferedWriter(new FileWriter(new File(pwd, "Main.c"), Charset.defaultCharset()))) {                      writer.write(code);                  }                  pb = new ProcessBuilder().command(config.getC()).directory(pwd);                  break;              case "CPP":                  try (Writer writer = new BufferedWriter(new FileWriter(new File(pwd, "Main.cpp"), Charset.defaultCharset()))) {                      writer.write(code);                  }                  pb = new ProcessBuilder().command(config.getCpp()).directory(pwd);                  break;              case "JAVA":                  try (Writer writer = new BufferedWriter(new FileWriter(new File(pwd, "Main.java"), Charset.defaultCharset()))) {                      writer.write(code);                  }                  String[] command = new String[]{getJavaExecutePath(), "-Dfile.encoding=" + Charset.defaultCharset(), "--source", "11", "--enable-preview", "Main.java"};                  pb = new ProcessBuilder().command(command).directory(pwd);                  break;              case "PYTHON":                  try (Writer writer = new BufferedWriter(new FileWriter(new File(pwd, "Main.py"), Charset.defaultCharset()))) {                      writer.write(code);                  }                  pb = new ProcessBuilder().command(config.getPython(), "Main.py").directory(pwd);                  break;              default:                  break;          }              pb.redirectErrorStream(true);          Process p = pb.start();          if (p.waitFor(5, TimeUnit.SECONDS)) {              String result = null;              try (InputStream input = p.getInputStream()) {                  result = readAsString(input, Charset.defaultCharset());              }              return new ProcessResult(p.exitValue(), result);          } else {              System.err.println(String.format("Error: process %s timeout. destroy forcibly.", p.pid()));              p.destroyForcibly();              return new ProcessResult(p.exitValue(), "運行超時");          }      }            private String getJavaExecutePath() {          if (javaExec == null) {              String javaHome = System.getProperty("java.home");              String os = System.getProperty("os.name");              boolean isWindows = os.toLowerCase().startsWith("windows");              Path javaPath = Paths.get(javaHome, "bin", isWindows ? "java.exe" : "java");              javaExec = javaPath.toString();          }          return javaExec;      }        public String readAsString(InputStream input, Charset charset) throws IOException {          ByteArrayOutputStream output = new ByteArrayOutputStream();          byte[] buffer = new byte[102400];          for (; ; ) {              int n = input.read(buffer);              if (n == (-1)) {                  break;              }              output.write(buffer, 0, n);          }          return output.toString(charset);      }  }

完整代碼

 

寫完這些,我們就基本完成了代碼在後端的運行並返回結果

接下來可以寫一個測試方法測試一下結果的運行

import org.junit.jupiter.api.Test;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.boot.test.context.SpringBootTest;    @SpringBootTest  class RunApplicationTests {        @Autowired      private RunCode runCode;        @Test      void contextLoads() throws Exception {          String code = "#include <stdio.h>n" +                  "n" +                  "int main()n" +                  "{n" +                  "   printf("Hello, World! \n");n" +                  "   n" +                  "   return 0;n" +                  "}";          System.out.println(runCode.runCode("C", code).getOutput());      }    }

測試運行

如果沒有異常,應該可以看到如下內容

最後,寫一個controller,用來接收前端提交的代碼

@RestController  @CrossOrigin("*")  public class WebController {      public final RunCode runCode;        @Autowired      public WebController(RunCode runCode) {          this.runCode = runCode;      }        @PostMapping("/run")      public ProcessResult runCode(@RequestBody CodeModel codeModel) throws Exception {          return runCode.runCode(codeModel.getType(), codeModel.getCode());      }  }
public class CodeModel {      private String type;      private String code;        public String getType() {          return type;      }        public void setType(String type) {          this.type = type;      }        public String getCode() {          return code;      }        public void setCode(String code) {          this.code = code;      }  }

CodeModel

/**   * @author Pu Zhiwei {@literal puzhiweipuzhiwei@foxmail.com}   * create          2020-03-13 18:26   */  public class ProcessResult {      private int exitCode;        private String output;        public ProcessResult(int exitCode, String output) {          this.exitCode = exitCode;          this.output = output;      }        public int getExitCode() {          return exitCode;      }        public String getOutput() {          return output;      }  }

ProcessResult

 

至此,我們的後端就基本完成了。

前端

我們先寫一個簡單的html頁面來進行測試

<!DOCTYPE html>  <html lang="en">  <head>      <meta charset="UTF-8">      <title>Title</title>  </head>  <body>  <select>      <option selected>Java</option>      <option>C</option>  </select>  <br/>  <textarea id="code" style="height: 500px; width: 600px"></textarea>  <button id="sub-btn" onclick="submit()">提交</button>  <br/>  <textarea id="output"></textarea>    <script>      function submit() {          let data = document.querySelector("#code").value;            fetch("http://127.0.0.1:8848/run", {              method: "POST",              headers: {                  "Content-Type": "application/json; charset=UTF-8"              },              body: JSON.stringify({                  code: data,                  type: "JAVA"              })            }).then(response => response.json())              .then(json => {                  console.log(json)                  document.querySelector("#output").value = json.output;              });      }  </script>  </body>  </html>

如果沒有問題,我們就能看到如下結果了

最後,完善一下頁面

<!DOCTYPE html>  <html lang="en">  <head>      <meta charset="UTF-8">      <title>代碼在線運行工具</title>      <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">      <style>          #editor {              position: absolute;              width: 100%;              height: 100%;          }      </style>  </head>  <body>  <nav class="navbar navbar-expand-lg navbar-light bg-light">      <div class="container">          <a class="navbar-brand" href="/">代碼在線運行工具</a>          <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">              <span class="navbar-toggler-icon"></span>          </button>      </div>  </nav>  <div style="height: 30px"></div>  <div class="container shadow p-3 mb-5 bg-white rounded">      <div class="container-fluid">          <div class="row">              <div class="col-2">                  <button id="sub-btn" class="btn btn-success " onclick="submit()">點擊運行!</button>              </div>              <div class="col-3">                  <select onchange="selectLanguage(this)" id="language-type" class="form-control">                      <option selected>Java</option>                      <option>C</option>                      <option>CPP</option>                      <option>Python</option>                  </select>              </div>              <div class="col-3">                  <button type="button" class="btn btn-secondary" onclick="clean()">清空</button>              </div>          </div>      </div>      <div style="height: 20px"></div>        <div class="row">          <div class="col-7 border border-light">              <div id="editor"></div>          </div>          <div class="col-1 border-left"></div>          <div class="col text-center">              <textarea id="output" class="form-control" rows="15"></textarea>          </div>      </div>  </div>  <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.8/ace.js" type="text/javascript"></script>  <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.8/ext-language_tools.min.js" type="text/javascript"></script>  <!--<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.8/mode-java.min.js" type="text/javascript"></script>-->  <script>      ace.require("ace/ext/language_tools");      const editor = ace.edit("editor");      editor.session.setMode("ace/mode/java");      editor.setTheme("ace/theme/github");      // enable autocompletion and snippets      editor.setOptions({          enableBasicAutocompletion: true,          enableSnippets: true,          enableLiveAutocompletion: true      });        function submit() {          document.querySelector("#output").value = "代碼運行中!";          let data = editor.getValue();              fetch("http://127.0.0.1:8848/run", {              method: "POST",              headers: {                  "Content-Type": "application/json; charset=UTF-8"              },              body: JSON.stringify({                  code: data,                  type: document.querySelector("#language-type").value.toUpperCase()              })            }).then(response => response.json())              .then(json => {                  console.log(json)                  document.querySelector("#output").value = json.output;              });      }        function clean() {          editor.setValue("");      }        function selectLanguage(e) {          let mode = "ace/mode/" + e.value.toLowerCase();          if (e.value.toLowerCase() === "c" || e.value.toLowerCase() === "cpp") {              mode = "ace/mode/c_cpp"          }          editor.session.setMode(mode);      }  </script>  </body>  </html>

效果如下