该项目是一个类似于力扣的在线OJ平台,可以进行题目的编写和提交编译运行以及结果展示,使用的技术栈有:Java、MySQL、SpringBoot、MyBatis、Redis、Nginx、Docker
主要功能如下:
什么是进程?
进程可以看做操作系统中一个正在运行的程序的一个抽象,也可以把进程看做是程序的一次运行过程。在操作系统内部,进程是操作系统进行资源分配的基本单位
使用 PCB(进程控制块) 描述进程
组织:使用一定的数据结构来组织,常见做法就是使用双向链表
进程之间是相互独立的
什么是多进程?
一个CPU运行多个进程
由于CPU的运行速度极快,虽然CPU在一直进行切换,但是咱们坐在电脑前的用户,是感知不到这个切换过程的
进程和线程的关系
Java中的多进程编程
Java中中对系统提供的进程创建、进程终止、进程程序替换、进程间通信进程了限制,最终只给用户提供了两个操作
进程的创建
创建出一个新的进程,让这个新的进程来执行一系列任务,被创建出来的进程,称为"子进程",创建子进程的进程,称为"父进程",服务器的进程就相当于一个父进程
根据收到的用户发送过来的代码再 创建出一个子进程,一个父进程,可以有多个子进程,但是一个子进程,只能有一个父进程
一个操作系统上是运行了很多进程的,因为进程之间是相互隔离的,一个进程挂了是不会影响到其它进程的。如果使用多线程,我们并不知道用户提交的会提交什么样的代码,很可能提交一些恶意代码导致线程崩溃,而线程挂了很有可能就影响到了我们的整个服务进程。所以一定要采用多进程而不是多线程。
java和javac是一个控制台程序,它的输出,是输出到“标准输出”和"标准错误"这两个特殊的文件当中的,一个进程启动的时候,就会自动打开三个文件:
Runtime是Java中内置的一个单例类
process.getInputStream()
:该方法能把process这个子进程的标准输出给读取出来process.getErrorStream()
:该方法能把process这个子进程的标准错误给读取出来 process.waitFor()
:该方法能能让主进程进行阻塞等待,等待子进程process执行完毕。题目实体类
public class Problem {
private Integer id;
private String title;
private String levels;
private String description;
private String templateCode;
private String testCode;
private Date createTime;
private Date updateTime;
}
新增修改题目通过判断url中的querystr里是否存在题目Id,来判断是修改题目还是新增题目
约定请求:
post
{
"id" : "",
"title" : "题目标题",
"levels" : "题目难度",
"description" : "题干",
"templateCode" : "题目代码模板",
"testCode" : "题目测试用例"
}
响应:
{
code : 200,
message : ""
data:
}
@PostMapping("/add")
public Response add(@RequestBody Problem problem) {
if (problem == null || problem.getTitle() == null || "".equals(problem.getTitle().trim()) || problem.getLevels() == null ||
"".equals(problem.getLevels().trim()) || problem.getTestCode() == null || "".equals(problem.getTestCode().trim()) ||
problem.getTemplateCode() == null || "".equals(problem.getTemplateCode().trim())) {
return Response.fail("题目参数不完整");
}
int ret = problemService.add(problem);
if (ret == 1) {
return Response.success(200,"添加成功");
}
return Response.fail("添加失败");
}
请求:
post
{
/problem/all
}
响应:
{
code : 200,
message:"",
data:
[
{
id : 1,
title: "两数之和",
levels: "简单",
description: "题干",
template: "题目模板"
}
]
}
通过Answer表示编译运行结果,约定:
public class Answer {
// 错误码 0表示运行成功,1表示编译错误,2表示运行错误,-1表示违规代码
private Integer errorCode;
// 标准输出
private String stdout;
// 错误信息
private String errorInfo;
}
Task类描述的是每一次代码的提交:
通过UUID生成唯一的目录,保证每个用户提交的代码相互隔离
public class Task {
// 存放临时文件目录
private String workDir;
// 运行文件路径
private String className;
// 编译文件路径
private String classFile;
// 存放编译错误信息文件
private String compileErrorFile;
// 标准输出文件
private String stdoutFile;
// 标准错误文件
private String stderrFile;
public Task() {
this.workDir = "./tmp/"+UUID.randomUUID().toString()+"/";
this.className = "Solution";
this.classFile = workDir+ "Solution.java";
this.compileErrorFile = workDir+"compileErrInfo.txt";
this.stdoutFile = workDir+"stdout.txt";
this.stderrFile = workDir+"stderr.txt";
}
}
请求:
{
problemId : "题目id",
code : "提交的代码"
}
响应:
{
code : 200,
message : "信息",
data:{
errorCode : "错误码",
stdout: "标准输出",
derrorInfo, "出错信息"
}
}
编译运行流程:
如下方法表示一次编译或者运行:
/**
* 编译运行
* @param cmd 执行的命令
* @param stdoutFile
* @param stderrFile
* @return
*/
public static int run(String cmd,String stdoutFile,String stderrFile) {
Runtime runtime = Runtime.getRuntime();
int exitCode = -1;
try {
// 执行命令获得子进程
Process process = runtime.exec(cmd);
// 编译
if (stdoutFile == null) {
try (InputStream stderrInoutStream = process.getErrorStream();OutputStream stderrOutputSteam = new FileOutputStream(stderrFile);){
int ch;
// 将错误信息读入到错误日志文件
while ((ch = stderrInoutStream.read()) != -1) {
stderrOutputSteam.write(ch);
}
}
}
// 说明是运行
if (stdoutFile != null) {
try (InputStream stderrInoutStream = process.getErrorStream();
OutputStream stderrOutputSteam = new FileOutputStream(stderrFile);
InputStream stdoutInputStream = process.getInputStream();
OutputStream stdOutputStream = new FileOutputStream(stdoutFile)){
// 获取标准错误输入流
int ch;
// 将错误信息读入到错误日志文件
while ((ch = stderrInoutStream.read()) != -1) {
stderrOutputSteam.write(ch);
}
// 将子进程标准输出写入到指定文件
while ((ch = stdoutInputStream.read()) != -1) {
stdOutputStream.write(ch);
}
}
}
// 进程等待
exitCode = process.waitFor();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return exitCode;
}
拼接编译命令时通过 -d指定编译后的文件存放到指定位置,不然找不到字节码文件位置。
// 2.拼接编译命令
String compileCmd = String.format("javac -encoding utf8 %s -d %s",classFile,workDir);
//4.运行代码
String runCmd = String.format("java -classpath %s %s",workDir,className);
统一登录拦截
定义拦截器:
提供一个管理员页面来对题目进行添加和修改。管理员页面使用拦截器对普通用户进行拦截.
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/user/login")
.excludePathPatterns("/user/reg")
.excludePathPatterns("/user/verificationCode")
.excludePathPatterns("/login.html")
.excludePathPatterns("/reg.html")
.excludePathPatterns("/css/**")
.excludePathPatterns("/js/**")
.excludePathPatterns("/img/**");
registry.addInterceptor(new AdminInterceptor())
.addPathPatterns("/admin.html")
.addPathPatterns("/addProblem.html")
.addPathPatterns("/problem/update")
.addPathPatterns("/problem/add");
}
}
统一格式返回
统一的数据返回格式使用@ControllerAdvice
+ResponseBodyAdvice
实现
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Resource
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Response) {
return body;
}
if (body instanceof String) {
try {
return objectMapper.writeValueAsString(body);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return Response.success(body);
}
}
统一异常处理
@ControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler(Exception.class)
@ResponseBody
public Response exceptionAdvice(Exception e) {
return Response.fail("服务器异常");
}
}