1. 项目介绍
1.1项目背景
- 基于经常在牛客网或leetcode 刷题,比较好奇这样的网站是如何形成呢?然后做一个;类似于leetcode这样的项目,那么这个项目我主要实现了哪些功能呢?
1.2项目实现的功能
- 能够保存题目;
- 展示题目列表;(id,标题,难度)
- 展示题目的详细信息(标题,难度,题目的描述,题目的代码模板)
- 能够在线编辑代码并且能够提交运行
2.项目具体实现
2.1 创建进程实现javac,java等命令
- 创建该类的目的是执行javac,java等命令,首先,先创建一个新进程(创建好进程之后,新的进程和当前的进程是一个并列的关系,新的进程跑起来之后,就需要获取新进程的的输出结果,需要使用process.getInputStream();然后读取新进程的内容依次写入到stdout中,标准错误也是类似。之后使用waifFor()获得子进程的退出码,一直阻塞等待到子进程结束,父进程才能够结束。当前CommandUtil这个类,就能够创建子进程实现某个命令。
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class CommandUtil{
public static int run(String cmd,String stdoutFile,
String stderrFile) throws IOException, InterruptedException {
Process process = Runtime.getRuntime().exec(cmd);
if (stdoutFile != null) {
InputStream stdoutFrom = process.getInputStream();
FileOutputStream stdoutTo = new FileOutputStream(stdoutFile);
while (true){
int ch = stdoutFrom.read();
if(ch == -1){
break;
}
stdoutTo.write(ch);
}
stdoutFrom.close();
stdoutTo.close();
}
if(stderrFile != null){
InputStream stderrFrom = process.getErrorStream();
FileOutputStream stderrTo = new FileOutputStream(stderrFile);
while(true){
int ch = stderrFrom.read();
if(ch == -1){
break;
}
stderrTo.write(ch);
}
stderrFrom.close();
stderrTo.close();
}
int exitCode = process.waitFor();
return exitCode;
}
}
2.2 创建类Question,Answer
- 实现了CommandUtil这个类,这个类的作用是创建一个子进程,使子进程能够执行某个命令,比如说是javac ,或者java这样的命令。之后就需要创建两个类,Question和Answer,Question表示去编译运行的代码,Answer表示运行后的结果。设计Question类的时候,需要一个要编译运行的源代码,可以用code表示。设计Answer类的时候,Answer类的属性:errno0表示编译运行都ok,1表示编译出错,2表示运行抛异常,reason,errno1,reason包含了编译错误的信息,errno2,reason包含了异常的调用栈信息,程序的标准输出 stdout.程序的标准错误,stderr.
2.3 Task类
- 创建好Answer,Question之后,创建Task类,主要实现的功能是实现完整的编译或者运行的功能。这个类主要实现的方法是compileAndRun,主要有四个部分:
- 先准备好需要用到的临时文件(在这之前需要给这些文件准备好一个目录)(使用UUID来辅助生成目录)
- 构造编译指令(javac),并进行执行,预期得到的结果
- 构造运行指令(java),并进行执行,预期得到的结果
- 把最终的结果构造成Answer对象。
import util.FileUtil;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
public class Task {
private String WORK_DIR ;
private String CLASS = "Solution";
private String CODE ;
private String STDOUT ;
private String STDERR ;
private String COMPILE_ERROR ;
public Task(){
WORK_DIR = "./tmp/"+UUID.randomUUID().toString()+"/";
CODE = WORK_DIR + CLASS +".java";
STDOUT = WORK_DIR + "stdout.txt";
STDERR = WORK_DIR + "stderr.txt";
COMPILE_ERROR = WORK_DIR + "compile_error.txt";
}
public Answer compileAndRun(Question question) throws IOException, InterruptedException {
Answer answer = new Answer();
File file = new File(WORK_DIR);
if(!file.exists()){
file.mkdirs();
}
FileUtil.writeFile(CODE,question.getCode());
String compileCmd = String.format("javac -encoding utf-8 %s -d %s",CODE,WORK_DIR);
System.out.println("编译命令:"+compileCmd);
CommandUtil.run(compileCmd,null,COMPILE_ERROR);
String compileError = FileUtil.readFile(COMPILE_ERROR);
if(!compileError.equals("")){
answer.setErrno(1);
answer.setReason(compileError);
return answer;
}
String runCmd = String.format("java -classpath %s %s",WORK_DIR,CLASS);
System.out.println("runCmd"+runCmd);
CommandUtil.run(runCmd,STDOUT,STDERR);
String runError = FileUtil.readFile(STDERR);
if(!runError.equals("")){
answer.setErrno(2);
answer.setReason(runError);
return answer;
}
answer.setErrno(0);
String runStdout = FileUtil.readFile(STDOUT);
answer.setStdout(runStdout);
return answer;
}
2.4创建 FileUtil
- 创建一个类FileUtil,使这个类封装一下Java的文件操作,在后面的代码中更方便使用。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileUtil {
public static String readFile(String filePath){
StringBuilder stringBuilder = new StringBuilder();
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(filePath);
while(true){
int ch = fileInputStream.read();
if(ch == -1){
break;
}
stringBuilder.append((char)ch);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return stringBuilder.toString();
}
public static void writeFile(String filePath,String content){
try(FileOutputStream fileOutputStream = new FileOutputStream(filePath)){
fileOutputStream.write(content.getBytes());
}catch (IOException e){
e.printStackTrace();
}
}
}
2.5 题目存储
- 题目存储这个模块我通过用数据库来进行存储。我实现的OJ项目有题目的列表和题目的详情页,因此题目的列表包括id ,标题,还有难度,题目的详情页包括题目表述和模板代码,还有测试代码。因此我在设计库中的表的时候就必须要包含着几个字段。
create database if not exists oj;
use oj;
drop table if exists oj_table;
create table oj_table(
id int primary key auto_increment,
title varchar(50),
level varchar(50),
description varchar(4096),
templateCode varchar(4096),
testCode varchar(4096)
);
- 建立完数据库以及表之后,就需要创建一个实体类problem,这个类的每一个实体对应数据库中的一条记录。problem类中包含的属性和数据库表中的子段是相匹配的。
- 设计完数据库之后,需要通过JDBC来完成针对数据库的操作。
- 首先和数据库建立连接,使用双重校验锁可以保证线程的安全。
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class DBUtil {
private static final String URL = "jdbc:mysql://127.0.0.1:3306/oj?Character=UTF8&&useSSL=false";
private static final String USERNAME = "root";
private static final String PASSWORD ="12345678";
private static volatile DataSource dataSource = null;
public static DataSource getDataSource(){
if(dataSource == null){
synchronized (DBUtil.class){
dataSource = new MysqlDataSource();
((MysqlDataSource)dataSource).setURL(URL);
((MysqlDataSource)dataSource).setUser(USERNAME);
((MysqlDataSource)dataSource).setPassword(PASSWORD);
}
}
return dataSource;
}
public static Connection getConnection(){
try {
return getDataSource().getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static void close(Connection connection, PreparedStatement preparedStatement, ResultSet resultSet){
if(resultSet != null){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(preparedStatement != null){
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(connection != null){
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
- 和数据库建立连接之后,写一个类,problemDAO主要包含一些增删改查的方法,主要用来操作数据库。
public class problemDAO{
public List<Problem> selectAll(){
List<Problem> problems = new ArrayList<>();
Connection connection = DBUtil.getConnection();
PreparedStatement statement = null;
ResultSet resultSet = null;
String sql = "select id,title,level from oj_table";
try {
statement = connection.prepareStatement(sql);
resultSet = statement.executeQuery();
while(resultSet.next()){
Problem problem = new Problem();
problem.setId(resultSet.getInt("id"));
problem.setTitle(resultSet.getString("title"));
problem.setLevel(resultSet.getString("level"));
problems.add(problem);
}
} catch (SQLException e) {
e.printStackTrace();
}finally{
DBUtil.close(connection,statement,resultSet);
}
return problems;
}
public static Problem selectOne(int problemId){
Connection connection = DBUtil.getConnection();
String sql = "select * from oj_table where id=?";
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
statement = connection.prepareStatement(sql);
statement.setInt(1,problemId);
resultSet = statement.executeQuery();
if(resultSet.next()){
Problem problem = new Problem();
problem.setId(resultSet.getInt("id"));
problem.setTitle(resultSet.getString("title"));
problem.setLevel(resultSet.getString("level"));
problem.setDescription(resultSet.getString("description"));
problem.setTemplateCode(resultSet.getString("templateCode"));
problem.setTestCode(resultSet.getString("testCode"));
return problem;
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
DBUtil.close(connection,statement,resultSet);
}
return null;
}
}
2.6 实现前后的交互
- 实现一下服务器的逻辑,让服务器能够跑起来,能够接受请求,再给服务器的逻辑中调用上面的编译模块和数据库操作模块。要想实现前后端的交互操作,就需要约定前后端接口。
- 实现前后端的交互接下来就要约定好,服务器都能够接受怎么样的HTTP请求以及每一个请求都返回啥样的结果数据。
- 前后端交互的API的设计风格我用的是Restful风格:
- 使用不同的HTTP方法表示不同的操作类型(GET表示获取数据,POST表示新增数据,PUT表示修改数据,DELETE表示删除数据);
- 使用URL中的路径表示要操作的对象,比如说:POST/problem 新增一个题目,GET/problem获取题目信息,PUT/problem 修改题目 POST/user新增一个用户信息;GET/user获取用户信息,PUT/user修改用户信息
- 使用JSON格式来表示HTTP body中的数据。(JSON最大的优势在于可读性较好,JSON最大的劣势是效率比较低)
- 使用HTTP的状态码表示执行结果。
- 那么知道了如何设计前后端的交互接口,那么我这个项目的前后交互接口有哪些呢?
- 请求获取题目列表 —— GET/problem
- 请求获取 题目的详情信息——GET/problem?id = 2
- 提交代码并且运行——POST/compile