OJ项目
项目描述
模拟实现类似于牛客以及leedcode的在线做题,通过浏览器实现客户端与服务器端的交互,将数据在网络中传输。
项目创建
基于Java 实现一个web程序(servlet)
1、创建maven项目,引入相关依赖
MySQL -connector:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
Servlet:`
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
2、创建目录
servlet项目基本要有的目录结构
web.xml
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>
ps:前置知识
1、I/O流(Java中的文件操作主要是I/O读写)
Java标准库中,java.io这个包里提供了很多操作文件的类
操作字节(二进制数据)——字节流:InputStream,FileInputStream,OutputStream,FileOutputStream
和操作字符(文本文件)——字符流:Reader,Writer,FileReader,FileWriter
注意:读写文件之前先打开文件,读写文件之后必须关闭文件
示例:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class TestFile {
public static void main(String[] args) throws IOException {
//把一个文件的内容读出来写到另一个指定的文件中(相当于拷贝功能)
String srcPath="e:/test1.txt";
String destPath="e:/test2.txt";
//打开test1文件
FileInputStream fileInputStream=new FileInputStream(srcPath);
//打开第二个文件test2
FileOutputStream fileOutputStream=new FileOutputStream(destPath);
//循环把第一个文件里的内容按照字节的方式读取到第二个文件
while(true){
//read方法依次返回一个字节(byte)但是用int接收的理由:
//1、Java中没有无符号数byte的范围:-128~127,实际应用需要返回0~255的数,就不能用byte接受
//2、read都到末尾返回EOF(可以用-1表示)
int ch= fileInputStream.read();
if (ch==-1){
//文件读取结束
break;
}
fileOutputStream.write(ch);
}
//关闭文件
fileInputStream.close();
fileOutputStream.close();
}
}
2、进程和线程的相关知识
进程:任务,操作系统想要执行一个具体的动作就要创建出一个对应的进程
“可执行文件”运行起来就是一个“进程”
并发:多个进程同时运行
“多进程编程”用来解决“并发编程”(将任务拆分成多个进程)
“线程”为了解决“多进程创建销毁的低效”,每个线程是独立的执行流,一个进程包含多个线程
虽然二者都能解决并发的问题,但是进程的独立性强,相互不影响(各有各的地址空间),多线程共用同一进程的地址空间
OJ项目:
服务器进程:运行Servlet,接受用户请求做出响应……
用户提交的代码也是一个独立的逻辑(使用多进程) 避免错误代码影响服务器进程
Java中的多进程编程
进程创建及进程等待
创建新进程,让新进程执行任务
服务器进程“父进程”,用户发来的进程“子进程”
Runtime.exec方法(属于Java中的内置类Runtime的一个方法):参数是一个字符串,表示一个可执行程序的路径;执行这个方法,就会把指定路径的可执行程序创建出进程并执行
一个进程在启动的时候,会自动打开三个文件;
1、标准输入——键盘
2、标准输出——显示器
3、标准错误——显示器
由于子进程未与idea关联,需手动获取
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class TestExec {
public static void main(String[] args) throws IOException, InterruptedException {
//Runtime在Java中是一个单例
Runtime runtime=Runtime.getRuntime();
//可创建子进程,但是父子进程并发执行
Process process=runtime.exec("javac");//相当于在cmd中输入的命令
//获取子进程标准输出
InputStream stdoutFrom= process.getInputStream();
//写入到另外一个文件
FileOutputStream stdoutTo=new FileOutputStream("stdout.txt");
while(true){
int ch=stdoutFrom.read();
if(ch==-1){
break;
}
stdoutTo.write(ch);
}
stdoutFrom.close();
stdoutTo.close();
//获取标准错误,从这个文件对象中读,把子进程的标准错误读出来
InputStream stderrFrom=process.getErrorStream();
FileOutputStream stderrTo=new FileOutputStream("stderr.txt");
while(true){
int ch=stderrFrom.read();
if(ch==-1){
break;
}
stderrTo.write(ch);
}
stderrFrom.close();
stderrTo.close();
//通过process类的waitFor方法实现进程等待(父进程执行到waitFor就会阻塞,直到子进程执行完毕为止)
//进程码为0正常退出,非0异常退出
int exitCode=process.waitFor();
System.out.println(exitCode);
}
}
创建CommandUtil类
实现一个完整的“编译运行”的模块
要做的事情:
输入:用户提交的代码
输出:程序的编译结果和运行结果
由于Java中的类名和文件名需要一致,code字符串中的类名需要和写入的文件名一致
因此约定:类名文件名都叫Solution(代码往Solution里面写)
package compile;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class CommandUtil {
//1、通过Runtime类得到Runtime实例,执行exec方法
//2、获取到标准输出,并写入到指定文件中
//3、获取到标准错误,并写入到指定文件中
//4、等待子进程结束,拿到子进程的状态码并返回
public static int run(String cmd,String stdoutFile,String stderrFile) throws InterruptedException {
try {
//1、通过Runtime类得到Runtime实例,执行exec方法
Process process=Runtime.getRuntime().exec(cmd);
//2、获取到标准输出,并写入到指定文件中
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();
}
//3、获取到标准错误,并写入到指定文件中
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();
}
//4、等待子进程结束,拿到子进程的状态码并返回
int exitCode=process.waitFor();
} catch (IOException e) {
throw new RuntimeException(e);
}
return 1;
}
public static void main(String[] args) throws InterruptedException {
CommandUtil.run("javac","stdout.txt","stderr.txt");
}
}
编译运行模块
创建Task类
package compile;
import com.sun.org.apache.xpath.internal.patterns.ContextMatchStepPattern;
import common.FileUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
//每次“编译+运行”的过程称为一个Task
public class Task {
//通过一组常量约定临时文件名字
//所有文件所在目录WORK_DIR
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这个类就能生成一个UUID
WORK_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";
}
//这个Task类提供的核心方法,叫做compileAndRun
//参数:需要编译运行的Java源代码
//返回值:编译运行的结果,编译出错/运行出错/运行正确...
public Answer compileAndRun(Question question) throws InterruptedException {
Answer answer = new Answer();
//准备用来存放临时文件的目录
File workDir =new File(WORK_DIR);
if(!workDir.exists()){
//创建多级目录
workDir.mkdirs();
}
//进行安全性判定
if(!checkCodeSafe(question.getCode())){
System.out.println("用户提交了不安全的代码");
answer.setError(3);
answer.setReason("提交的代码可能危害到服务器,禁止运行!");
return answer;
}
//1、创建子进程,调用javac进行编译,编译需要.java文件,当前是通过String的方法提供的代码
FileUtil.writeFile(CODE,question.getCode());
// 如果编译出错,javac就会把错误信息写到stderr里面,可以用一个专门的文件进行保存(compileError.txt)
//2、将Question中的code写入到一个Solution.java文件中
//如果编译出错,javac就会把错误信息写入到stderr里面,专门用一个文件来保存compileError.txt
//需要先把编译命令给构造出来
String 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,让Answer里面记录编译的错误信息
answer.setError(1);
answer.setReason(compileError);
return answer;
}
//3、创建子进程,调用java命令执行
// 运行程序时,会把java子进程的标准输出和标准错误获取到(stdout.txt,stderr.txt)
String 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、让父进程获取到刚才编译执行的结果并打包成Answer对象
// 编译执行的结果就通过刚才约定的这几个文件来进行获取
answer.setError(0);
answer.setStdout(FileUtil.readFile(STDOUT));
return answer;
}
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;
}
public static void main(String[] args) throws InterruptedException {
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" +
"}");
Answer answer = task.compileAndRun(question);
System.out.println(answer);
}
}
为啥创建这么多临时文件?
目的:进行进程间通讯
进程和进程间存在独立性,一个进程很难影响到其他进程
实质上:某一个东西只要能被多个进程访问到,便可以用来进行进程间通讯。
创建Question类
package compile;
//用这个类表示一个task的输入内容
//会包含要编译的代码
public class Question {
private String code;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
创建Answer类
package compile;//表示一个task的执行结果
public class Answer {
//错误码,约定error为0表示编译运行都OK,为1表示编译出错,为2表示运行出错(抛异常)
private int error;
//设置出错原因,error为1,放编译错误信息,error为2,放运行错误信息
private String reason;
//运行程序得到的标准输出的结果
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;
}
@Override
public String toString() {
return "compile.Answer{" +
"error=" + error +
", reason='" + reason + '\'' +
", stdout='" + stdout + '\'' +
", stderr='" + stderr + '\'' +
'}';
}
}
封装文件
FileUtil类
package common;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class FileUtil {
//把filePath对应的文件内容读取出来,放到返回值中
public static String readFile(String filePath){
StringBuilder result = new StringBuilder();//String里字符串不能修改
try( FileReader fileReader=new FileReader(filePath)) {
while(true){
int ch = fileReader.read();//每次读取一个字符
if(ch==-1){
break;
}
result.append((char)ch);
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
return result.toString();
}
//负责把content写入到filePath对应的文件中
public static void writeFile(String filePath,String content){
try(FileWriter fileWriter= new FileWriter(filePath)){
fileWriter.write(content);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
题目管理模块
数据库:保存当前题目信息
题目标题id、题目名称title、题目难度level、题目描述description、题目模板代码templateCode、测试用例代码testCode
create database if not exists oj_database;
use oj_database;
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)
);
DBUtil类
package common;
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
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_database?characterEncoding=utf8&&useSSL=false";
private static final String USERNAME = "root";
private static final String PASSWORD =null;
private static volatile MysqlDataSource dataSource = null;
private static MysqlDataSource getDataSource(){
if(dataSource == null){ //提高效率
synchronized (DBUtil.class){ //加锁保证线程安全
if(dataSource==null){ //保证代码一定是单例实现
MysqlDataSource mysqlDataSource = new MysqlDataSource();
mysqlDataSource.setURL(URL);
mysqlDataSource.setUser(USERNAME);
mysqlDataSource.setPassword(PASSWORD);
dataSource = mysqlDataSource;//将数据类型迁移到mysqldatasource
}
}
}
return dataSource;
}
public static Connection getConnection() throws SQLException {
return getDataSource().getConnection();
}
public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet){
if(resultSet != null){
try {
resultSet.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if(statement != null){
try {
statement.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if(resultSet != null){
try {
resultSet.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
封装数据库操作
通过一个实体类对象对应表中的一条记录
创建dao包(数据访问对象)
Problem类(实体类),一个Problem对象对应表中的一条记录
package dao;
public class Problem {
private int id;
private String title;
private String level;
private String description;
private String templateCode;
private String testCode;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getTemplateCode() {
return templateCode;
}
public void setTemplateCode(String templateCode) {
this.templateCode = templateCode;
}
public String getTestCode() {
return testCode;
}
public void setTestCode(String testCode) {
this.testCode = testCode;
}
@Override
public String toString() {
return "Problem{" +
"id=" + id +
", title='" + title + '\'' +
", level='" + level + '\'' +
", description='" + description + '\'' +
", templateCode='" + templateCode + '\'' +
", testCode='" + testCode + '\'' +
'}';
}
}
对于表的增删改查操作,创建一个ProblemDAO来负责进行这些操作
ProblemDAO类
package dao;
import common.DBUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
//封装对problem的增删改查
//管理员:新增题目,删除题目
//普通用户:查询题目列表,查询题目详情
public class ProblemDAO {
public void insert(Problem problem) {
Connection connection =null;
PreparedStatement statement =null;
try{
//1、和数据可建立连接
connection = DBUtil.getConnection();
//2、构造sql语句
String sql = "insert into oj_table values(null,?,?,?,?,?)";
statement = connection.prepareStatement(sql);
statement.setString(1,problem.getTitle());
statement.setString(2,problem.getLevel());
statement.setString(3,problem.getDescription());
statement.setString(4,problem.getTemplateCode());
statement.setString(5,problem.getTestCode());
//3、执行sql语句
int ret = statement.executeUpdate();//影响数据条数
if(ret !=1){
System.out.println("新增题目失败!");
}else{
System.out.println("新增题目成功!");
}
}catch(SQLException e){
e.printStackTrace();
}finally {
DBUtil.close(connection,statement,null);
}
}
public void delete(int id){
Connection connection = null;
PreparedStatement statement = null;
try{
//1、和数据库建立连接
connection =DBUtil.getConnection();
//2、拼装sql语句
String sql = "delete from oj_table where id =?";
statement =connection.prepareStatement(sql);
statement.setInt(1,id);
//3、执行sql
int ret = statement.executeUpdate();
if (ret !=1){
System.out.println("删除题目失败!");
}else{
System.out.println("删除题目成功!");
}
} catch (SQLException e) {
throw new RuntimeException(e);
}finally{
DBUtil.close(connection,statement,null);
}
}
//将当前列表中的所有题目都查出来"分页查询“
//前端实现分页器,后端用sql limit offset算
public List<Problem> selectAll(){
List<Problem> problems = new ArrayList<>();
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try{
//1、和数据库建立连接
connection = DBUtil.getConnection();
//2、拼装sql
String sql = "select id,title,level from oj_table ";
statement = connection.prepareStatement(sql);
//3、执行sql
resultSet = statement.executeQuery();
//4、遍历resultSet
while(resultSet.next()){
//每一行都是一个Problem对象
Problem problem = new Problem();
problem.setId(resultSet.getInt("id"));
problem.setTitle(resultSet.getString("title"));
problem.setLevel(resultSet.getString("level"));
problems.add(problem);
}
return problems;
} catch (SQLException e) {
throw new RuntimeException(e);
}finally{
DBUtil.close(connection,statement,resultSet);
}
// return null;
}
//查找一道具体的题目
public Problem selectOne(int id){
Connection connection =null;
PreparedStatement statement =null;
ResultSet resultSet =null;
try {
//1、和数据库建立连接
connection =DBUtil.getConnection();
//2、拼接SQL语句
String sql = "select * from oj_table where id=? ";
statement = connection.prepareStatement(sql);
statement.setInt(1,id);
//3、执行SQL语句
resultSet = statement.executeQuery();
//4、遍历查询结果(由于id是主键,按照id查处的结果一定唯一)
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) {
throw new RuntimeException(e);
}finally{
DBUtil.close(connection,statement,resultSet);
}
return null;
}
private static void testInsert() throws SQLException {
ProblemDAO problemDAO =new ProblemDAO();
Problem problem =new Problem();
problemDAO.insert(problem);
problem.setTitle("两数之和");
problem.setLevel("简单");
problem.setDescription("给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。\n" +
"\n" +
"你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。\n" +
"\n" +
"你可以按任意顺序返回答案。\n" +
"\n" +
" \n" +
"\n" +
"示例 1:\n" +
"\n" +
"输入:nums = [2,7,11,15], target = 9\n" +
"输出:[0,1]\n" +
"解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。\n" +
"示例 2:\n" +
"\n" +
"输入:nums = [3,2,4], target = 6\n" +
"输出:[1,2]\n" +
"示例 3:\n" +
"\n" +
"输入:nums = [3,3], target = 6\n" +
"输出:[0,1]\n" +
" \n" +
"\n" +
"提示:\n" +
"\n" +
"2 <= nums.length <= 104\n" +
"-109 <= nums[i] <= 109\n" +
"-109 <= target <= 109\n" +
"只会存在一个有效答案\n" +
"进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗?\n" +
"\n" +
"来源:力扣(LeetCode)\n" +
"链接:https://leetcode.cn/problems/two-sum\n" +
"著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。");
problem.setTemplateCode("class Solution {\n" +
" public int[] twoSum(int[] nums, int target) {\n" +
"\n" +
" }\n" +
"}");
problem.setTestCode(" public static void main(String[] args) {\n" +
" Solution solution =new Solution();\n" +
" //testcase1\n" +
" int[] nums ={2,7,11,15};\n" +
" int target = 9;\n" +
" int[] result = solution.twoSum(nums,target);\n" +
" if(result.length == 2 && result[0]==0 && result[1]==1){\n" +
" System.out.println(\"testcase1 successful\");\n" +
" }else{\n" +
" System.out.println(\"testcase1 failed\");\n" +
" }\n" +
" //testcase2\n" +
" int[] nums2={3,2,4};\n" +
" int target2 =6;\n" +
" int[] result2 =solution.twoSum(nums2,target2);\n" +
" if(result2.length==2 && result[0]==1 && result[1]==2){\n" +
" System.out.println(\"testcase2 successful\");\n" +
" }else{\n" +
" System.out.println(\"testcase2 failed\");\n" +
" }\n" +
" }");
problemDAO.insert(problem);
}
private static void testSelectAll(){
ProblemDAO problemDAO = new ProblemDAO();
List<Problem> problem = problemDAO.selectAll();
System.out.println(problem);
}
private static void testSelectOne(){
ProblemDAO problemDAO =new ProblemDAO();
Problem problem = problemDAO.selectOne(1);
System.out.println(problem);
}
private static void testDelete(){
ProblemDAO problemDAO =new ProblemDAO();
problemDAO.delete(8);
}
public static void main(String[] args) throws SQLException {
testInsert();
//testSelectAll();
//testSelectOne();
//testDelete();
}
}
Solution类
public class Solution {
public int[] twoSum(int[] nums, int target) {
int [] a={0,1};
return a;
}
//这个main方法就相当于测试用例的代码testCode
public static void main(String[] args) {
Solution solution =new Solution();
//testcase1
int[] nums ={2,7,11,15};
int target = 9;
int[] result = solution.twoSum(nums,target);
if(result.length == 2 && result[0]==0 && result[1]==1){
System.out.println("testcase1 successful");
}else{
System.out.println("testcase1 failed");
}
//testcase2
int[] nums2={3,2,4};
int target2 =6;
int[] result2 =solution.twoSum(nums2,target2);
if(result2.length==2 && result[0]==1 && result[1]==2){
System.out.println("testcase2 successful");
}else{
System.out.println("testcase2 failed");
}
}
}
设计服务器提供的API
API(一些预先定义的接口,如函数、HTTP接口,通过这些接口可以和网页前端进行交互)
一、设计服务器需要考虑到有哪些页面
1、题目列表页:
功能是展示当前题目的列表→向服务器请求,题目的列表
2、题目详情页:
功能一:展示题目的详细要求→向服务器请求,获取指定题目的详细信息
功能二:有一个代码编辑框,让用户编写代码(纯前端,无需交互)
功能三:有一个提交按钮,点击提交按钮就能把用户的代码提交到服务器上,服务器进行编译和运行,并返回结果→向服务器发送用户当前编写得代码,并获取结果
二、具体设计这几根前后端交互的API
流行的前后端交互方式:通过JSON格式来组织,解析比较麻烦→引入第三方库Jackson
1、实现第一个API(向服务器请求,题目的列表)
2、实现第二个API(向服务器请求,获取指定题目的详细信息)
3、实现第三个API(向服务器发送用户当前编写得代码,并获取结果)
如何向服务器发送用户的代码
①GET:将代码放到url中,通过querystring来发,但需要对代码的字符串进行urlencode(比较麻烦)
②POST:将代码放到body中即可
实现题目管理页
创建api包
ProblemServlet类
package api;
import com.fasterxml.jackson.databind.ObjectMapper;
import dao.Problem;
import dao.ProblemDAO;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@WebServlet ("/problem")//便于服务器找到这个类
public class ProblemServlet extends HttpServlet {
private ObjectMapper objectMapper = new ObjectMapper();//jason核心类
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(200);
resp.setContentType("application/json;charset=utf8");
ProblemDAO problemDAO =new ProblemDAO();
//尝试获取id参数,如果能获取到,说明获取的是题目详情;如果不能获取到,说明书获取题目列表
String idString = req.getParameter("id");
if(idString == null || "".equals(idString)){
//没有获取到id字段,查询题目列表
List<Problem> problems =problemDAO.selectAll();
//将problems转化为字符串
String respString =objectMapper.writeValueAsString(problems);
//设置HTTP响应的body部分
resp.getWriter().write(respString);
}else{
//获取到题目id,查询题目详情
Problem problem = problemDAO.selectOne(Integer.parseInt(idString));
String respString = objectMapper.writeValueAsString(problem);
resp.getWriter().write(respString);
}
}
}
CompileServlet类
package api;
import com.fasterxml.jackson.databind.ObjectMapper;
//import compile.Answer;
import common.CodeInvalidException;
import common.ProblemNotFoundException;
import compile.Answer;
import compile.Question;
import compile.Task;
import dao.Problem;
import dao.ProblemDAO;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
@WebServlet("/compile")
public class CompileServlet extends HttpServlet {
static class CompileRequest{
public int id;
public String code;
}
static class CompileResponse{
//约定error为0表示编译运行都正常,1表示编译出错,2表示运行出错(用户代码异常),3表示其他错误
public int error;
public String reason;
public String stdout;
}
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
CompileRequest compileRequest = null;
CompileResponse compileResponse = new CompileResponse();
try{
resp.setStatus(200);
resp.setContentType("application/json;charset=utf8");
//1、先读取请求的正文,按照json格式解析
String body = readBody(req);
compileRequest = objectMapper.readValue(body, CompileRequest.class);
//2、根据id在数据库中查找到题目的详情,从而得到测试用例代码
ProblemDAO problemDAO = new ProblemDAO();
Problem problem = problemDAO.selectOne(compileRequest.id);
if(problem==null){
//为了统一处理错误,在这个地方抛出一个异常
throw new ProblemNotFoundException();
}
//testCode是测试用例的代码
String testCode = problem.getTestCode();
//requestCode是用户提交的代码
String requestCode = compileRequest.code;
//3、把用户提交的代码和测试用例代码,拼接成一个完整的代码
String finalCode = mergeCode(requestCode, testCode);
System.out.println(finalCode);
if(finalCode == null){
throw new CodeInvalidException();
}
//4、创建一个Task实例,调用里面的compileAndRun进行编译运行
Task task = new Task();
Question question = new Question();
question.setCode(finalCode);
Answer answer;
try {
answer = task.compileAndRun(question);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//5、根据Task运行的结果,包装成一个HTTP响应
compileResponse.error = answer.getError();
compileResponse.reason = answer.getReason();
compileResponse.stdout = answer.getStdout();
String respString = objectMapper.writeValueAsString(compileResponse);
resp.getWriter().write(respString);
} catch (ProblemNotFoundException e) {
//处理题目没有找到的异常
compileResponse.error=3;
compileResponse.reason ="没有找到指定题目 id ="+ compileRequest.id;
} catch (CodeInvalidException e) {
compileResponse.error=3;
compileResponse.reason ="提交的代码不符合要求!";
String respString = objectMapper.writeValueAsString(compileResponse);
resp.getWriter().write(respString);
}
}
private String mergeCode(String requestCode, String testCode) {//合并代码的方法实现
//1、查找requestCode中的最后一个 }
int pos = requestCode.lastIndexOf("}");
if(pos==-1){
//说明提交的代码完全没有},代码必然是错误的
return null;
}
//2、根据这个位置进行字符串截取,排除最后一个}
String subStr = requestCode.substring(0,pos);
//3、进行拼接
return subStr + testCode + "\n";
}
private 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) {
throw new RuntimeException(e);
}
//5、把这个byte[]的内容构造成一个String
return new String (buffer,"utf8");//把二进制转成文本数据
}
}
每次有一个请求过来,都需要生成一组临时文件,如果同一时刻,有N个请求一起过来了,这些请求的临时文件名和所在目录都是一样的,此时多个请求之间就会产生“相互干扰”的情况(类似于线程安全问题)
解决:让每一个请求创建的work-dir目录都不一样(可以使用“唯一ID”来作为目录名字→UUID“全球唯一”),每个请求生成一个唯一的UUID,进一步创建一个以UUID命名的临时目录,请求生成的临时文件就放在这个临时目录中。由于UUID唯一,请求也就不会互相影响。(磁盘上的临时目录在达到一定上限时,磁盘会定期清理)
前端的实现
题目列表页
页面不能写死,需要让页面通过ajax的方式从服务器获取
<script>
//在页面加载的时候,尝试从服务器上获取题目列表,通过ajax的方式进行获取。
function getProblems(){
//1、先通过ajax从服务器获取到题目列表,$是jQuery中的特殊变量
$.ajax({
url:"problem",
type:"GET",
success: function(data,status){
//data是响应的body,status是响应的状态码
//2、把得到的响应数据给构造成HTML片段
makeProblemTable(data);
}
});
}
//通过这个函数把数据转化成HTML页面片段
function makeProblemTable(data){
let problemTable =document.querySelector("#problemTable");
for (let problem of data){
let tr =document.createElement("tr");
let tdId = document.createElement("td");
tdId.innerHTML =problem.id;
tr.appendChild(tdId);
let tdTitle = document.createElement("td");
let a =document.createElement("a");
a.innerHTML = problem.title;
a.herf = 'problemDetail.html?id='+problem.id;
a.target = '_blank';
tdTitle.appendChild(a);
tr.appendChild(tdTitle);
let tdLevel = document.createElement("td");
tdLevel.innerHTML = problrm.level;
tr.appendChild(tdLevel);
problemTable.appendChild(tr);
}
}
getProblems();
</script>