在线体验地址: https://zhouyi.ink/#/runjava
实现思路
- web前端提供一个代码输入框,一个结果显示框;
- 前端携带用户输入的代码请求后端java程序执行运行代码的命令,并将执行结果返回给前端
- 前端展示后端返回的代码执行结果
前端实现
前端采用的vue3 + Element plus +typescript + vite实现的,主要代码如下
<template>
<div>
<el-container>
<!-- 头部区域 -->
<el-header class="header">在线运行Java代码(基于Oracle JDK1.8)</el-header>
<!-- 下方主体区域 -->
<el-main class="main">
<!-- 下方左边区域 -->
<div class="main-left">
<el-header class="left-header">
<el-button type="info" @click="examples(1)">九九乘法表</el-button>
<el-button type="info" @click="examples(2)">字符串反转</el-button>
</el-header>
<el-main>
<el-input type="textarea" rows="30" resize="none" v-model="inputCode" />
</el-main>
</div>
<!-- 下方右边区域 -->
<div class="main-right">
<el-header class="right-header">
<el-button type="success" @click="runCode">运行</el-button>
</el-header>
<el-main>
<el-input type="textarea" :readonly="true" resize="none" rows="30" v-model="codeResult" />
</el-main>
</div>
</el-main>
</el-container>
</div>
</template>
<script lang="ts" setup>
import axios from 'axios'
import { ref } from 'vue'
const inputCode = ref('public class Main {\n\t public static void main(String[] args) {\n\t\t System.out.println("Hello, World");\n\t } \n }')
const codeResult = ref('运行结果')
const examples = (implType) => {
if (implType == 1) {
inputCode.value = "public class Main {\n\t public static void main(String[] args) {\n // 外层循环 也就是乘法的第一个数\n\t\tfor (int j = 0; j <= 9; j++) {\n // 内层循环,乘法的第二个数\n\t\t\tfor (int i = 1; i <= j; i++) {\n\t\t\t\tSystem.out.print(j + \"*\" + i + \"=\" + (j * i) + \"\\t\");\n\t\t\t}\n\t\t\tSystem.out.println();\n\t\t}\n\t } \n }"
} else if (implType == 2) {
inputCode.value = "public class Main {\n\t public static void main(String[] args) {\n String string=\"zhouyi\";\n String reverse = new StringBuffer(string).reverse().toString();\n System.out.println(\"字符串反转前:\"+string);\n System.out.println(\"字符串反转后:\"+reverse);\n\t } \n }"
}
}
const runCode = () => {
axios({
method: "POST",
url: "https://www.angyum.com:10040/runJava",
headers: {'Content-Type':'application/json'},
data: {
"javaCode": inputCode.value
}
}).then(response => {
const responseData = response.data
var errCode = responseData.error
codeResult.value = errCode==0 ? responseData.stdout : responseData.stderr
})
}
</script>
<style lang="scss" scoped>
.header {
text-align: center;
border: 1px;
background-color: #F7F7F7;
/* line-height 和 height相等即可实现文字上下居中 */
line-height: 60px;
height: 60px;
}
.main {
margin-top: 2px;
height: 800px;
width: 100%;
}
.main-left {
width: 50%;
float: left;
height: 100%;
border: 1px;
}
.main-right {
width: 50%;
float: right;
height: 100%;
border: 1px;
}
.left-header {
margin-left: 1px;
}
.right-header {
margin-left: 1px;
}
</style>
后端实现
实时运行java代码原理是调用Runtime.getRuntime()的exec(cmd)执行命令
- 将用户录入的java代码保存为临时的java文件
- 执行javac命令将临时java文件编译成class文件
- 执行java命令调用class文件
执行命令的工具类
package com.angyum.zhouyi_online.util;
import com.angyum.zhouyi_online.entity.Answer;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* @author zhangmy
* @date 2023/2/15 17:10
* @description 编译工具
*/
public class CompileUtil {
//1.通过Runtime类得到实例,执行exec方法
//2.希望获取到标准输出,并写入到指定的文件中
//3.获取到标准的错误,并写入到指定文件中
//4.等待子进程结束,拿到状态码,并返回。
//防止不小心实例化工具类
private CompileUtil() {
}
public static Answer run(String cmd, Boolean stdoutB, Boolean stderrB) {
//System.out.println(cmd);
Answer answer = new Answer();
try {
//1.通过Runtime类得到实例,执行exec方法
Process process = Runtime.getRuntime().exec(cmd);
//2.获取到标准输出,并写入到指定文件中
String line = "", re = "", stdout = "";
// 判断是windows还是linux
String os = System.getProperty("os.name");
String charsetName = "UTF-8";
if(os.toLowerCase().startsWith("windows")){
charsetName = "GBK";
}else if(os.toLowerCase().startsWith("linux")){
charsetName = "UTF-8";
}
if (stdoutB) {
//windows上指定GBK中文才能正常显示,Linux还没试明先不管
//也会自动阻塞
BufferedReader stdoutFrom = new BufferedReader(new InputStreamReader(process.getInputStream(), charsetName));
while ((line = stdoutFrom.readLine()) != null)
stdout += line + "\n";
stdoutFrom.close();
;
}
if (stderrB) {
BufferedReader stdoutFrom = new BufferedReader(new InputStreamReader(process.getErrorStream(), charsetName));
while ((line = stdoutFrom.readLine()) != null) {
re += line + "\n";
}
stdoutFrom.close();
}
//exec 执行过程是异步的,waitfor会阻塞主进程,直到子进程完毕
int exitCode = process.waitFor();
answer.setError(exitCode);
answer.setStderr(re);
answer.setStdout(stdout);
return answer;
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
answer.setError(2);
return answer;
}
}
业务逻辑实现
package com.angyum.zhouyi_online.service.impl;
import com.angyum.zhouyi_online.entity.Answer;
import com.angyum.zhouyi_online.service.RunJavaService;
import com.angyum.zhouyi_online.util.CompileUtil;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Map;
/**
* @author zhangmy
* @date 2023/2/15 17:53
* @description
*/
@Service
public class RunJavaServiceImpl implements RunJavaService {
@Override
public synchronized Answer runJavaCode(Map<String, Object> map) throws IOException {
String DIR = "~/javaTest/";
File dirFile = new File(DIR);
if (!dirFile.exists()) {
dirFile.mkdirs();
}
String javaFile = DIR + "Main.java";
String javaClass = "Main";
String compileCmd = String.format("javac -encoding utf8 %s -d %s", javaFile, DIR);
String runningCmd = String.format("java -classpath %s %s", DIR, javaClass);
//将代码写入到定义路径下特定的java源文件中
FileWriter writer = new FileWriter(javaFile);
Object javaCodeObj = map.get("javaCode");
writer.write(javaCodeObj.toString());
writer.close();
//编译源文件为class文件
Answer answer = CompileUtil.run(compileCmd, false, true);
System.out.println(answer.getStderr());
//若编译成功即可开始运行
if (answer.getError() == 0) {
answer = CompileUtil.run(runningCmd, true, true);
if (answer.getError() != 0)
answer.setReason(Answer.Success);
else
answer.setReason(Answer.RuntimeError);
System.out.println(answer.getStdout());
} else {
answer.setReason(Answer.Error);
}
File sourceFile = new File(javaFile);
sourceFile.deleteOnExit();
File classFile = new File(DIR + "Main.class");
classFile.deleteOnExit();
return answer;
}
}
执行结果响应类
package com.angyum.zhouyi_online.entity;
/**
* @author zhangmy
* @date 2023/2/15 17:11
* @description
*/
public class Answer {
//错误码如果error为0表示运行ok,1为编译错误,2为命令运行出错…………很多很多状态
private int error;
//出错的提示信息-运行成功-编译错误-运行时错误。
private String reason;
public static String Success="运行成功";
public static String Error="编译错误";
public static String RuntimeError="运行时错误";
//运行程序得到的标准输出
private String stdout;
//运行程序得到的标准错误
private String stderr;
public int getError() {
return error;
}
public void setError(int error) {
this.error = error;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
public String getStdout() {
return stdout;
}
public void setStdout(String stdout) {
this.stdout = stdout;
}
public String getStderr() {
return stderr;
}
public void setStderr(String stderr) {
this.stderr = stderr;
}
}
一些问题
记录开发过程中遇到的一些问题
- 首先要明确实现的思路以及原理
就像文中提到的调用Runtime.getRuntime()的exec(cmd)执行命令。一开始我想着直接在js前端直接保存文件,然后再调用系统命令编译文件和执行结果。多番实验之后中间有许多解决不了的问题,所以我选择了一种简单的方式实现
- 跨域问题的解决
因为后端是springboot项目,所以跨域只需要简单配置下即可
package com.angyum.zhouyi_online.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
/**
* @author zhangmy
* @date 2023/2/16 9:45
* @description
*/
@Configuration
@EnableWebMvc
public class ConfigurerAdapter implements WebMvcConfigurer {
// 导入Cors的过滤器,在配置文件中进行配置
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// 允许访问的客户端域名
List<String> allowedOriginPatterns = new ArrayList<>();
allowedOriginPatterns.add("*");
config.setAllowedOriginPatterns(allowedOriginPatterns);
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
- 包含一些前端的知识
因为前端我不是太懂,都是慢慢摸索来的,页面的布局,布局的实现,axios发送请求,样式调整等等,这块花费了大头的时间
- 阿里云服务器申请和部署ssl证书
因为前端代码是在另一个服务器上的,域名是加了https证书的,所以在单独请求运行java代码的服务器上也得加上SSL证书,刚好我这是阿里云的服务器,陪着nginx即可添加https安全证书
①:登录阿里云控制,访问SLL证书免费证书页
②:先购买,购买之后创建证书,因为我已经申请了,显示证书已有,阿里云每个账号每年可以申请20个免费的证书
③:创建之后即可通过nginx部署到服务器了
下载好之后根据一下的nginx配置即可
server {
listen 10040 ssl;
server_name www.angyum.com;
ssl_certificate /etc/nginx/cert/9305602_www.angyum.com.pem;
ssl_certificate_key /etc/nginx/cert/9305602_www.angyum.com.key;
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
location / {
# 代理的后端真实访问IP+端口
proxy_pass http://127.0.0.1:10041;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
这里的10040是前端访问后端的入口,10041是后端真实的的端口,写好配置之后重启nginx就可以了
到这里一个简单的在线运行java代码的工具就实现了,还有很多优化的地方,比如界面布局,添加可录入参数,如何防止恶意请求等等,这些都等会后面有时间再补上吧
评论区