# 《蓝桥OJ在线判题系统》
# 实现方案和项目初始化
OJ = Online Judge 在线判题评测系统
用户可以选择题目,在线做题,编写代码并且提交代码,系统会对用户提交的代码,根据我们出题人设置
的答案,来判断用户的提交结果是否正确
ACM(程序设计竞赛),也是需要依赖判题系统来检测参赛者的答案是否合理
OJ系统最大的难度就在于判题系统用于在线评测编程题目代码和系统,能够根据用户提交的代码、出题人
预先设置的题目输入和输出用例,进行编译代码、运行代码、判断代码运行结果是否正确
# OJ系统的常用概念
ac表示你的题目通过,结果正确
题目限制:时间限制、内存限制
题目介绍
题目输入
题目输出
题目输入用例
题目输出用例
普通测评:管理员设置题目的输入和输出用例,比如我输入1,你要输出2才是正确的;交给判题机去执行用
户的代码,给用户的代码喂输入用例,比如1,看用户程序的执行结果是否和标准答案的输出一致。
(比对用例文件)
不能让用户随便引入包、随便遍历、暴力破解,需要使用正确的算法。 => 安全性
判题过程是异步的 => 异步化
提交之后,会生成一个提交记录,有运行的结果以及运行信息(时间限制、内存限制)
# 为什么带大家写这个项目?
- 这个项目网上教程很少,基本上找不到教程
- 比较新颖,写在简历上会有区分度、有亮点(人家写外卖,你写 OJ)
- 能学到东西,相比于传统的 CRUD 来讲,这个项目的 CRUD 成分很少,更多的在于一些编程思想、计算机基础、架构设计方面的知识
- 复杂度 “高”,很多同学觉得 OJ 很难做,一起来攻克它
- 可扩展性非常强
# 现有系统调研
我也去网上找了很多相关的资料和页面
https://github.com/HimitZH/HOJ(适合学习)
https://github.com/QingdaoU/OnlineJudge(python,不好学,很成熟)
https://github.com/hzxie/voj(星星没那么多,没那么成熟,但相对好学)
https://github.com/vfleaking/uoj(php 实现的)
https://github.com/zhblue/hustoj(成熟,但是 php)
https://github.com/hydro-dev/Hydro(功能强大,Node.js 实现)
# 核心业务流程
为啥要编译? 因为有些语言不编译不能运行
判题服务:获取题目信息、预计的输入输出结果,返回给主业务后端:用户的答案是否正确
代码沙箱:只负责运行代码,给出结果,不管什么结果是正确的。
实现了解耦
# 功能
- 题目模块
- 创建题目(管理员)
- 删除题目(管理员)
- 修改题目(管理员)
- 搜索题目(用户)
- 在线做题
- 提交题目代码
- 用户模块
- 注册
- 登录
- 判题模块
- 提交判题(结果是否正确与错误)
- 错误处理(内存溢出、安全性、超时)
- 自主实现 代码沙箱(安全沙箱)
- 开放接口(提供一个独立的新服务)
# 技术选型
前后端全栈,所有都有
前端:Vue3、Arco Design 组件库、项目模板、在线代码编辑器、在线文档浏览
Java 进程控制、Java 安全管理器、部分 JVM 知识点
虚拟机(云服务器)、Docker(代码沙箱实现)
Spring Cloud 微服务 、消息队列、多种设计模式
# 前端
确认环境!!!
nodeJS版本:v18 或者 v16
node -v
切换和管理 node 版本的工具:https://github.com/nvm-sh/nvm
npm 版本:9.5.1
npm -v
拉取前端代码
https://github.com/1123377679/lanqiao-oj-web
# 后端
# 库表设计
用户表
只有管理员才能发布和管理题目,普通用户只能看题
-- 用户表
create table if not exists user
(
id bigint auto_increment comment 'id' primary key,
userAccount varchar(256) not null comment '账号',
userPassword varchar(512) not null comment '密码',
unionId varchar(256) null comment '微信开放平台id',
mpOpenId varchar(256) null comment '公众号openId',
userName varchar(256) null comment '用户昵称',
userAvatar varchar(1024) null comment '用户头像',
userProfile varchar(512) null comment '用户简介',
userRole varchar(256) default 'user' not null comment '用户角色:user/admin/ban',
createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
isDelete tinyint default 0 not null comment '是否删除',
index idx_unionId (unionId)
) comment '用户' collate = utf8mb4_unicode_ci;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
题目表
题目标题
题目内容:存放题目的介绍、输入输出提示、描述、具体的详情
题目标签(json 数组字符串):栈、队列、链表、简单、中等、困难
题目答案:管理员 / 用户设置的标准答案
提交数、通过题目的人数等:便于分析统计(可以考虑根据通过率自动给题目打难易度标签)
判题相关字段:
如果说题目不是很复杂,用例文件大小不大的话,可以直接存在数据库表里 但是如果用例文件比较大,>
512 KB 建议单独存放在一个文件中,数据库中只保存文件 url(类似存储用户头像)
- 输入用例:1、2
- 输出用例:3、4
- 时间限制
- 内存限制
judgeConfig 判题配置(json 对象):
- 时间限制 timeLimit
- 内存限制 memoryLimit
judgeCase 判题用例(json 数组)
- 每一个元素是:一个输入用例对应一个输出用例
[
{
"input": "1 2",
"output": "3 4"
},
{
"input": "1 3",
"output": "2 4"
}
]
2
3
4
5
6
7
8
9
10
11
存 json 的好处:便于扩展,只需要改变对象内部的字段,而不用修改数据库表(可能会影响数据库)
{
"timeLimit": 1000,
"memoryLimit": 1000,
"stackLimit": 1000
}
2
3
4
5
6
存 json 的前提:
- 你不需要根据某个字段去倒查这条数据
- 你的字段含义相关,属于同一类的值
- 你的字段存储空间占用不能太大
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for question
-- ----------------------------
DROP TABLE IF EXISTS `question`;
CREATE TABLE `question` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`title` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '标题',
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '内容',
`tags` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '标签列表(json 数组)',
`answer` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '题目答案',
`submitNum` int NOT NULL DEFAULT 0 COMMENT '题目提交数',
`acceptedNum` int NOT NULL DEFAULT 0 COMMENT '题目通过数',
`judgeCase` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '判题用例(json 数组)',
`judgeConfig` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '判题配置(json 对象)',
`thumbNum` int NOT NULL DEFAULT 0 COMMENT '点赞数',
`favourNum` int NOT NULL DEFAULT 0 COMMENT '收藏数',
`userId` bigint NOT NULL COMMENT '创建用户 id',
`createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`isDelete` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除',
`codeTemplate` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '题目代码模版',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_userId`(`userId` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1853788763736973314 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '题目' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
题目提交表
哪个用户提交了哪道题目,存放判题结果等
提交用户 id:userId
题目 id:questionId
语言:language
用户的代码:code
判题状态:status(0 - 待判题、1 - 判题中、2 - 成功、3 - 失败)
判题信息(判题过程中得到的一些信息,比如程序的失败原因、程序执行消耗的时间、空间):
judgeInfo(json 对象)
{
"message": "程序执行信息",
"time": 1000, // 单位为 ms
"memory": 1000, // 单位为 kb
}
2
3
4
5
6
判题信息枚举值:
- Accepted 成功
- Wrong Answer 答案错误
- Compile Error (opens new window) 编译错误
- Memory Limit Exceeded 内存溢出
- Time Limit Exceeded 超时
- Presentation Error 展示错误
- Output Limit Exceeded 输出溢出
- Waiting 等待中
- Dangerous Operation 危险操作
- Runtime Error 运行错误(用户程序的问题)
- System Error 系统错误(做系统人的问题)
-- 题目提交表
create table if not exists question_submit
(
id bigint auto_increment comment 'id' primary key,
language varchar(128) not null comment '编程语言',
code text not null comment '用户代码',
judgeInfo text null comment '判题信息(json 对象)',
status int default 0 not null comment '判题状态(0 - 待判题、1 - 判题中、2 - 成功、3 - 失败)',
questionId bigint not null comment '题目 id',
userId bigint not null comment '创建用户 id',
createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
isDelete tinyint default 0 not null comment '是否删除',
index idx_questionId (questionId),
index idx_userId (userId)
) comment '题目提交';
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 后端接口开发
# 后端开发流程
1)根据功能设计库表
2)自动生成对数据库基本的增删改查(mapper 和 service 层的基本功能)
3)编写 Controller 层,实现基本的增删改查和权限校验(复制粘贴)
4)去根据业务定制开发新的功能 / 编写新的代码
# 代码生成方法
1)安装MyBatisX插件
2)根据项目去调整生成设置,建议生成代码到独立的包,不要影响老的项目
3)把代码从生成包中移动到实际项目对应目录中
4)找相似的代码去复制Controller
- 单表去复制单表Controller(question => post)
- 关联表去复制关联表(比如 question_submit => post_thumb)
@RestController
@RequestMapping("/question")
@Slf4j
public class QuestionController {
@Resource
private QuestionService questionService;
@Resource
private UserService userService;
private final static Gson GSON = new Gson();
// region 增删改查
/**
* 创建
*
* @param questionAddRequest
* @param request
* @return
*/
@PostMapping("/add")
public BaseResponse<Long> addQuestion(@RequestBody QuestionAddRequest questionAddRequest, HttpServletRequest request) {
if (questionAddRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Question question = new Question();
BeanUtils.copyProperties(questionAddRequest, question);
List<String> tags = questionAddRequest.getTags();
if (tags != null) {
question.setTags(GSON.toJson(tags));
}
List<JudgeCase> judgeCase = questionAddRequest.getJudgeCase();
if (judgeCase != null) {
question.setJudgeCase(GSON.toJson(judgeCase));
}
JudgeConfig judgeConfig = questionAddRequest.getJudgeConfig();
if (judgeConfig != null) {
question.setJudgeConfig(GSON.toJson(judgeConfig));
}
questionService.validQuestion(question, true);
User loginUser = userService.getLoginUser(request);
question.setUserId(loginUser.getId());
question.setFavourNum(0);
question.setThumbNum(0);
boolean result = questionService.save(question);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
long newQuestionId = question.getId();
return ResultUtils.success(newQuestionId);
}
/**
* 删除
*
* @param deleteRequest
* @param request
* @return
*/
@PostMapping("/delete")
public BaseResponse<Boolean> deleteQuestion(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
if (deleteRequest == null || deleteRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User user = userService.getLoginUser(request);
long id = deleteRequest.getId();
// 判断是否存在
Question oldQuestion = questionService.getById(id);
ThrowUtils.throwIf(oldQuestion == null, ErrorCode.NOT_FOUND_ERROR);
// 仅本人或管理员可删除
if (!oldQuestion.getUserId().equals(user.getId()) && !userService.isAdmin(request)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
boolean b = questionService.removeById(id);
return ResultUtils.success(b);
}
/**
* 更新(仅管理员)
*
* @param questionUpdateRequest
* @return
*/
@PostMapping("/update")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> updateQuestion(@RequestBody QuestionUpdateRequest questionUpdateRequest) {
if (questionUpdateRequest == null || questionUpdateRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Question question = new Question();
BeanUtils.copyProperties(questionUpdateRequest, question);
List<String> tags = questionUpdateRequest.getTags();
if (tags != null) {
question.setTags(GSON.toJson(tags));
}
List<JudgeCase> judgeCase = questionUpdateRequest.getJudgeCase();
if (judgeCase != null) {
question.setJudgeCase(GSON.toJson(judgeCase));
}
JudgeConfig judgeConfig = questionUpdateRequest.getJudgeConfig();
if (judgeConfig != null) {
question.setJudgeConfig(GSON.toJson(judgeConfig));
}
// 参数校验
questionService.validQuestion(question, false);
long id = questionUpdateRequest.getId();
// 判断是否存在
Question oldQuestion = questionService.getById(id);
ThrowUtils.throwIf(oldQuestion == null, ErrorCode.NOT_FOUND_ERROR);
boolean result = questionService.updateById(question);
return ResultUtils.success(result);
}
/**
* 根据 id 获取
*
* @param id
* @return
*/
@GetMapping("/get/vo")
public BaseResponse<QuestionVO> getQuestionVOById(long id, HttpServletRequest request) {
if (id <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Question question = questionService.getById(id);
if (question == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
return ResultUtils.success(questionService.getQuestionVO(question, request));
}
/**
* 分页获取列表(封装类)
*
* @param questionQueryRequest
* @param request
* @return
*/
@PostMapping("/list/page/vo")
public BaseResponse<Page<QuestionVO>> listQuestionVOByPage(@RequestBody QuestionQueryRequest questionQueryRequest,
HttpServletRequest request) {
long current = questionQueryRequest.getCurrent();
long size = questionQueryRequest.getPageSize();
// 限制爬虫
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
Page<Question> questionPage = questionService.page(new Page<>(current, size),
questionService.getQueryWrapper(questionQueryRequest));
return ResultUtils.success(questionService.getQuestionVOPage(questionPage, request));
}
/**
* 分页获取当前用户创建的资源列表
*
* @param questionQueryRequest
* @param request
* @return
*/
@PostMapping("/my/list/page/vo")
public BaseResponse<Page<QuestionVO>> listMyQuestionVOByPage(@RequestBody QuestionQueryRequest questionQueryRequest,
HttpServletRequest request) {
if (questionQueryRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getLoginUser(request);
questionQueryRequest.setUserId(loginUser.getId());
long current = questionQueryRequest.getCurrent();
long size = questionQueryRequest.getPageSize();
// 限制爬虫
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
Page<Question> questionPage = questionService.page(new Page<>(current, size),
questionService.getQueryWrapper(questionQueryRequest));
return ResultUtils.success(questionService.getQuestionVOPage(questionPage, request));
}
/**
* 分页获取题目列表(仅管理员)
*
* @param questionQueryRequest
* @param request
* @return
*/
@PostMapping("/list/page")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Page<Question>> listQuestionByPage(@RequestBody QuestionQueryRequest questionQueryRequest,
HttpServletRequest request) {
long current = questionQueryRequest.getCurrent();
long size = questionQueryRequest.getPageSize();
Page<Question> questionPage = questionService.page(new Page<>(current, size),
questionService.getQueryWrapper(questionQueryRequest));
return ResultUtils.success(questionPage);
}
// endregion
/**
* 编辑(用户)
*
* @param questionEditRequest
* @param request
* @return
*/
@PostMapping("/edit")
public BaseResponse<Boolean> editQuestion(@RequestBody QuestionEditRequest questionEditRequest, HttpServletRequest request) {
if (questionEditRequest == null || questionEditRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Question question = new Question();
BeanUtils.copyProperties(questionEditRequest, question);
List<String> tags = questionEditRequest.getTags();
if (tags != null) {
question.setTags(GSON.toJson(tags));
}
List<JudgeCase> judgeCase = questionEditRequest.getJudgeCase();
if (judgeCase != null) {
question.setJudgeCase(GSON.toJson(judgeCase));
}
JudgeConfig judgeConfig = questionEditRequest.getJudgeConfig();
if (judgeConfig != null) {
question.setJudgeConfig(GSON.toJson(judgeConfig));
}
// 参数校验
questionService.validQuestion(question, false);
User loginUser = userService.getLoginUser(request);
long id = questionEditRequest.getId();
// 判断是否存在
Question oldQuestion = questionService.getById(id);
ThrowUtils.throwIf(oldQuestion == null, ErrorCode.NOT_FOUND_ERROR);
// 仅本人或管理员可编辑
if (!oldQuestion.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
boolean result = questionService.updateById(question);
return ResultUtils.success(result);
}
/**
* 根据 id 获取
*
* @param id
* @return
*/
@GetMapping("/get")
public BaseResponse<Question> getQuestionById(long id, HttpServletRequest request) {
if (id <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Question question = questionService.getById(id);
if (question == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
User loginUser = userService.getLoginUser(request);
// 不是本人或管理员,不能直接获取所有信息
if (!question.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
return ResultUtils.success(question);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
@RestController
@RequestMapping("/question_submit")
@Slf4j
public class QuestionSubmitController {
@Resource
private QuestionSubmitService questionSubmitService;
@Resource
private UserService userService;
/**
* 提交题目
*
* @param questionSubmitAddRequest
* @param request
* @return 提交记录的 id
*/
@PostMapping("/")
public BaseResponse<Long> doQuestionSubmit(@RequestBody QuestionSubmitAddRequest questionSubmitAddRequest,
HttpServletRequest request) {
if (questionSubmitAddRequest == null || questionSubmitAddRequest.getQuestionId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 登录才能点赞
final User loginUser = userService.getLoginUser(request);
long questionSubmitId = questionSubmitService.doQuestionSubmit(questionSubmitAddRequest, loginUser);
return ResultUtils.success(questionSubmitId);
}
/**
* 分页获取题目提交列表(除了管理员外,普通用户只能看到非答案、提交代码等公开信息)
*
* @param questionSubmitQueryRequest
* @param request
* @return
*/
@PostMapping("/list/page")
public BaseResponse<Page<QuestionSubmitVO>> listQuestionSubmitByPage(@RequestBody QuestionSubmitQueryRequest questionSubmitQueryRequest,
HttpServletRequest request) {
long current = questionSubmitQueryRequest.getCurrent();
long size = questionSubmitQueryRequest.getPageSize();
// 从数据库中查询原始的题目提交分页信息
Page<QuestionSubmit> questionSubmitPage = questionSubmitService.page(new Page<>(current, size),
questionSubmitService.getQueryWrapper(questionSubmitQueryRequest));
final User loginUser = userService.getLoginUser(request);
// 返回脱敏信息
return ResultUtils.success(questionSubmitService.getQuestionSubmitVOPage(questionSubmitPage, loginUser));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
5)复制实体类相关的DTO、VO、枚举值字段(用于接收前端请求、或者业务间传递信息)
复制之后,调整需要的字段
question相关dto
@Data
public class QuestionAddRequest implements Serializable {
/**
* 标题
*/
private String title;
/**
* 内容
*/
private String content;
/**
* 标签列表
*/
private List<String> tags;
/**
* 题目答案
*/
private String answer;
/**
* 判题用例
*/
private List<JudgeCase> judgeCase;
/**
* 判题配置
*/
private JudgeConfig judgeConfig;
private static final long serialVersionUID = 1L;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Data
public class QuestionEditRequest implements Serializable {
/**
* id
*/
private Long id;
/**
* 标题
*/
private String title;
/**
* 内容
*/
private String content;
/**
* 标签列表
*/
private List<String> tags;
/**
* 题目答案
*/
private String answer;
/**
* 判题用例
*/
private List<JudgeCase> judgeCase;
/**
* 判题配置
*/
private JudgeConfig judgeConfig;
private static final long serialVersionUID = 1L;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@EqualsAndHashCode(callSuper = true)
@Data
public class QuestionQueryRequest extends PageRequest implements Serializable {
/**
* id
*/
private Long id;
/**
* 标题
*/
private String title;
/**
* 内容
*/
private String content;
/**
* 标签列表
*/
private List<String> tags;
/**
* 题目答案
*/
private String answer;
/**
* 创建用户 id
*/
private Long userId;
private static final long serialVersionUID = 1L;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Data
public class QuestionUpdateRequest implements Serializable {
/**
* id
*/
private Long id;
/**
* 标题
*/
private String title;
/**
* 内容
*/
private String content;
/**
* 标签列表
*/
private List<String> tags;
/**
* 题目答案
*/
private String answer;
/**
* 判题用例
*/
private List<JudgeCase> judgeCase;
/**
* 判题配置
*/
private JudgeConfig judgeConfig;
private static final long serialVersionUID = 1L;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
questionSubmit相关dto
@Data
@EqualsAndHashCode(callSuper = true)
public class QuestionSubmitQueryRequest extends PageRequest implements Serializable {
/**
* 编程语言
*/
private String language;
/**
* 提交状态
*/
private Integer status;
/**
* 题目 id
*/
private Long questionId;
/**
* 用户 id
*/
private Long userId;
private static final long serialVersionUID = 1L;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Data
public class QuestionSubmitAddRequest implements Serializable {
/**
* 编程语言
*/
private String language;
/**
* 用户代码
*/
private String code;
/**
* 题目 id
*/
private Long questionId;
private static final long serialVersionUID = 1L;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
6)为了更方便地处理 json 字段中的某个字段,需要给对应的 json 字段编写独立的类,比如
judgeConfig、judgeInfo、judgeCase。
/**
* 题目用例
*/
@Data
public class JudgeCase {
/**
* 输入用例
*/
private String input;
/**
* 输出用例
*/
private String output;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 题目配置
*/
@Data
public class JudgeConfig {
/**
* 时间限制(ms)
*/
private Long timeLimit;
/**
* 内存限制(KB)
*/
private Long memoryLimit;
/**
* 堆栈限制(KB)
*/
private Long stackLimit;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 判题信息
*/
@Data
public class JudgeInfo {
/**
* 程序执行信息
*/
private String message;
/**
* 消耗内存
*/
private Long memory;
/**
* 消耗时间(KB)
*/
private Long time;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
小知识:什么情况下要加业务前缀?什么情况下不加?
加业务前缀的好处,防止多个表都有类似的类,产生冲突;不加的前提,因为可能这个类是多个
业务之间共享的,能够复用的。
定义 VO 类:作用是专门给前端返回对象,可以节约网络传输大小、或者过滤字段(脱敏)、保证安全性。
比如 judgeCase、answer 字段,一定要删,不能直接给用户答案。
/**
* 题目提交封装类
* @TableName question
*/
@Data
public class QuestionSubmitVO implements Serializable {
/**
* id
*/
private Long id;
/**
* 编程语言
*/
private String language;
/**
* 用户代码
*/
private String code;
/**
* 判题信息
*/
private JudgeInfo judgeInfo;
/**
* 判题状态(0 - 待判题、1 - 判题中、2 - 成功、3 - 失败)
*/
private Integer status;
/**
* 题目 id
*/
private Long questionId;
/**
* 创建用户 id
*/
private Long userId;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 提交用户信息
*/
private UserVO userVO;
/**
* 对应题目信息
*/
private QuestionVO questionVO;
/**
* 包装类转对象
*
* @param questionSubmitVO
* @return
*/
public static QuestionSubmit voToObj(QuestionSubmitVO questionSubmitVO) {
if (questionSubmitVO == null) {
return null;
}
QuestionSubmit questionSubmit = new QuestionSubmit();
BeanUtils.copyProperties(questionSubmitVO, questionSubmit);
JudgeInfo judgeInfoObj = questionSubmitVO.getJudgeInfo();
if (judgeInfoObj != null) {
questionSubmit.setJudgeInfo(JSONUtil.toJsonStr(judgeInfoObj));
}
return questionSubmit;
}
/**
* 对象转包装类
*
* @param questionSubmit
* @return
*/
public static QuestionSubmitVO objToVo(QuestionSubmit questionSubmit) {
if (questionSubmit == null) {
return null;
}
QuestionSubmitVO questionSubmitVO = new QuestionSubmitVO();
BeanUtils.copyProperties(questionSubmit, questionSubmitVO);
String judgeInfoStr = questionSubmit.getJudgeInfo();
questionSubmitVO.setJudgeInfo(JSONUtil.toBean(judgeInfoStr, JudgeInfo.class));
return questionSubmitVO;
}
private static final long serialVersionUID = 1L;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/**
* 题目封装类
* @TableName question
*/
@Data
public class QuestionVO implements Serializable {
/**
* id
*/
private Long id;
/**
* 标题
*/
private String title;
/**
* 内容
*/
private String content;
/**
* 标签列表
*/
private List<String> tags;
/**
* 题目提交数
*/
private Integer submitNum;
/**
* 题目通过数
*/
private Integer acceptedNum;
/**
* 判题配置(json 对象)
*/
private JudgeConfig judgeConfig;
/**
* 点赞数
*/
private Integer thumbNum;
/**
* 收藏数
*/
private Integer favourNum;
/**
* 创建用户 id
*/
private Long userId;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 创建题目人的信息
*/
private UserVO userVO;
/**
* 包装类转对象
*
* @param questionVO
* @return
*/
public static Question voToObj(QuestionVO questionVO) {
if (questionVO == null) {
return null;
}
Question question = new Question();
BeanUtils.copyProperties(questionVO, question);
List<String> tagList = questionVO.getTags();
if (tagList != null) {
question.setTags(JSONUtil.toJsonStr(tagList));
}
JudgeConfig voJudgeConfig = questionVO.getJudgeConfig();
if (voJudgeConfig != null) {
question.setJudgeConfig(JSONUtil.toJsonStr(voJudgeConfig));
}
return question;
}
/**
* 对象转包装类
*
* @param question
* @return
*/
public static QuestionVO objToVo(Question question) {
if (question == null) {
return null;
}
QuestionVO questionVO = new QuestionVO();
BeanUtils.copyProperties(question, questionVO);
List<String> tagList = JSONUtil.toList(question.getTags(), String.class);
questionVO.setTags(tagList);
String judgeConfigStr = question.getJudgeConfig();
questionVO.setJudgeConfig(JSONUtil.toBean(judgeConfigStr, JudgeConfig.class));
return questionVO;
}
private static final long serialVersionUID = 1L;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
7)校验 Controller 层的代码,看看除了要调用的方法缺失外,还有无报错
8)实现 Service 层的代码,从对应的已经编写好的实现类复制粘贴,全局替换(比如 question => post)
public interface QuestionService extends IService<Question> {
/**
* 校验
*
* @param question
* @param add
*/
void validQuestion(Question question, boolean add);
/**
* 获取查询条件
*
* @param questionQueryRequest
* @return
*/
QueryWrapper<Question> getQueryWrapper(QuestionQueryRequest questionQueryRequest);
/**
* 获取题目封装
*
* @param question
* @param request
* @return
*/
QuestionVO getQuestionVO(Question question, HttpServletRequest request);
/**
* 分页获取题目封装
*
* @param questionPage
* @param request
* @return
*/
Page<QuestionVO> getQuestionVOPage(Page<Question> questionPage, HttpServletRequest request);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public interface QuestionSubmitService extends IService<QuestionSubmit> {
/**
* 题目提交
*
* @param questionSubmitAddRequest 题目提交信息
* @param loginUser
* @return
*/
long doQuestionSubmit(QuestionSubmitAddRequest questionSubmitAddRequest, User loginUser);
/**
* 获取查询条件
*
* @param questionSubmitQueryRequest
* @return
*/
QueryWrapper<QuestionSubmit> getQueryWrapper(QuestionSubmitQueryRequest questionSubmitQueryRequest);
/**
* 获取题目封装
*
* @param questionSubmit
* @param loginUser
* @return
*/
QuestionSubmitVO getQuestionSubmitVO(QuestionSubmit questionSubmit, User loginUser);
/**
* 分页获取题目封装
*
* @param questionSubmitPage
* @param loginUser
* @return
*/
Page<QuestionSubmitVO> getQuestionSubmitVOPage(Page<QuestionSubmit> questionSubmitPage, User loginUser);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question>
implements QuestionService{
@Resource
private UserService userService;
/**
* 校验题目是否合法
* @param question
* @param add
*/
@Override
public void validQuestion(Question question, boolean add) {
if (question == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
String title = question.getTitle();
String content = question.getContent();
String tags = question.getTags();
String answer = question.getAnswer();
String judgeCase = question.getJudgeCase();
String judgeConfig = question.getJudgeConfig();
// 创建时,参数不能为空
if (add) {
ThrowUtils.throwIf(StringUtils.isAnyBlank(title, content, tags), ErrorCode.PARAMS_ERROR);
}
// 有参数则校验
if (StringUtils.isNotBlank(title) && title.length() > 80) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "标题过长");
}
if (StringUtils.isNotBlank(content) && content.length() > 8192) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "内容过长");
}
if (StringUtils.isNotBlank(answer) && answer.length() > 8192) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "答案过长");
}
if (StringUtils.isNotBlank(judgeCase) && judgeCase.length() > 8192) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "判题用例过长");
}
if (StringUtils.isNotBlank(judgeConfig) && judgeConfig.length() > 8192) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "判题配置过长");
}
}
/**
* 获取查询包装类(用户根据哪些字段查询,根据前端传来的请求对象,得到 mybatis 框架支持的查询 QueryWrapper 类)
*
* @param questionQueryRequest
* @return
*/
@Override
public QueryWrapper<Question> getQueryWrapper(QuestionQueryRequest questionQueryRequest) {
QueryWrapper<Question> queryWrapper = new QueryWrapper<>();
if (questionQueryRequest == null) {
return queryWrapper;
}
Long id = questionQueryRequest.getId();
String title = questionQueryRequest.getTitle();
String content = questionQueryRequest.getContent();
List<String> tags = questionQueryRequest.getTags();
String answer = questionQueryRequest.getAnswer();
Long userId = questionQueryRequest.getUserId();
String sortField = questionQueryRequest.getSortField();
String sortOrder = questionQueryRequest.getSortOrder();
String codeTemplate = questionQueryRequest.getCodeTemplate();
// 拼接查询条件
queryWrapper.like(StringUtils.isNotBlank(title), "title", title);
queryWrapper.like(StringUtils.isNotBlank(content), "content", content);
queryWrapper.like(StringUtils.isNotBlank(answer), "answer", answer);
if (CollectionUtils.isNotEmpty(tags)) {
for (String tag : tags) {
queryWrapper.like("tags", "\"" + tag + "\"");
}
}
queryWrapper.eq(ObjectUtils.isNotEmpty(id), "id", id);
queryWrapper.eq(ObjectUtils.isNotEmpty(userId), "userId", userId);
queryWrapper.eq("isDelete", false);
queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC),
sortField);
return queryWrapper;
}
@Override
public QuestionVO getQuestionVO(Question question, HttpServletRequest request) {
QuestionVO questionVO = QuestionVO.objToVo(question);
// 1. 关联查询用户信息
Long userId = question.getUserId();
User user = null;
if (userId != null && userId > 0) {
user = userService.getById(userId);
}
UserVO userVO = userService.getUserVO(user);
questionVO.setUserVO(userVO);
return questionVO;
}
@Override
public Page<QuestionVO> getQuestionVOPage(Page<Question> questionPage, HttpServletRequest request) {
List<Question> questionList = questionPage.getRecords();
Page<QuestionVO> questionVOPage = new Page<>(questionPage.getCurrent(), questionPage.getSize(), questionPage.getTotal());
if (CollectionUtils.isEmpty(questionList)) {
return questionVOPage;
}
// 1. 关联查询用户信息
Set<Long> userIdSet = questionList.stream().map(Question::getUserId).collect(Collectors.toSet());
Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()
.collect(Collectors.groupingBy(User::getId));
// 填充信息
List<QuestionVO> questionVOList = questionList.stream().map(question -> {
QuestionVO questionVO = QuestionVO.objToVo(question);
Long userId = question.getUserId();
User user = null;
if (userIdUserListMap.containsKey(userId)) {
user = userIdUserListMap.get(userId).get(0);
}
questionVO.setUserVO(userService.getUserVO(user));
return questionVO;
}).collect(Collectors.toList());
questionVOPage.setRecords(questionVOList);
return questionVOPage;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
@Service
public class QuestionSubmitServiceImpl extends ServiceImpl<QuestionSubmitMapper, QuestionSubmit>
implements QuestionSubmitService{
@Resource
private QuestionService questionService;
@Resource
private UserService userService;
/**
* 提交题目
*
* @param questionSubmitAddRequest
* @param loginUser
* @return
*/
@Override
public long doQuestionSubmit(QuestionSubmitAddRequest questionSubmitAddRequest, User loginUser) {
// 校验编程语言是否合法
String language = questionSubmitAddRequest.getLanguage();
QuestionSubmitLanguageEnum languageEnum = QuestionSubmitLanguageEnum.getEnumByValue(language);
if (languageEnum == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "编程语言错误");
}
long questionId = questionSubmitAddRequest.getQuestionId();
// 判断实体是否存在,根据类别获取实体
Question question = questionService.getById(questionId);
if (question == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
// 是否已提交题目
long userId = loginUser.getId();
// 每个用户串行提交题目
QuestionSubmit questionSubmit = new QuestionSubmit();
questionSubmit.setUserId(userId);
questionSubmit.setQuestionId(questionId);
questionSubmit.setCode(questionSubmitAddRequest.getCode());
questionSubmit.setLanguage(language);
// 设置初始状态
questionSubmit.setStatus(QuestionSubmitStatusEnum.WAITING.getValue());
questionSubmit.setJudgeInfo("{}");
boolean save = this.save(questionSubmit);
if (!save){
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "数据插入失败");
}
return questionSubmit.getId();
}
/**
* 获取查询包装类(用户根据哪些字段查询,根据前端传来的请求对象,得到 mybatis 框架支持的查询 QueryWrapper 类)
*
* @param questionSubmitQueryRequest
* @return
*/
@Override
public QueryWrapper<QuestionSubmit> getQueryWrapper(QuestionSubmitQueryRequest questionSubmitQueryRequest) {
QueryWrapper<QuestionSubmit> queryWrapper = new QueryWrapper<>();
if (questionSubmitQueryRequest == null) {
return queryWrapper;
}
String language = questionSubmitQueryRequest.getLanguage();
Integer status = questionSubmitQueryRequest.getStatus();
Long questionId = questionSubmitQueryRequest.getQuestionId();
Long userId = questionSubmitQueryRequest.getUserId();
String sortField = questionSubmitQueryRequest.getSortField();
String sortOrder = questionSubmitQueryRequest.getSortOrder();
// 拼接查询条件
queryWrapper.eq(StringUtils.isNotBlank(language), "language", language);
queryWrapper.eq(ObjectUtils.isNotEmpty(userId), "userId", userId);
queryWrapper.eq(ObjectUtils.isNotEmpty(questionId), "questionId", questionId);
queryWrapper.eq(QuestionSubmitStatusEnum.getEnumByValue(status) != null, "status", status);
queryWrapper.eq("isDelete", false);
queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC),
sortField);
return queryWrapper;
}
@Override
public QuestionSubmitVO getQuestionSubmitVO(QuestionSubmit questionSubmit, User loginUser) {
QuestionSubmitVO questionSubmitVO = QuestionSubmitVO.objToVo(questionSubmit);
// 脱敏:仅本人和管理员能看见自己(提交 userId 和登录用户 id 不同)提交的代码
long userId = loginUser.getId();
// 处理脱敏
if (userId != questionSubmit.getUserId() && !userService.isAdmin(loginUser)) {
questionSubmitVO.setCode(null);
}
return questionSubmitVO;
}
@Override
public Page<QuestionSubmitVO> getQuestionSubmitVOPage(Page<QuestionSubmit> questionSubmitPage, User loginUser) {
List<QuestionSubmit> questionSubmitList = questionSubmitPage.getRecords();
Page<QuestionSubmitVO> questionSubmitVOPage = new Page<>(questionSubmitPage.getCurrent(), questionSubmitPage.getSize(), questionSubmitPage.getTotal());
if (CollectionUtils.isEmpty(questionSubmitList)) {
return questionSubmitVOPage;
}
List<QuestionSubmitVO> questionSubmitVOList = questionSubmitList.stream()
.map(questionSubmit -> getQuestionSubmitVO(questionSubmit, loginUser))
.collect(Collectors.toList());
questionSubmitVOPage.setRecords(questionSubmitVOList);
return questionSubmitVOPage;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
9)编写 QuestionVO 的 json / 对象转换工具类
/**
* 包装类转对象
*
* @param questionSubmitVO
* @return
*/
public static QuestionSubmit voToObj(QuestionSubmitVO questionSubmitVO) {
if (questionSubmitVO == null) {
return null;
}
QuestionSubmit questionSubmit = new QuestionSubmit();
BeanUtils.copyProperties(questionSubmitVO, questionSubmit);
JudgeInfo judgeInfoObj = questionSubmitVO.getJudgeInfo();
if (judgeInfoObj != null) {
questionSubmit.setJudgeInfo(JSONUtil.toJsonStr(judgeInfoObj));
}
return questionSubmit;
}
/**
* 对象转包装类
*
* @param questionSubmit
* @return
*/
public static QuestionSubmitVO objToVo(QuestionSubmit questionSubmit) {
if (questionSubmit == null) {
return null;
}
QuestionSubmitVO questionSubmitVO = new QuestionSubmitVO();
BeanUtils.copyProperties(questionSubmit, questionSubmitVO);
String judgeInfoStr = questionSubmit.getJudgeInfo();
questionSubmitVO.setJudgeInfo(JSONUtil.toBean(judgeInfoStr, JudgeInfo.class));
return questionSubmitVO;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 包装类转对象
*
* @param questionVO
* @return
*/
public static Question voToObj(QuestionVO questionVO) {
if (questionVO == null) {
return null;
}
Question question = new Question();
BeanUtils.copyProperties(questionVO, question);
List<String> tagList = questionVO.getTags();
if (tagList != null) {
question.setTags(JSONUtil.toJsonStr(tagList));
}
JudgeConfig voJudgeConfig = questionVO.getJudgeConfig();
if (voJudgeConfig != null) {
question.setJudgeConfig(JSONUtil.toJsonStr(voJudgeConfig));
}
return question;
}
/**
* 对象转包装类
*
* @param question
* @return
*/
public static QuestionVO objToVo(Question question) {
if (question == null) {
return null;
}
QuestionVO questionVO = new QuestionVO();
BeanUtils.copyProperties(question, questionVO);
List<String> tagList = JSONUtil.toList(question.getTags(), String.class);
questionVO.setTags(tagList);
String judgeConfigStr = question.getJudgeConfig();
questionVO.setJudgeConfig(JSONUtil.toBean(judgeConfigStr, JudgeConfig.class));
return questionVO;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
10)用同样的方法,编写 questionSubmit 提交类,这次参考 postThumb 相关文件
11)编写枚举类
public enum QuestionSubmitStatusEnum {
// 0 - 待判题、1 - 判题中、2 - 成功、3 - 失败
WAITING("等待中", 0),
RUNNING("判题中", 1),
SUCCEED("成功", 2),
FAILED("失败", 3);
private final String text;
private final Integer value;
QuestionSubmitStatusEnum(String text, Integer value) {
this.text = text;
this.value = value;
}
/**
* 获取值列表
*
* @return
*/
public static List<Integer> getValues() {
return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());
}
/**
* 根据 value 获取枚举
*
* @param value
* @return
*/
public static QuestionSubmitStatusEnum getEnumByValue(Integer value) {
if (ObjectUtils.isEmpty(value)) {
return null;
}
for (QuestionSubmitStatusEnum anEnum : QuestionSubmitStatusEnum.values()) {
if (anEnum.value.equals(value)) {
return anEnum;
}
}
return null;
}
public Integer getValue() {
return value;
}
public String getText() {
return text;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 题目提交编程语言枚举
*
*/
public enum QuestionSubmitLanguageEnum {
JAVA("java", "java"),
CPLUSPLUS("c++", "c++"),
GOLANG("golang", "golang");
private final String text;
private final String value;
QuestionSubmitLanguageEnum(String text, String value) {
this.text = text;
this.value = value;
}
/**
* 获取值列表
*
* @return
*/
public static List<String> getValues() {
return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());
}
/**
* 根据 value 获取枚举
*
* @param value
* @return
*/
public static QuestionSubmitLanguageEnum getEnumByValue(String value) {
if (ObjectUtils.isEmpty(value)) {
return null;
}
for (QuestionSubmitLanguageEnum anEnum : QuestionSubmitLanguageEnum.values()) {
if (anEnum.value.equals(value)) {
return anEnum;
}
}
return null;
}
public String getValue() {
return value;
}
public String getText() {
return text;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import org.apache.commons.lang3.ObjectUtils;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public enum JudgeInfoMessageEnum {
ACCEPTED("成功", "Accepted"),
WRONG_ANSWER("答案错误", "Wrong Answer"),
COMPILE_ERROR("Compile Error", "编译错误"),
MEMORY_LIMIT_EXCEEDED("", "内存溢出"),
TIME_LIMIT_EXCEEDED("Time Limit Exceeded", "超时"),
PRESENTATION_ERROR("Presentation Error", "展示错误"),
WAITING("Waiting", "等待中"),
OUTPUT_LIMIT_EXCEEDED("Output Limit Exceeded", "输出溢出"),
DANGEROUS_OPERATION("Dangerous Operation", "危险操作"),
RUNTIME_ERROR("Runtime Error", "运行错误"),
SYSTEM_ERROR("System Error", "系统错误");
private final String text;
private final String value;
JudgeInfoMessageEnum(String text, String value) {
this.text = text;
this.value = value;
}
/**
* 获取值列表
*
* @return
*/
public static List<String> getValues() {
return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());
}
/**
* 根据 value 获取枚举
*
* @param value
* @return
*/
public static JudgeInfoMessageEnum getEnumByValue(String value) {
if (ObjectUtils.isEmpty(value)) {
return null;
}
for (JudgeInfoMessageEnum anEnum : JudgeInfoMessageEnum.values()) {
if (anEnum.value.equals(value)) {
return anEnum;
}
}
return null;
}
public String getValue() {
return value;
}
public String getText() {
return text;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
编写好基本代码后,记得通过 Swagger 或者编写单元测试去验证。
# 小知识
为了防止用户按照 id 顺序爬取题目,建议把 id 的生成规则改为 ASSIGN_ID 而不是从 1 开始自增,示例代码如下:
/**
* id
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
2
3
4
5
# 分页获取题目提交列表
/**
* 分页获取题目提交列表(除了管理员外,普通用户只能看到非答案、提交代码等公开信息)
*
* @param questionSubmitQueryRequest
* @param request
* @return
*/
@PostMapping("/list/page")
public BaseResponse<Page<QuestionSubmitVO>> listQuestionSubmitByPage(@RequestBody QuestionSubmitQueryRequest questionSubmitQueryRequest,
HttpServletRequest request) {
long current = questionSubmitQueryRequest.getCurrent();
long size = questionSubmitQueryRequest.getPageSize();
// 从数据库中查询原始的题目提交分页信息
Page<QuestionSubmit> questionSubmitPage = questionSubmitService.page(new Page<>(current, size),
questionSubmitService.getQueryWrapper(questionSubmitQueryRequest));
final User loginUser = userService.getLoginUser(request);
// 返回脱敏信息
return ResultUtils.success(questionSubmitService.getQuestionSubmitVOPage(questionSubmitPage, loginUser));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 查询提交信息接口
功能:能够根据用户 id、或者题目 id、编程语言、题目状态,去查询提交记录
注意事项:
仅本人和管理员能看见自己(提交 userId 和登录用户 id 不同)提交的代码
实现方案:先查询,再根据权限去脱敏
核心代码:
@Override
public QuestionSubmitVO getQuestionSubmitVO(QuestionSubmit questionSubmit, User loginUser) {
QuestionSubmitVO questionSubmitVO = QuestionSubmitVO.objToVo(questionSubmit);
// 脱敏:仅本人和管理员能看见自己(提交 userId 和登录用户 id 不同)提交的代码
long userId = loginUser.getId();
// 处理脱敏
if (userId != questionSubmit.getUserId() && !userService.isAdmin(loginUser)) {
questionSubmitVO.setCode(null);
}
return questionSubmitVO;
}
2
3
4
5
6
7
8
9
10
11
12
# 后端判题机开发
判题模块:调用代码沙箱,把代码和输入交给代码沙箱去执行
代码沙箱:只负责接收代码和输入,返回编译运行的结果,不负责判题(可以作为独立的项目/服务,提供给
其它的需要执行代码的项目去使用)
这两个模块完全解耦
思考:为什么代码沙箱要接受和输出一组运行用例
如果是每个用例单独调用一次代码沙箱,会调用多次接口,需要多次网络传输、程序要多次编译、记录程
序的执行状态(重复的代码不用重复编译)
这是一种很常见的性能优化的方法!(类似于批量处理)
# 开发代码沙箱模块
1)新建一个judge.codesandbox文件夹
2)创建CodeSandbox接口
3)创建executeCode方法接口(执行代码)
/**
* 响应对象 和 接收对象
*/
ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest);
2
3
4
这里定义接口主要是为了以后要使用其他的代码沙箱我们就可以直接实现接口就好了,不需要去调用实现类,方便扩展
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ExecuteCodeRequest {
/**
* 接收前端的代码
*/
private String code;
/**
* 输入用例
*/
private List<String> inputList;
/**
* 语言
*/
private String language;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ExecuteCodeResponse {
/**
* 输出用例
*/
private List<String> outputList;
/**
* 接口信息
*/
private String message;
/**
* 执行状态
*/
private String status;
/**
* 判题信息
*/
private JudgeInfo judgeInfo;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
4)实现代码沙箱接口
/**
* @ Author: 李某人
* @ Date: 2024/11/28/21:39
* @ Description: 实例代码沙箱(仅仅是用来跑通测试用例的)
*/
public class ExampleCodeSandbox implements CodeSandbox {
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
System.out.println("实例代码沙箱");
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
/**
* @ Author: 李某人
* @ Date: 2024/11/28/21:46
* @ Description: 远程代码沙箱(实际要调用的沙箱)
*/
public class RemoteCodeSandbox implements CodeSandbox {
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
System.out.println("远程代码沙箱");
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
5)测试跑通代码流程
@Test
void contextLoads() {
CodeSandbox codeSandbox = new ExampleCodeSandbox();
String code = "int main(){}";
String language = QuestionSubmitLanguageEnum.JAVA.getValue();
List<String> inputList = Arrays.asList("1 2", "3 4");
ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
.code(code)
.language(language)
.inputList(inputList)
.build();
ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);
Assertions.assertNotNull(executeCodeResponse);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
现在的问题是,我们把new某个沙箱的代码写死了,如果后面项目要改用其他沙箱,可能要改很多地方的代码
接下来我们可以使用工厂模式来优化一下
使用工程模式根据用户传入的字符串参数,来生成对应的接口对象
# 工厂模式优化
/**
* @ Author: 李某人
* @ Date: 2024/12/01/22:37
* @ Description: 静态工厂(还有单例工厂 和 抽象工厂等等,大家可以自己去查)
* 扩展思路:如果确定代码沙箱示例不会出现线程问题,可复用,那么可以使用单例工厂模式
*/
public class CodeSandboxFactory {
/**
* 静态工厂:顾名思义就是一个静态方法
*/
public static CodeSandbox newInstance(String type){
switch (type){
case "example":
return new ExampleCodeSandbox();
case "remote":
return new RemoteCodeSandbox();
default:
return new ExampleCodeSandbox();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
参数配置化,把项目中一些可以交给用户去自定义的选项或字符串,写到配置文件中,这样开发者只需要
改配置文件,而不需要去看你的项目代码,就能够自定义使用你项目的更多功能
application.yml配置文件中指定变量
codesandbox:
value: example
2
//创建代码沙箱
//设计模式:工厂模式
CodeSandbox codeSandbox = CodeSandboxFactory.newInstance(codesandboxValue);
//接收前端的参数
//模拟一些数据
String code = "int main(){}";
List<String> inputList = Arrays.asList("1 2", "3 4");
String language = QuestionSubmitLanguageEnum.JAVA.getValue();
ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
.code(code)
.language(language)
.inputList(inputList)
.build();
//执行代码沙箱
ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);
//判断是否为空
Assertions.assertNotNull(executeCodeResponse);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 代理模式
比如:我们需要再调用代码沙箱前,输出请求参数日志,在代码沙箱调用后,输出响应结果日志,便于管理去分析
每个代码沙箱都写一遍log.info?难道每次调用代码沙箱前后 都执行log?
使用代理模式,使用Proxy,来增强代码沙箱的能力(代码模式的作用就是增强能力)
原本:需要用户自己去调用多次
使用代理后,不仅不用改变原来的代码沙箱实现类,而且对调用者来说,调用方式几乎没有改变,也不需要在每个调用代码的地方去写日志代码
代理模式的实现原理:
1、实现被代理的接口
2、通过构造函数接受一个被代理的接口实现类
3、调用被代理的接口实现类,在调用前后增加对应的操作
@Slf4j
public class CodeSandboxProxy implements CodeSandbox {
private final CodeSandbox codeSandbox;
public CodeSandboxProxy(CodeSandbox codeSandbox) {
this.codeSandbox = codeSandbox;
}
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
log.info("代码沙箱请求信息:" + executeCodeRequest.toString());
ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);
log.info("代码沙箱响应信息:" + executeCodeResponse.toString());
return executeCodeResponse;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用方式
CodeSandbox codeSandbox = CodeSandboxFactory.newInstance(type);
codeSandbox = new CodeSandboxProxy(codeSandbox);
2
3
/**
* 示例代码沙箱(仅为了跑通业务流程)
*/
@Slf4j
public class ExampleCodeSandbox implements CodeSandbox {
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
List<String> inputList = executeCodeRequest.getInputList();
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
executeCodeResponse.setOutputList(inputList);
executeCodeResponse.setMessage("测试执行成功");
executeCodeResponse.setStatus(QuestionSubmitStatusEnum.SUCCEED.getValue());
JudgeInfo judgeInfo = new JudgeInfo();
judgeInfo.setMessage(JudgeInfoMessageEnum.ACCEPTED.getText());
judgeInfo.setMemory(100L);
judgeInfo.setTime(100L);
executeCodeResponse.setJudgeInfo(judgeInfo);
return executeCodeResponse;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 判题服务完整业务流程实现
新建一个judgeservice
public interface JudgeService{
QuestionSubmitVO doJudge(long questionSubmitId);
}
2
3
然后实现类
@Autowire
private QuestionService questionService;
@Autowire
private QuestionSubmitService questionSubmitService;
@Override
private QuestionSubmitVO doJudge(long questionSubmitId){
//1.传入题目的提交id,获取到对应的题目,提交信息(代码,编程语言等)
QuestionSubmit questionSubmit = questionSubmitService.getById(questionSubmitId);
if(questionSubmit==null){
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR,"提交信息不存在");
}
long questionId = questionSubmit.getQuestionId();
Question question = questionService.getById(questionId);
if(question==null){
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR,"题目不存在");
}
//如果不为等待状态
if(!questionSubmit.getStatus().equals(QuestionSubmitStatusEnum.WAITING.getValue())){
throw new BusinessException(ErrorCode.OPERATION_ERROR,"题目正在判题中");
}
//更改判题的状态为判题中,防止重复提交
QuestionSubmit questionSubmitUpate = new QuestionSubmit();
questionSubmitUpate.setId(questionSubmitId);
questionSubmitUpate.setStatus(QuestionSubmitStatusEnum.RUNNING.getValue());
boolean update = questionSubmitService.updateById(questionSubmitUpadte);
if(!update){
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"题目状态更新错误");
}
//调用代码沙箱
CodeSandbox codeSandbox = CodeSandboxFactory.newInstance(type);
codeSandbox = new CodeSanboxProxy(codeSandbox);
String language = questionSubmit.getLanguage();
String code = questionSubmit.getCode();
//获取输入用例
String judgeCaseStr = question.getJudgeCase();
List<JudgeCase> judgeCaseList = JSONUtils.toList(judgeCaseStr,JudgeCase.class);
List<String> inputList = judgeCaseList.steam().map(JudgeCase::getInput).collect(Collectors.toList());
ExectuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder().
code(code)
.language(language)
.inputList(inputList)
.build();
ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);
List<String> outputList = executeCodeResponse.getOutputList();
//根据沙箱的执行结果,设置题目的判题状态和信息
JudgeInfoMessageEnum judgeInfoMessageEnum = JudgeInfoMessageEnum.WAITING;
if(outputList.size()!=inputList.size()){
judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
return null;
}
for(int i = 0; i<judgeCaseList.size();i++){
JudgeCase judgeCase = judgeCaseList.get(i);
if(!judgeCase.getOutput().equals(outputList.get(i))){
judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
return null;
}
}
//判断题目限制
JudgeInfo judgeInfo = executeCodeResponse.getJudgeInfo();
Long memory = judgeInfo.getMemory();
Long time = judgeInfo.getTime();
String judgeConfigStr = question.getJudgeConfig();
JudgeConfig judgeConfig = JSONUtil.toBean(judgeConfigStr,JudgeConfig.class);
Long needMemoryLimit = judgeConfig.getMemoryLimit();
Long needTimeLimit = judgeConfig.getTimeLimit();
if(memory > needMemoryLimit){
judgeInfoMessageEnum = JudgeInfoMessageEnum.MEMORY_LIMIT_EXCEEDED;
return null;
}
if(time > needTimeLimit){
judgeInfoMessageEnum = JudgeInfoMessageEnum.TIME_LIMIT_EXCEEDED;
return null;
}
return null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
我们这里可以使用策略模式,针对不同的情况,定义独立的策略,而不是把所有的判题逻辑都写在一起
# 策略模式
1.先定义一个判题策略模式的接口
strategy.JudgeStrategy
public interface JudgeStrategy{
//这里可以用一个上下文的概念
//执行判题
JudgeInfo doJudge(JudgeContext judgeContext);
}
2
3
4
5
JudgeContext
@Data
public class JudgeContext{
private JudgeInfo judgeInfo;
private List<String> inputList;
private List<String> outputList;
private List<JudgeCase> judgeCaseList;
private Question question;
}
2
3
4
5
6
7
8
上下文就是一个用于传递的参数
现在来一个默认策略
DefaultJudgeStrategy
public class DefaultJudgeStrategy implements JudgeContext{
@Override
public JudgeInfo doJudge(JudgeContext judgeContext) {
JudgeInfo judgeInfo = judgeContext.getJudgeInfo();
List<String> inputList = judgeContext.getInputList();
List<String> outputList = judgeContext.getOutputList();
List<JudgeCase> judgeCaseList = judgeContext.getJudgeCaseList();
Question question = judgeContext.getQuestion();
Long memory = judgeInfo.getMemory();
Long time = judgeInfo.getTime();
JudgeInfoMessageEnum judgeInfoMessageEnum = JudgeInfoMessageEnum.ACCEPTED;
JudgeInfo judgeInfoResponse = new JudgeInfo();
judgeInfoResponse.setMemory(memory);
judgeInfoResponse.setTime(time);
if (outputList.size()!=inputList.size()){
judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
return judgeInfoResponse;
}
for (int i = 0 ; i<judgeCaseList.size();i++){
JudgeCase judgeCase = judgeCaseList.get(i);
if (!judgeCase.getOutput().equals(outputList.get(i))){
judgeInfoMessageEnum = JudgeInfoMessageEnum.WRONG_ANSWER;
judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
return judgeInfoResponse;
}
}
//执行完之后的时间
String judgeConfigStr = question.getJudgeConfig();
JudgeConfig judgeConfig = JSONUtil.toBean(judgeConfigStr, JudgeConfig.class);
//题目要求的时间
Long timeLimit = judgeConfig.getTimeLimit();
Long memoryLimit = judgeConfig.getMemoryLimit();
if(memory > memoryLimit){
judgeInfoMessageEnum = JudgeInfoMessageEnum.MEMORY_LIMIT_EXCEEDED;
judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
return judgeInfoResponse;
}
if(time > timeLimit){
judgeInfoMessageEnum = JudgeInfoMessageEnum.TIME_LIMIT_EXCEEDED;
judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
return judgeInfoResponse;
}
judgeInfoResponse.setMessage(judgeInfoMessageEnum.getValue());
return judgeInfoResponse;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
再回到判题service里面
@Override
public QuestionSubmit doJudge(long questionSubmitId) {
//1.传入题目的提交id,获取到对应的题目,提交信息(代码,编程语言等)
QuestionSubmit questionSubmit = questionSubmitService.getById(questionSubmitId);
if (questionSubmit == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR,"提交信息不存在");
}
Long questionId = questionSubmit.getQuestionId();
Question question = questionService.getById(questionId);
if(question==null){
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR,"题目不存在");
}
//判断题目是否在等待中,如果不是在等待就说明在判题
if (!questionSubmit.getStatus().equals(QuestionSubmitStatusEnum.WAITING.getValue())){
throw new BusinessException(ErrorCode.OPERATION_ERROR,"题目正在判题中");
}
//将题目的状态设置成判题中 防止重复提交
QuestionSubmit questionSubmitUpdate = new QuestionSubmit();
questionSubmitUpdate.setId(questionId);
questionSubmitUpdate.setStatus(QuestionSubmitStatusEnum.RUNNING.getValue());
//通过数据库进行修改
boolean update = questionSubmitService.updateById(questionSubmitUpdate);
//判断当前代码是否修改成功
if (!update){
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"题目状态更新错误");
}
//调用代码沙箱
CodeSandbox codeSandbox = CodeSandboxFactory.newInstance(type);
codeSandbox = new CodeSandboxProxy(codeSandbox);
//接收前端的参数
//模拟一些数据
String code = questionSubmit.getCode();
String language = questionSubmit.getLanguage();
String judgeCaseStr = question.getJudgeCase();
List<JudgeCase> judgeCaseList = JSONUtil.toList(judgeCaseStr, JudgeCase.class);
List<String> inputList = judgeCaseList.stream().map(JudgeCase::getInput).collect(Collectors.toList());
ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
.code(code)
.language(language)
.inputList(inputList)
.build();
//执行代码沙箱
ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);
//根据沙箱的执行结果,设置题目的判题状态和信息
//输出用例要跟输入用例
List<String> outputList = executeCodeResponse.getOutputList();
//设置一下上下文的值
JudgeContext judgeContext = new JudgeContext();
judgeContext.setJudgeInfo(executeCodeResponse.getJudgeInfo());
judgeContext.setInputList(inputList);
judgeContext.setOutputList(outputList);
judgeContext.setJudgeCaseList(judgeCaseList);
judgeContext.setQuestion(question);
JudgeStrategy judgeStrategy = new DefaultJudgeStrategy();
JudgeInfo judgeInfo = judgeStrategy.doJudge(judgeContext);
//修改数据库中的判题结果
questionSubmitUpdate = new QuestionSubmit();
questionSubmitUpdate.setId(questionSubmitId);
questionSubmitUpdate.setStatus(QuestionSubmitStatusEnum.RUNNING.getValue());
questionSubmitUpdate.setJudgeInfo(JSONUtil.toJsonStr(judgeInfo));
update = questionSubmitService.updateById(questionSubmitUpdate);
//判断当前代码是否修改成功
if (!update){
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"题目状态更新错误");
}
//再从数据库中查询一下状态
QuestionSubmit questionSubmitServiceById = questionSubmitService.getById(questionSubmitId);
return questionSubmitServiceById;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
如果选择某种判题策略的过程比较复杂,都写在调用判题服务的代码中,代码会越来越复杂,会有很多if...else ,所以呢,建议单独编写一个判断策略的类
JudgeManager
//判题管理简化调用
@Service
public class JudgeManager{
JudgeInfo doJudge(JudgeContext judgeContext){
QuestionSubmit questionSubmit = judgeContext.getQuestionSubmit();
String language = questionSubmit.getLanguage();
JudgeStrategy judgeStrategy = new DefaultJudgeStrategy();
if("java".equals(language)){
judgeStrategy = new JavaLanguageJudgeStrategy();
}
return judgeStrategy.doJudge(judgeContext);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
@Data
public class JudgeContext{
private JudgeInfo judgeInfo;
private List<String> inputList;
private List<String> outputList;
private List<JudgeCase> judgeCaseList;
private Question question;
private QuestionSubmit questionSubmit;//新增questionSubmit对象
}
2
3
4
5
6
7
8
9
//然后再设置上下文的地方去设置
judgeContext.QuestionSubmit(questionSubmit);
2
//最后修改
JudgeInfo judgeInfo = judgeManager.doJudge(judgeContext);
2
最后修改一下QuestionSubmitServiceImpl
@Resource
@Lazy
private JudgeService judgeService;
//判题服务
Long questionSubmitId = questionSubmit.getId();
CompletableFuture.runAsync(()->{
judgeService.doJudge(questionSubmitId);
});
return questionSubmitId;
2
3
4
5
6
7
8
9
10
11
# Java语言实现代码沙箱
首先需要新建一个项目
这里建议选择jdk8 和 springboot 2.7.14版本的
编写启动配置
server:
port: 8090
2
首先把之前写好的CodeSandbox的接口拿到沙箱模块来
还有judgeInfo类
代码沙箱需要:接收代码->编译代码(javac)->执行代码(java)
我们先通过控制台的方式去执行代码
public class SimpleCompute {
public static void main(String[] args) {
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
System.out.println("结果:" + (a + b));
}
}
2
3
4
5
6
7
进入到simpleComputeArgs目录下
javac -encoding utf-8 .\SimpleCompute.java
java -cp . SimpleCompute
java -cp . SimpleCompute 1 2
2
3
实际OJ系统中,对用户输入的代码会有一定的要求,便于系统统一处理,我们把用户输入代码的类名限制为Main(参照蓝桥云课或者清华OJ)
public class Main {
public static void main(String[] args) {
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
System.out.println("结果:" + (a + b));
}
}
2
3
4
5
6
7
实际执行
javac -encoding utf-8 .\Main.java
java -cp . Main 1 2
2
# 核心流程实现
/**
* Java 原生代码沙箱实现(直接复用模板方法)
*/
@Component
public class JavaNativeCodeSandbox extends JavaCodeSandboxTemplate {
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
return null;
}
}
2
3
4
5
6
7
8
9
10
11
Java进程管理类 Process
1.把用户的代码保存为文件
2.编译代码,得到class文件
3.执行代码,得到输出结果
4.收集整理输出结果
5.文件清理
6.错误处理,提高程序健壮性
1)先在项目下创建一个tmpCode文件夹,用来存放用户写的代码文件,记得引入hutool工具类
# 把用户的代码保存为文件
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.8</version>
</dependency>
2
3
4
5
private static final String GLOBAL_CODE_DIR_NAME = "tmpCode";
private static final String GLOBAL_JAVA_CLASS_NAME = "Main.java";
public static void main(String[] args) {
JavaNativeCodeSandboxOld javaNativeCodeSandbox = new JavaNativeCodeSandboxOld();
ExecuteCodeRequest executeCodeRequest = new ExecuteCodeRequest();
executeCodeRequest.setInputList(Arrays.asList("1 2", "1 3"));
String code = ResourceUtil.readStr("testCode/simpleComputeArgs/Main.java", StandardCharsets.UTF_8);
// String code = ResourceUtil.readStr("testCode/simpleCompute/Main.java", StandardCharsets.UTF_8);
executeCodeRequest.setCode(code);
executeCodeRequest.setLanguage("java");
ExecuteCodeResponse executeCodeResponse = javaNativeCodeSandbox.executeCode(executeCodeRequest);
System.out.println(executeCodeResponse);
}
/**
* Java 原生代码沙箱实现(直接复用模板方法)
*/
@Component
public class JavaNativeCodeSandbox extends JavaCodeSandboxTemplate {
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
//1. 把用户的代码保存为文件
//获取到当前用户的工作目录
String userDir = System.getProperty("user.dir");
//File.separator为了兼容不同系统的\
String globalCodePathName = userDir + File.separator + GLOBAL_CODE_DIR_NAME;
// 判断全局代码目录是否存在,没有则新建
if (!FileUtil.exist(globalCodePathName)) {
FileUtil.mkdir(globalCodePathName);
}
//把用户的代码隔离存放
String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID();
//实际的存放路径
String userCodePath = userCodeParentPath + File.separator + GLOBAL_JAVA_CLASS_NAME;
//写入程序中
File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 编译代码,得到class文件
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
//1. 把用户的代码保存为文件
//获取到当前用户的工作目录
String userDir = System.getProperty("user.dir");
//File.separator为了兼容不同系统的\
String globalCodePathName = userDir + File.separator + GLOBAL_CODE_DIR_NAME;
// 判断全局代码目录是否存在,没有则新建
if (!FileUtil.exist(globalCodePathName)) {
FileUtil.mkdir(globalCodePathName);
}
//把用户的代码隔离存放
String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID();
//实际的存放路径
String userCodePath = userCodeParentPath + File.separator + GLOBAL_JAVA_CLASS_NAME;
//写入程序中
File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);
//2.编译代码,得到 class 文件
String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());
try {
Process compileProcess = Runtime.getRuntime().exec(compileCmd);
ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
System.out.println(executeMessage);
} catch (Exception e) {
return getErrorResponse(e);
}
return null;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
获取异常信息输出
ProcessUtils
import cn.hutool.core.util.StrUtil;
import com.lanqiao.lanqiaodemosandbox.model.ExecuteMessage;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.StopWatch;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
/**
* 进程工具类
*/
public class ProcessUtils {
/**
* 执行进程并获取信息
*
* @param runProcess
* @param opName
* @return
*/
public static ExecuteMessage runProcessAndGetMessage(Process runProcess, String opName) {
ExecuteMessage executeMessage = new ExecuteMessage();
try {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 等待程序执行,获取错误码
int exitValue = runProcess.waitFor();
executeMessage.setExitValue(exitValue);
// 正常退出
if (exitValue == 0) {
System.out.println(opName + "成功");
// 分批获取进程的正常输出
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(runProcess.getInputStream()));
List<String> outputStrList = new ArrayList<>();
// 逐行读取
String compileOutputLine;
while ((compileOutputLine = bufferedReader.readLine()) != null) {
outputStrList.add(compileOutputLine);
}
executeMessage.setMessage(StringUtils.join(outputStrList, "\n"));
} else {
// 异常退出
System.out.println(opName + "失败,错误码: " + exitValue);
// 分批获取进程的正常输出
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(runProcess.getInputStream()));
List<String> outputStrList = new ArrayList<>();
// 逐行读取
String compileOutputLine;
while ((compileOutputLine = bufferedReader.readLine()) != null) {
outputStrList.add(compileOutputLine);
}
executeMessage.setMessage(StringUtils.join(outputStrList, "\n"));
// 分批获取进程的错误输出
BufferedReader errorBufferedReader = new BufferedReader(new InputStreamReader(runProcess.getErrorStream()));
// 逐行读取
List<String> errorOutputStrList = new ArrayList<>();
// 逐行读取
String errorCompileOutputLine;
while ((errorCompileOutputLine = errorBufferedReader.readLine()) != null) {
errorOutputStrList.add(errorCompileOutputLine);
}
executeMessage.setErrorMessage(StringUtils.join(errorOutputStrList, "\n"));
}
stopWatch.stop();
executeMessage.setTime(stopWatch.getLastTaskTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}
return executeMessage;
}
/**
* 执行交互式进程并获取信息
*
* @param runProcess
* @param args
* @return
*/
public static ExecuteMessage runInteractProcessAndGetMessage(Process runProcess, String args) {
ExecuteMessage executeMessage = new ExecuteMessage();
try {
// 向控制台输入程序
OutputStream outputStream = runProcess.getOutputStream();
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
String[] s = args.split(" ");
String join = StrUtil.join("\n", s) + "\n";
outputStreamWriter.write(join);
// 相当于按了回车,执行输入的发送
outputStreamWriter.flush();
// 分批获取进程的正常输出
InputStream inputStream = runProcess.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder compileOutputStringBuilder = new StringBuilder();
// 逐行读取
String compileOutputLine;
while ((compileOutputLine = bufferedReader.readLine()) != null) {
compileOutputStringBuilder.append(compileOutputLine);
}
executeMessage.setMessage(compileOutputStringBuilder.toString());
// 记得资源的释放,否则会卡死
outputStreamWriter.close();
outputStream.close();
inputStream.close();
runProcess.destroy();
} catch (Exception e) {
e.printStackTrace();
}
return executeMessage;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import lombok.Data;
/**
* 进程执行信息
*/
@Data
public class ExecuteMessage {
private Integer exitValue;
private String message;
private String errorMessage;
private Long time;
private Long memory;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 执行代码
// 3. 执行代码,得到输出结果
for (String inputArgs : inputList) {
String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeParentPath, inputArgs);
try {
ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(runProcess, "运行");
System.out.println(executeMessage);
}catch (Exception e) {
return getErrorResponse(e);
}
}
2
3
4
5
6
7
8
9
10
11
# 整理输出
List<ExecuteMessage> executeMessageList = new ArrayList<>();//存储输出信息
for (String inputArgs : inputList) {
String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeParentPath, inputArgs);
try {
ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(runProcess, "运行");
System.out.println(executeMessage);
executeMessageList.add(executeMessage);
}catch (Exception e) {
return getErrorResponse(e);
}
}
//4.收集整理输出结果
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
List<String> outputList = new ArrayList<>();
// 取用时最大值,便于判断是否超时
long maxTime = 0;
for (ExecuteMessage executeMessage : executeMessageList) {
String errorMessage = executeMessage.getErrorMessage();
//错误信息不为空
if (StrUtil.isNotBlank(errorMessage)) {
executeCodeResponse.setMessage(errorMessage);
// 用户提交的代码执行中存在错误
executeCodeResponse.setStatus(3);
break;
}
outputList.add(executeMessage.getMessage());
Long time = executeMessage.getTime();
if (time != null) {
maxTime = Math.max(maxTime, time);
}
}
// 正常运行完成
if (outputList.size() == executeMessageList.size()) {
executeCodeResponse.setStatus(1);
}
executeCodeResponse.setOutputList(outputList);
JudgeInfo judgeInfo = new JudgeInfo();
judgeInfo.setTime(maxTime);
// 要借助第三方库来获取内存占用,非常麻烦,此处不做实现
// judgeInfo.setMemory();
executeCodeResponse.setJudgeInfo(judgeInfo);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 文件清理
if (userCodeFile.getParentFile() != null) {
boolean del = FileUtil.del(userCodeParentPath);
System.out.println("删除" + (del ? "成功" : "失败"));
}
return executeCodeResponse;
2
3
4
5
# 通用的错误处理
有可能编译代码的时候就已经报错了
/**
* 获取错误响应
*
* @param e
* @return
*/
private ExecuteCodeResponse getErrorResponse(Throwable e) {
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
executeCodeResponse.setOutputList(new ArrayList<>());
executeCodeResponse.setMessage(e.getMessage());
// 表示代码沙箱错误
executeCodeResponse.setStatus(2);
executeCodeResponse.setJudgeInfo(new JudgeInfo());
return executeCodeResponse;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 目前的完整代码
package cn.lanqiao.lanqiaocodesandbox;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.StrUtil;
import cn.lanqiao.lanqiaocodesandbox.model.ExecuteCodeRequest;
import cn.lanqiao.lanqiaocodesandbox.model.ExecuteCodeResponse;
import cn.lanqiao.lanqiaocodesandbox.model.ExecuteMessage;
import cn.lanqiao.lanqiaocodesandbox.model.JudgeInfo;
import cn.lanqiao.lanqiaocodesandbox.utils.ProcessUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @ Author: 李某人
* @ Date: 2024/12/10/21:57
* @ Description:
*/
public class JavaNativeCodeSandbox implements CodeSandbox{
public static void main(String[] args) {
JavaNativeCodeSandbox javaNativeCodeSandbox = new JavaNativeCodeSandbox();
ExecuteCodeRequest executeCodeRequest = new ExecuteCodeRequest();
executeCodeRequest.setInputList(Arrays.asList("1 2","3 4"));
//传入用户写的代码
String code = ResourceUtil.readStr("testCode/simpleComputeArgs/Main.java", StandardCharsets.UTF_8);
executeCodeRequest.setCode(code);
executeCodeRequest.setLanguage("java");
ExecuteCodeResponse executeCodeResponse = javaNativeCodeSandbox.executeCode(executeCodeRequest);
System.out.println(executeCodeResponse);
}
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
/**
* 1.使用Java代码原生判题(不使用任何框架实现)
* 2.使用docker沙箱判题
*
* 1) 获取到用户的代码,然后将用户的代码保存为文件
* 2) 编译代码,得到class文件
* 3) 执行代码,得到输出结果
* 4) 收集整理输出结果
* 5) 文件清理
* 6) 错误处理,提高程序的健壮性
*/
String code = executeCodeRequest.getCode();
String language = executeCodeRequest.getLanguage();
List<String> inputList = executeCodeRequest.getInputList();
//1) 获取到用户的代码,然后将用户的代码保存为文件
//获取到当前用户的工作目录
String userDir = System.getProperty("user.dir");
//File.separator 是为了兼容不同的系统的 \
String globalCodePathName = userDir + File.separator + "tmpCode";
//判断全局代码目录是否存在,没有则新建
if (!FileUtil.exist(globalCodePathName)){
FileUtil.mkdir(globalCodePathName);
}
//将用户提交的代码隔离
String userCodeParentPath = globalCodePathName+File.separator+ UUID.randomUUID();
//真正的用户路径
String userCodePath = userCodeParentPath + File.separator + "Main.java";
//直接写入到程序中
File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);
//2) 编译代码,得到class文件
String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsoluteFile());
try {
Process compileProcess = Runtime.getRuntime().exec(compileCmd);
ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
System.out.println(executeMessage);
} catch (Exception e) {
return getErrorResponse(e);
}
//3) 执行代码,得到输出结果
//首先拿到输入用例进行遍历
List<ExecuteMessage> executeMessageList = new ArrayList<>();//用来存储输出信息
for(String inputArgs:inputList){
String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeParentPath, inputArgs);
try {
Process runProcess = Runtime.getRuntime().exec(runCmd);
ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(runProcess, "运行");
System.out.println(executeMessage);
executeMessageList.add(executeMessage);
} catch (IOException e) {
return getErrorResponse(e);
}
}
//4) 收集整理输出结果
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
List<String> outputList = new ArrayList<>();
//模拟一个最大值,便于判断是否超时
long maxTime = 0;
for (ExecuteMessage executeMessage : executeMessageList) {
String errorMessage = executeMessage.getErrorMessage();
if (StrUtil.isNotBlank(errorMessage)){
executeCodeResponse.setMessage(errorMessage);
//用户提交的代码执行存在错误
executeCodeResponse.setStatus(3);
break;
}
outputList.add(executeMessage.getMessage());
Long time = executeMessage.getTime();//程序执行时间
if (time!=null){
maxTime = Math.max(maxTime, time);
}
}
//正常运行结束
if (outputList.size() == executeMessageList.size()){
executeCodeResponse.setStatus(1);
}
executeCodeResponse.setOutputList(outputList);
JudgeInfo judgeInfo = new JudgeInfo();
judgeInfo.setMessage(outputList);
//要借助第三方库来进行实现,非常麻烦就不实现了,当然你可以自己去网上搜索一下
// judgeInfo.setMemory();
judgeInfo.setTime(maxTime);
//5) 文件清理
if (userCodeFile.getParentFile()!=null){
boolean del = FileUtil.del(userCodeParentPath);
System.out.println("删除"+(del?"成功":"失败"));
}
return executeCodeResponse;
}
//6) 错误处理,提高程序的健壮性
/**
* 获取错误响应
*/
private ExecuteCodeResponse getErrorResponse(Throwable e){
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
executeCodeResponse.setOutputList(new ArrayList<>());
executeCodeResponse.setMessage(e.getMessage());
//设置状态码为2 说明错误
executeCodeResponse.setStatus(2);
executeCodeResponse.setJudgeInfo(new JudgeInfo());
return executeCodeResponse;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
但是到目前我们的项目是安全的吗?
在resources文件夹下创建一个unsafeCode文件夹,模拟错误
# 执行阻塞,程序卡死
SleepError
/**
* 无限睡眠(阻塞程序执行)
*/
public class SleepError {
public static void main(String[] args) throws InterruptedException {
long ONE_HOUR = 60 * 60 * 1000L;
Thread.sleep(ONE_HOUR);
System.out.println("睡完了");
}
}
2
3
4
5
6
7
8
9
10
11
# 占用内存、不释放
/**
* 无限占用空间(浪费系统内存)
*/
public class MemoryError {
public static void main(String[] args) throws InterruptedException {
List<byte[]> bytes = new ArrayList<>();
while (true) {
bytes.add(new byte[10000]);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
# 读取服务器文件(文件泄露)
/**
* 读取服务器文件(文件信息泄露)
*/
public class ReadFileError {
public static void main(String[] args) throws InterruptedException, IOException {
String userDir = System.getProperty("user.dir");
String filePath = userDir + File.separator + "src/main/resources/application.yml";
List<String> allLines = Files.readAllLines(Paths.get(filePath));
System.out.println(String.join("\n", allLines));
}
}
2
3
4
5
6
7
8
9
10
11
12
# 木马程序
/**
* 向服务器写文件(植入危险程序)
*/
public class WriteFileError {
public static void main(String[] args) throws InterruptedException, IOException {
String userDir = System.getProperty("user.dir");
String filePath = userDir + File.separator + "src/main/resources/木马程序.bat";
String errorProgram = "java -version 2>&1";
Files.write(Paths.get(filePath), Arrays.asList(errorProgram));
System.out.println("写木马成功,你完了哈哈");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 运行木马程序
/**
* 运行其他程序(比如危险木马)
*/
public class RunFileError {
public static void main(String[] args) throws InterruptedException, IOException {
String userDir = System.getProperty("user.dir");
String filePath = userDir + File.separator + "src/main/resources/木马程序.bat";
Process process = Runtime.getRuntime().exec(filePath);
process.waitFor();
// 分批获取进程的正常输出
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
// 逐行读取
String compileOutputLine;
while ((compileOutputLine = bufferedReader.readLine()) != null) {
System.out.println(compileOutputLine);
}
System.out.println("执行异常程序成功");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 解决问题
# 超时控制
控制运行时间
我们可以使用一个新的线程来判断运行的程序线程是否超时(类似于守护线程)
private static final long TIME_OUT = 5000L;//定义一个超时时间
//新建一个线程
// 超时控制
Process runProcess = Runtime.getRuntime().exec(runCmd);
new Thread(() -> {
try {
Thread.sleep(TIME_OUT);
System.out.println("超时了,中断");
runProcess.destroy();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
2
3
4
5
6
7
8
9
10
11
12
13
# 限制资源分配
我们不能让每个Java进程的执行占用的JVM最大堆内存空间都和系统一致,实际上应该小一点,比如:256M
在启动Java时,可以指定JVM的参数:-Xmx256m (最大堆空间大小) -Xms(初始堆空间大小)
"java -Xmx256m"
如果需要更严格的内存限制,要在系统层面去限制,而不是JVM层面的限制
如果是Linux系统,可以使用cgruop来实现对某个进程的cpu、内存等资源的分配
String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s;%s -Djava.security.manager=%s Main %s", userCodeParentPath, SECURITY_MANAGER_PATH, SECURITY_MANAGER_CLASS_NAME, inputArgs);
# 限制代码-黑白名单
先定义一个黑白名单,比如哪些操作是禁止的,可以就是一个列表:
private static final List<String> blackList = Arrays.asList("Files", "exec");
private static final WordTree WORD_TREE;
static {
// 初始化字典树
WORD_TREE = new WordTree();
WORD_TREE.addWords(blackList);
}
//检验代码中是否包含黑名单的命令
FoundWord foundWord = WORD_TREE.matchWord(code);
if(foundWord != null){
System.out.println("包含禁止词:"+foundWord.getFoundWord());
return null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
字典树的优势:
- 快速查找匹配
- 空间效率高
- 支持前缀匹配
典型应用:
- 敏感词过滤
- 输入提示
- 字符串匹配
这种实现的好处:
- 只需初始化一次字典树
- 所有实例共享同一个字典树
- 线程安全(因为是不可变的)
- 性能高效(使用字典树结构)
字典树的原理:
# 限制用户的操作权限
上面的黑白名单解决不了所有的情况
Java安全管理器(Security Manager)是Java提供的保护JVM、Java安全的机制,可以实现更严格的资源和操作限制
创建security文件夹
/**
* 默认安全管理器
*/
public class DefaultSecurityManager extends SecurityManager {
// 检查所有的权限
@Override
public void checkPermission(Permission perm) {
System.out.println("默认不做任何限制");
System.out.println(perm);
// super.checkPermission(perm);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
怎么使用
System.setSecurityManager(new DenySecurityManager());
/**
* 禁用所有权限安全管理器
*/
public class DenySecurityManager extends SecurityManager {
// 检查所有的权限
@Override
public void checkPermission(Permission perm) {
throw new SecurityException("权限异常:" + perm.toString());
}
}
2
3
4
5
6
7
8
9
10
11
package com.lanqiao.lanqiaodemosandbox.security;
import java.security.Permission;
public class MySecurityManager extends SecurityManager {
// 检查所有的权限
@Override
public void checkPermission(Permission perm) {
// super.checkPermission(perm);
}
// 检测程序是否可执行文件
@Override
public void checkExec(String cmd) {
throw new SecurityException("checkExec 权限异常:" + cmd);
}
// 检测程序是否允许读文件
@Override
public void checkRead(String file) {
System.out.println(file);
if (file.contains("D:\\IT\\Idea2021\\JavaSystem\\OJStudy\\lanqiao-code-sandbox")) {
return;
}
// throw new SecurityException("checkRead 权限异常:" + file);
}
// 检测程序是否允许写文件
@Override
public void checkWrite(String file) {
// throw new SecurityException("checkWrite 权限异常:" + file);
}
// 检测程序是否允许删除文件
@Override
public void checkDelete(String file) {
// throw new SecurityException("checkDelete 权限异常:" + file);
}
// 检测程序是否允许连接网络
@Override
public void checkConnect(String host, int port) {
// throw new SecurityException("checkConnect 权限异常:" + host + ":" + port);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
写个测试类
/**
* 测试安全管理器
*/
public class TestSecurityManager {
public static void main(String[] args) {
System.setSecurityManager(new MySecurityManager());
FileUtil.writeString("aa", "aaa", Charset.defaultCharset());
}
}
2
3
4
5
6
7
8
9
10
private static final String SECURITY_MANAGER_CLASS_NAME = "MySecurityManager";
private static final String SECURITY_MANAGER_PATH = "C:\\code\\yuoj-code-sandbox\\src\\main\\resources\\security";
String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s;%s -Djava.security.manager=%s Main %s", userCodeParentPath, SECURITY_MANAGER_PATH, SECURITY_MANAGER_CLASS_NAME, inputArgs);
2
3
4
# 结合项目运用
实际情况下,不应该在主类(开发者自己写的程序)中做限制,只需要限制子程序的权限即可。
启动子进程执行命令时,设置安全管理器,而不是在外层设置(会限制住测试用例的读写和子命令的执行)。
具体操作如下:
1)根据需要开发自定义的安全管理器(比如 MySecurityManager)
2)复制 MySecurityManager 类到 resources/security
目录下, 移除类的包名
3)手动输入命令编译 MySecurityManager 类,得到 class 文件
4)在运行 java 程序时,指定安全管理器 class 文件的路径、安全管理器的名称。
命令如下:
注意,windows 下要用分号间隔多个类路径!
java -Dfile.encoding=UTF-8 -cp %s;%s -Djava.security.manager=MySecurityManager Main
依次执行之前的所有测试用例,发现资源成功被限制。
# 安全管理器优点
- 权限控制很灵活
- 实现简单
# 安全管理器缺点
- 如果要做比较严格的权限限制,需要自己去判断哪些文件、包名需要允许读写。粒度太细了,难以精细化控制。
- 安全管理器本身也是 Java 代码,也有可能存在漏洞。本质上还是程序层面的限制,没深入系统的层面。
# 5、运行环境隔离
原理:操作系统层面上,把用户程序封装到沙箱里,和宿主机(我们的电脑 / 服务器)隔离开,使得用户的程序无法影响宿主机。
实现方式:Docker 容器技术(底层是用 cgroup、namespace 等方式实现的),也可以直接使用 cgroup 实现。
# 安装Ubuntu虚拟机
https://ubuntu.com/download/desktop 进入官网直接下载
在vm中选择新建虚拟机 选择自己的ubuntu版本镜像就行
最好是根据你们自己的名字来改 密码我设置的123456
然后一直往下一步走就可以了 注意修改存放的位置
然后进来之后是全英文的,所以需要修改成中文
进去之后打开左下角
点击语言支持图标
找到中文简体
然后点击应用 应该会让你输入密码或者重新启动虚拟机
这个直接重新启动或者输入密码就行,等待下载
最后把下载好的中文拖到第一行
然后就OK了
然后下一步就是设置输入法为中文
把英文删掉就行
然后改一下日期
就OK了
# Docker容器技术
为什么要使用Docker容器技术?
为了提高系统的安全性,把不同的程序和宿主机进行隔离,使得某个程序(应用)的执行不会影响到本身的系统
在虚拟机中安装docker
sudo apt install docker.io
查看docker的版本号
docker -v
拉取远程的镜像测试一下
sudo docker run hello-world
拉取镜像报这个错误:说明没有用国内的镜像,拉取不下来
执行这个命令创建配置文件
sudo vim /etc/docker/daemon.json
如果报这个错误
安装一下
sudo apt install vim
然后再执行vim命令
https://blog.csdn.net/llc580231/article/details/139979603
{
"registry-mirrors": [
"https://docker.registry.cyou",
"https://docker-cf.registry.cyou",
"https://dockercf.jsdelivr.fyi",
"https://docker.jsdelivr.fyi",
"https://dockertest.jsdelivr.fyi",
"https://mirror.aliyuncs.com",
"https://dockerproxy.com",
"https://mirror.baidubce.com",
"https://docker.m.daocloud.io",
"https://docker.nju.edu.cn",
"https://docker.mirrors.sjtug.sjtu.edu.cn",
"https://docker.mirrors.ustc.edu.cn",
"https://mirror.iscas.ac.cn",
"https://docker.rainbond.cc"
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
然后执行
sudo systemctl daemon-reload
sudo service docker restart
然后再拉取镜像,可能要等很久,但基本就OK了
# Java操作docker
<!-- https://mvnrepository.com/artifact/com.github.docker-java/docker-java -->
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java</artifactId>
<version>3.3.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.docker-java/docker-java-transport-httpclient5 -->
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-transport-httpclient5</artifactId>
<version>3.3.0</version>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
我们会使用DockerClient进行操作:这个才是真正和Docker守护进程交互的,最方便的SDK,高层封装,对DockerHttpClient进行了一个封装(类似于Mybatis)
# 使用idea远程开发vm中的项目
打开虚拟机中的网络进行查看ipv4地址
回到windos系统ping这个ip地址看是否能够ping通
先关闭项目,然后点击ssh链接
然后点击新建项目
指定一个新的ssh配置
通过 ip addr也可以
如果连接失败
# 查看SSH服务状态
sudo systemctl status ssh
# 如果没有安装SSH,先安装
sudo apt-get update
sudo apt-get install openssh-server
# 启动SSH服务
sudo systemctl start ssh
# 设置开机自启
sudo systemctl enable ssh
2
3
4
5
6
7
8
9
10
11
12
如果还有问题看一下是不是防火墙没有关闭
接下来会说你的虚拟机内存不够
我们去设置一下
接下来就OK了 ,选择下安装的idea版本
我们把我们window本地的放在Linux服务器上
选择我们的目录就OK了,这样的话我们这个docker项目就完全在linux上面进行开发了
然后我们会发现项目没有办法正常运行因为虚拟机上面并没有jdk8
所以我们需要安装一下jdk8
sudo apt install openjdk-8-jdk
然后检查一下
java -version
javac -version
2
如果安装了还是发现没有办法运行的话
记得配置一下这个
-Djdk.lang.Process.launchMechanism=vfork
然后设置远程开发更新本地开发
运行接下来的代码看能不能拉取镜像下来或者能不能跑通
public class DockerDemo {
public static void main(String[] args) throws InterruptedException {
// 获取默认的 Docker Client
DockerClient dockerClient = DockerClientBuilder.getInstance().build();
// 拉取镜像
String image = "nginx:latest";
PullImageCmd pullImageCmd = dockerClient.pullImageCmd(image);
PullImageResultCallback pullImageResultCallback = new PullImageResultCallback() {
@Override
public void onNext(PullResponseItem item) {
System.out.println("下载镜像:" + item.getStatus());
super.onNext(item);
}
};
pullImageCmd
.exec(pullImageResultCallback)
.awaitCompletion();
System.out.println("下载完成");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
如果报了这个错误
设置一个docker组
cat /etc/group | grep 'docker'
这个时候我们会发现用户组中并没有我们当前的用户
添加用户就好了
sudo gpasswd -a ${USER} docker
如果接下来报这个错误:
把虚拟机这些全部重启
然后再来拉取
如果还是下载不了也无所谓了,后面应该不需要拉取镜像
或者换一个镜像源就能拉取下来了
{
"registry-mirrors": [
"https://docker.hpcloud.cloud",
"https://docker.m.daocloud.io",
"https://docker.unsee.tech",
"https://docker.1panel.live",
"http://mirrors.ustc.edu.cn",
"https://docker.chenby.cn",
"http://mirror.azure.cn",
"https://dockerpull.org",
"https://dockerhub.icu",
"https://hub.rat.dev"
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# docker实现代码沙箱
实现流程;docker负责运行Java程序,并且得到结果
1.把用户的代码保存为文件
2.编译代码,得到class文件
3.把编译好的文件上传到容器环境内
4.在docker容器中执行代码,得到输出结果
5.收集整理输出结果
6.文件清理
7.错误处理,提高程序健壮性
package cn.lanqiao.lanqiaocodesandbox;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.dfa.WordTree;
import cn.lanqiao.lanqiaocodesandbox.model.ExecuteCodeRequest;
import cn.lanqiao.lanqiaocodesandbox.model.ExecuteCodeResponse;
import cn.lanqiao.lanqiaocodesandbox.model.ExecuteMessage;
import cn.lanqiao.lanqiaocodesandbox.model.JudgeInfo;
import cn.lanqiao.lanqiaocodesandbox.utils.ProcessUtils;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @ Author: 李某人
* @ Date: 2024/12/10/21:57
* @ Description:
*
* 守护线程:用一个新的线程,判断一个正在运行的线程是否超时
*/
public class JavaDockerCodeSandboxOld implements CodeSandbox {
private static final String GLOBAL_CODE_DIR_NAME= "tmpCode";
private static final String GLOBAL_JAVA_CLASS_NAME= "Main.java";
private static final Long TIME_OUT = 5000L;
//使用字典树也可以存放在简历中,字典树的使用场景
//定义一个黑白名单,可以通过集合实现
private static final List<String> blackList = Arrays.asList("Files","exec");
//生成一个字典树的对象
private static final WordTree WORD_TREE;
//安全管理器对象
private static final String SECURITY_MANAGER_CLASS_NAME = "MySecurityManager";
//安全管理器的路径
private static final String SECURITY_MANAGER_PATH = "D:\\IT\\LanqiaoJavaProject\\OJProject\\lanqiao-code-sandbox\\src\\main\\resources\\security";
private static final Boolean FIRST_INIT = true;
static {
WORD_TREE = new WordTree();
WORD_TREE.addWords(blackList);
}
public static void main(String[] args) {
JavaDockerCodeSandboxOld javaNativeCodeSandbox = new JavaDockerCodeSandboxOld();
ExecuteCodeRequest executeCodeRequest = new ExecuteCodeRequest();
executeCodeRequest.setInputList(Arrays.asList("1 2","3 4"));
//传入用户写的代码
String code = ResourceUtil.readStr("testCode/simpleComputeArgs/Main.java", StandardCharsets.UTF_8);
// String code = ResourceUtil.readStr("testCode/errorCode/RunFileError.java", StandardCharsets.UTF_8);
executeCodeRequest.setCode(code);
executeCodeRequest.setLanguage("java");
ExecuteCodeResponse executeCodeResponse = javaNativeCodeSandbox.executeCode(executeCodeRequest);
System.out.println(executeCodeResponse);
}
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
// System.setSecurityManager(new DenySecurityManager());
List<String> inputList = executeCodeRequest.getInputList();
String code = executeCodeRequest.getCode();
String language = executeCodeRequest.getLanguage();
// 1. 把用户的代码保存为文件
String userDir = System.getProperty("user.dir");
String globalCodePathName = userDir + File.separator + GLOBAL_CODE_DIR_NAME;
// 判断全局代码目录是否存在,没有则新建
if (!FileUtil.exist(globalCodePathName)) {
FileUtil.mkdir(globalCodePathName);
}
// 把用户的代码隔离存放
String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID();
String userCodePath = userCodeParentPath + File.separator + GLOBAL_JAVA_CLASS_NAME;
File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);
// 2. 编译代码,得到 class 文件
String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());
try {
Process compileProcess = Runtime.getRuntime().exec(compileCmd);
ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
System.out.println(executeMessage);
} catch (Exception e) {
return getErrorResponse(e);
}
// 3. 创建容器,把文件复制到容器内
// 获取默认的 Docker Client
DockerClient dockerClient = DockerClientBuilder.getInstance().build();
// 拉取镜像
String image = "openjdk:8-alpine";
if (FIRST_INIT) {
PullImageCmd pullImageCmd = dockerClient.pullImageCmd(image);
PullImageResultCallback pullImageResultCallback = new PullImageResultCallback() {
@Override
public void onNext(PullResponseItem item) {
System.out.println("下载镜像:" + item.getStatus());
super.onNext(item);
}
};
try {
pullImageCmd
.exec(pullImageResultCallback)
.awaitCompletion();
} catch (InterruptedException e) {
System.out.println("拉取镜像异常");
throw new RuntimeException(e);
}
}
System.out.println("下载完成");
// 创建容器
CreateContainerCmd containerCmd = dockerClient.createContainerCmd(image);
HostConfig hostConfig = new HostConfig();
hostConfig.withMemory(100 * 1000 * 1000L);
hostConfig.withMemorySwap(0L);
hostConfig.withCpuCount(1L);
// hostConfig.withSecurityOpts(Arrays.asList("seccomp=安全管理配置字符串"));
hostConfig.setBinds(new Bind(userCodeParentPath, new Volume("/app")));
CreateContainerResponse createContainerResponse = containerCmd
.withHostConfig(hostConfig)
.withNetworkDisabled(true)
.withReadonlyRootfs(true)
.withAttachStdin(true)
.withAttachStderr(true)
.withAttachStdout(true)
.withTty(true)
.exec();
System.out.println(createContainerResponse);
String containerId = createContainerResponse.getId();
// 启动容器
dockerClient.startContainerCmd(containerId).exec();
// docker exec keen_blackwell java -cp /app Main 1 3
// 执行命令并获取结果
for (String inputArgs : inputList) {
StopWatch stopWatch = new StopWatch();
String[] inputArgsArray = inputArgs.split(" ");
String[] cmdArray = ArrayUtil.append(new String[]{"java", "-cp", "/app", "Main"}, inputArgsArray);
ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
.withCmd(cmdArray)
.withAttachStderr(true)
.withAttachStdin(true)
.withAttachStdout(true)
.exec();
System.out.println("创建执行命令:" + execCreateCmdResponse);
String execId = execCreateCmdResponse.getId();
ExecStartResultCallback execStartResultCallback = new ExecStartResultCallback() {
@Override
public void onNext(Frame frame) {
StreamType streamType = frame.getStreamType();
if (StreamType.STDERR.equals(streamType)) {
System.out.println("输出错误结果:" + new String(frame.getPayload()));
} else {
System.out.println("输出结果:" + new String(frame.getPayload()));
}
super.onNext(frame);
}
};
try {
stopWatch.start();
dockerClient.execStartCmd(execId)
.exec(execStartResultCallback)
.awaitCompletion();
stopWatch.stop();
} catch (InterruptedException e) {
System.out.println("程序执行异常");
throw new RuntimeException(e);
}
}
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
return executeCodeResponse;
}
//6) 错误处理,提高程序的健壮性
/**
* 获取错误响应
*/
private ExecuteCodeResponse getErrorResponse(Throwable e){
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
executeCodeResponse.setOutputList(new ArrayList<>());
executeCodeResponse.setMessage(e.getMessage());
//设置状态码为2 说明错误
executeCodeResponse.setStatus(2);
executeCodeResponse.setJudgeInfo(new JudgeInfo());
return executeCodeResponse;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
System.out.println("创建执行命令:" + execCreateCmdResponse);
ExecuteMessage executeMessage = new ExecuteMessage();
final String[] message = {null};
final String[] errorMessage = {null};
@Override
public void onNext(Frame frame) {
StreamType streamType = frame.getStreamType();
if (StreamType.STDERR.equals(streamType)) {
errorMessage[0] = new String(frame.getPayload());
System.out.println("输出错误结果:" + errorMessage[0]);
} else {
message[0] = new String(frame.getPayload());
System.out.println("输出结果:" + message[0]);
}
super.onNext(frame);
}
try {
stopWatch.start();
dockerClient.execStartCmd(execId)
.exec(execStartResultCallback)
.awaitCompletion();
stopWatch.stop();
statsCmd.close();
time = stopWatch.getLastTaskTimeMillis();
} catch (InterruptedException e) {
System.out.println("程序执行异常");
throw new RuntimeException(e);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# docker安全信息处理
ExecuteMessage executeMessage = new ExecuteMessage();
final String[] message = {null};
final String[] errorMessage = {null};
long time = 0L;
executeMessage.setMessage(message[0]);
executeMessage.setErrorMessage(errorMessage[0]);
executeMessage.setTime(time);
executeMessage.setMemory(maxMemory[0]);
2
3
4
5
6
7
8
9
final long[] maxMemory = {0L};
// 获取占用的内存
StatsCmd statsCmd = dockerClient.statsCmd(containerId);
ResultCallback<Statistics> statisticsResultCallback = statsCmd.exec(new ResultCallback<Statistics>() {
@Override
public void onNext(Statistics statistics) {
System.out.println("内存占用:" + statistics.getMemoryStats().getUsage());
maxMemory[0] = Math.max(statistics.getMemoryStats().getUsage(), maxMemory[0]);
}
@Override
public void close() throws IOException {
}
@Override
public void onStart(Closeable closeable) {
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onComplete() {
}
});
executeMessage.setMemory(maxMemory[0]);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 4、封装结果,跟原生实现方式完全一致
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
List<String> outputList = new ArrayList<>();
// 取用时最大值,便于判断是否超时
long maxTime = 0;
for (ExecuteMessage executeMessage : executeMessageList) {
String errorMessage = executeMessage.getErrorMessage();
if (StrUtil.isNotBlank(errorMessage)) {
executeCodeResponse.setMessage(errorMessage);
// 用户提交的代码执行中存在错误
executeCodeResponse.setStatus(3);
break;
}
outputList.add(executeMessage.getMessage());
Long time = executeMessage.getTime();
if (time != null) {
maxTime = Math.max(maxTime, time);
}
}
// 正常运行完成
if (outputList.size() == executeMessageList.size()) {
executeCodeResponse.setStatus(1);
}
executeCodeResponse.setOutputList(outputList);
JudgeInfo judgeInfo = new JudgeInfo();
judgeInfo.setTime(maxTime);
// 要借助第三方库来获取内存占用,非常麻烦,此处不做实现
// judgeInfo.setMemory();
executeCodeResponse.setJudgeInfo(judgeInfo);
// 5. 文件清理
if (userCodeFile.getParentFile() != null) {
boolean del = FileUtil.del(userCodeParentPath);
System.out.println("删除" + (del ? "成功" : "失败"));
}
return executeCodeResponse;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
final boolean[] timeout = {true};
String execId = execCreateCmdResponse.getId();
ExecStartResultCallback execStartResultCallback = new ExecStartResultCallback() {
@Override
public void onComplete() {
// 如果执行完成,则表示没超时
timeout[0] = false;
super.onComplete();
}
@Override
public void onNext(Frame frame) {
StreamType streamType = frame.getStreamType();
if (StreamType.STDERR.equals(streamType)) {
errorMessage[0] = new String(frame.getPayload());
System.out.println("输出错误结果:" + errorMessage[0]);
} else {
message[0] = new String(frame.getPayload());
System.out.println("输出结果:" + message[0]);
}
super.onNext(frame);
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
完整代码
package cn.lanqiao.lanqiaocodesandbox;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.lanqiao.lanqiaocodesandbox.model.ExecuteCodeRequest;
import cn.lanqiao.lanqiaocodesandbox.model.ExecuteCodeResponse;
import cn.hutool.dfa.WordTree;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.lanqiao.lanqiaocodesandbox.model.ExecuteMessage;
import cn.lanqiao.lanqiaocodesandbox.model.JudgeInfo;
import cn.lanqiao.lanqiaocodesandbox.utils.ProcessUtils;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.async.ResultCallback;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.nio.charset.StandardCharsets;
public class JavaDockerCodeSandbox implements CodeSandbox{
private static final String GLOBAL_CODE_DIR_NAME= "tmpCode";
private static final String GLOBAL_JAVA_CLASS_NAME= "Main.java";
private static final Long TIME_OUT = 5000L;
//使用字典树也可以存放在简历中,字典树的使用场景
//定义一个黑白名单,可以通过集合实现
private static final List<String> blackList = Arrays.asList("Files","exec");
//生成一个字典树的对象
private static final WordTree WORD_TREE;
private static final boolean FIRST_INIT = true;
//安全管理器对象
private static final String SECURITY_MANAGER_CLASS_NAME = "MySecurityManager";
//安全管理器的路径
private static final String SECURITY_MANAGER_PATH = "D:\\IT\\LanqiaoJavaProject\\OJProject\\lanqiao-code-sandbox\\src\\main\\resources\\security";
static {
WORD_TREE = new WordTree();
WORD_TREE.addWords(blackList);
}
public static void main(String[] args) {
JavaDockerCodeSandbox javaNativeCodeSandbox = new JavaDockerCodeSandbox();
ExecuteCodeRequest executeCodeRequest = new ExecuteCodeRequest();
executeCodeRequest.setInputList(Arrays.asList("1 2","3 4"));
//传入用户写的代码
String code = ResourceUtil.readStr("testCode/simpleComputeArgs/Main.java", StandardCharsets.UTF_8);
// String code = ResourceUtil.readStr("testCode/errorCode/RunFileError.java", StandardCharsets.UTF_8);
executeCodeRequest.setCode(code);
executeCodeRequest.setLanguage("java");
ExecuteCodeResponse executeCodeResponse = javaNativeCodeSandbox.executeCode(executeCodeRequest);
System.out.println(executeCodeResponse);
}
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
String code = executeCodeRequest.getCode();
String language = executeCodeRequest.getLanguage();
List<String> inputList = executeCodeRequest.getInputList();
String userDir = System.getProperty("user.dir");
//File.separator 是为了兼容不同的系统的 \
String globalCodePathName = userDir + File.separator + GLOBAL_CODE_DIR_NAME;
//判断全局代码目录是否存在,没有则新建
if (!FileUtil.exist(globalCodePathName)){
FileUtil.mkdir(globalCodePathName);
}
//将用户提交的代码隔离
String userCodeParentPath = globalCodePathName+ File.separator+ UUID.randomUUID();
//真正的用户路径
String userCodePath = userCodeParentPath + File.separator + GLOBAL_JAVA_CLASS_NAME;
//直接写入到程序中
File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);
//2) 编译代码,得到class文件
String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsoluteFile());
try {
Process compileProcess = Runtime.getRuntime().exec(compileCmd);
ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
System.out.println(executeMessage);
} catch (Exception e) {
return getErrorResponse(e);
}
//3.拉取镜像
DockerClient dockerClient = DockerClientBuilder.getInstance().build();
//拉取jdk8的镜像
String image = "openjdk:8-alpine";
if (FIRST_INIT){
//用户第一次进来需要拉取镜像,其他的时候咱们都不需要拉取镜像
PullImageCmd pullImageCmd = dockerClient.pullImageCmd(image);
PullImageResultCallback pullImageResultCallback = new PullImageResultCallback(){
@Override
public void onNext(PullResponseItem item) {
System.out.println("下载镜像:"+item.getStatus());
super.onNext(item);
}
};
try {
pullImageCmd.exec(pullImageResultCallback)
.awaitCompletion();
} catch (InterruptedException e) {
System.out.println("拉取镜像异常");
throw new RuntimeException(e);
}
}
System.out.println("下载完成");
//4.创建容器
CreateContainerCmd containerCmd = dockerClient.createContainerCmd(image);
HostConfig hostConfig = new HostConfig();
// 设置容器的内存限制为 100MB
hostConfig.withMemory(100 * 1000 * 1000L);
// 设置容器的交换内存为 0,禁用交换内存
hostConfig.withMemorySwap(0L);
// 限制容器只能使用 1 个 CPU 核心
hostConfig.withCpuCount(1L);
// 设置安全选项,使用 seccomp 配置来限制容器的系统调用
// hostConfig.withSecurityOpts(Arrays.asList("seccomp=安全管理配置字符串"));
// 将主机的 userCodeParentPath 目录挂载到容器的 /app 目录
hostConfig.setBinds(new Bind(userCodeParentPath, new Volume("/app")));
// 执行容器创建命令,配置各种参数:
CreateContainerResponse createContainerResponse = containerCmd
.withHostConfig(hostConfig) // 设置主机配置
.withNetworkDisabled(true) // 禁用网络访问
.withReadonlyRootfs(true) // 将根文件系统设置为只读
.withAttachStdin(true) // 允许附加标准输入
.withAttachStderr(true) // 允许附加标准错误输出
.withAttachStdout(true) // 允许附加标准输出
.withTty(true) // 分配一个伪 TTY
.exec(); // 执行创建命令
// 打印容器创建的响应信息
System.out.println(createContainerResponse);
//去启动容器执行命令并且获取结果
String containerId = createContainerResponse.getId();
dockerClient.startContainerCmd(containerId).exec();
//执行命令并获取结果
//docker exec 'docker镜像名字' java -cp /app Main 1 3 2 4
List<ExecuteMessage> executeMessageList = new ArrayList<>();//用来存储输出信息
for (String inputArgs : inputList){
StopWatch stopWatch = new StopWatch();
String[] inputArgsArray = inputArgs.split(" ");
// 构建完整的命令数组:
// - "java": 使用 Java 运行程序
// - "-cp": 指定类路径
// - "/app": 类路径为容器中的 /app 目录
// - "Main": 要运行的主类名
// - inputArgsArray: 附加用户的输入参数
String[] cmdArray = ArrayUtil.append(new String[]{"java", "-cp", "/app", "Main"}, inputArgsArray);
// 创建在容器中执行命令的请求:
ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
.withCmd(cmdArray) // 设置要执行的命令数组
.withAttachStderr(true) // 附加标准错误输出
.withAttachStdin(true) // 附加标准输入
.withAttachStdout(true) // 附加标准输出
.exec(); // 执行命令创建
// 打印创建的执行命令信息
System.out.println("创建执行命令:" + execCreateCmdResponse);
ExecuteMessage executeMessage = new ExecuteMessage();
final String[] message = {null};//运行结果的信息
final String[] errorMessage = {null};//运行错误结果的信息
long time = 0L;//记录运行时间
final long[] maxMemory ={0L};//记录最大内存
//执行命令
String execId = execCreateCmdResponse.getId();
ExecStartResultCallback execStartResultCallback = new ExecStartResultCallback(){
@Override
public void onNext(Frame frame) {
StreamType streamType = frame.getStreamType();
if (StreamType.STDERR.equals(streamType)){
errorMessage[0] = new String(frame.getPayload());
System.out.println("输出错误结果:"+ errorMessage[0]);
}else {
message[0] = new String(frame.getPayload());
System.out.println("输出结果:"+ message[0]);
}
super.onNext(frame);
}
};
//记录内存
StatsCmd statsCmd = dockerClient.statsCmd(containerId);
statsCmd.exec(new ResultCallback<Statistics>() {
@Override
public void onStart(Closeable closeable) {
}
@Override
public void onNext(Statistics statistics) {
System.out.println("内存占用:"+statistics.getMemoryStats().getUsage());
maxMemory[0] = Math.max(statistics.getMemoryStats().getUsage(), maxMemory[0]);
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onComplete() {
}
@Override
public void close() throws IOException {
}
});
stopWatch.start();
try {
dockerClient.execStartCmd(execId)
.exec(execStartResultCallback)
.awaitCompletion();
stopWatch.stop();
statsCmd.close();
time = stopWatch.getLastTaskTimeMillis();
} catch (InterruptedException e) {
System.out.println("程序执行异常");
throw new RuntimeException(e);
}
executeMessage.setTime(time);
executeMessage.setMessage(message[0]);
executeMessage.setErrorMessage(errorMessage[0]);
executeMessage.setMemory(maxMemory[0]);
executeMessageList.add(executeMessage);
}
// 4、封装结果,跟原生实现方式完全一致
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
List<String> outputList = new ArrayList<>();
// 取用时最大值,便于判断是否超时
long maxTime = 0;
for (ExecuteMessage executeMessage : executeMessageList) {
String errorMessage = executeMessage.getErrorMessage();
if (StrUtil.isNotBlank(errorMessage)) {
executeCodeResponse.setMessage(errorMessage);
// 用户提交的代码执行中存在错误
executeCodeResponse.setStatus(3);
break;
}
outputList.add(executeMessage.getMessage());
//注意这里需要修改一下
Long time = executeMessage.getTime();
if (time != null) {
maxTime = Math.max(maxTime, time);
}
}
// 正常运行完成
if (outputList.size() == executeMessageList.size()) {
executeCodeResponse.setStatus(1);
}
executeCodeResponse.setOutputList(outputList);
JudgeInfo judgeInfo = new JudgeInfo();
judgeInfo.setTime(maxTime);
executeCodeResponse.setJudgeInfo(judgeInfo);
//5. 文件清理
if (userCodeFile.getParentFile() != null) {
boolean del = FileUtil.del(userCodeParentPath);
System.out.println("删除" + (del ? "成功" : "失败"));
}
return executeCodeResponse;
}
//6) 错误处理,提高程序的健壮性
/**
* 获取错误响应
*/
private ExecuteCodeResponse getErrorResponse(Throwable e){
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
executeCodeResponse.setOutputList(new ArrayList<>());
executeCodeResponse.setMessage(e.getMessage());
//设置状态码为2 说明错误
executeCodeResponse.setStatus(2);
executeCodeResponse.setJudgeInfo(new JudgeInfo());
return executeCodeResponse;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# 模版代码沙箱
模版方法:定义一套通用的执行流程,让子类负责每个执行步骤的具体实现
模版方法的使用场景:适用于有规范的流程,且执行流程可以复用
作用:大幅节省重复代码量,便于项目扩展,更好维护
package com.lanqiao.lanqiaodemosandbox;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.lanqiao.lanqiaodemosandbox.model.ExecuteCodeRequest;
import com.lanqiao.lanqiaodemosandbox.model.ExecuteCodeResponse;
import com.lanqiao.lanqiaodemosandbox.model.ExecuteMessage;
import com.lanqiao.lanqiaodemosandbox.model.JudgeInfo;
import com.lanqiao.lanqiaodemosandbox.utils.ProcessUtils;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Java 代码沙箱模板方法的实现
*/
@Slf4j
public abstract class JavaCodeSandboxTemplate implements CodeSandbox {
private static final String GLOBAL_CODE_DIR_NAME = "tmpCode";
private static final String GLOBAL_JAVA_CLASS_NAME = "Main.java";
private static final long TIME_OUT = 5000L;
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
List<String> inputList = executeCodeRequest.getInputList();
String code = executeCodeRequest.getCode();
String language = executeCodeRequest.getLanguage();
//1. 把用户的代码保存为文件
File userCodeFile = saveCodeToFile(code);
//2. 编译代码,得到 class 文件
ExecuteMessage compileFileExecuteMessage = compileFile(userCodeFile);
System.out.println(compileFileExecuteMessage);
// 3. 执行代码,得到输出结果
List<ExecuteMessage> executeMessageList = runFile(userCodeFile, inputList);
//4. 收集整理输出结果
ExecuteCodeResponse outputResponse = getOutputResponse(executeMessageList);
//5. 文件清理
boolean b = deleteFile(userCodeFile);
if (!b) {
log.error("deleteFile error, userCodeFilePath = {}", userCodeFile.getAbsolutePath());
}
return outputResponse;
}
/**
* 1. 把用户的代码保存为文件
* @param code 用户代码
* @return
*/
public File saveCodeToFile(String code) {
String userDir = System.getProperty("user.dir");
String globalCodePathName = userDir + File.separator + GLOBAL_CODE_DIR_NAME;
// 判断全局代码目录是否存在,没有则新建
if (!FileUtil.exist(globalCodePathName)) {
FileUtil.mkdir(globalCodePathName);
}
// 把用户的代码隔离存放
String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID();
String userCodePath = userCodeParentPath + File.separator + GLOBAL_JAVA_CLASS_NAME;
File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);
return userCodeFile;
}
/**
* 2、编译代码
* @param userCodeFile
* @return
*/
public ExecuteMessage compileFile(File userCodeFile) {
String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath());
try {
Process compileProcess = Runtime.getRuntime().exec(compileCmd);
ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
if (executeMessage.getExitValue() != 0) {
throw new RuntimeException("编译错误");
}
return executeMessage;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 3、执行文件,获得执行结果列表
* @param userCodeFile
* @param inputList
* @return
*/
public List<ExecuteMessage> runFile(File userCodeFile, List<String> inputList) {
String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();
List<ExecuteMessage> executeMessageList = new ArrayList<>();
for (String inputArgs : inputList) {
// String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeParentPath, inputArgs);
String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeParentPath, inputArgs);
try {
Process runProcess = Runtime.getRuntime().exec(runCmd);
// 超时控制
new Thread(() -> {
try {
Thread.sleep(TIME_OUT);
System.out.println("超时了,中断");
runProcess.destroy();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(runProcess, "运行");
System.out.println(executeMessage);
executeMessageList.add(executeMessage);
} catch (Exception e) {
throw new RuntimeException("执行错误", e);
}
}
return executeMessageList;
}
/**
* 4、获取输出结果
* @param executeMessageList
* @return
*/
public ExecuteCodeResponse getOutputResponse(List<ExecuteMessage> executeMessageList) {
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
List<String> outputList = new ArrayList<>();
// 取用时最大值,便于判断是否超时
long maxTime = 0;
for (ExecuteMessage executeMessage : executeMessageList) {
String errorMessage = executeMessage.getErrorMessage();
if (StrUtil.isNotBlank(errorMessage)) {
executeCodeResponse.setMessage(errorMessage);
// 用户提交的代码执行中存在错误
executeCodeResponse.setStatus(3);
break;
}
outputList.add(executeMessage.getMessage());
Long time = executeMessage.getTime();
if (time != null) {
maxTime = Math.max(maxTime, time);
}
}
// 正常运行完成
if (outputList.size() == executeMessageList.size()) {
executeCodeResponse.setStatus(1);
}
executeCodeResponse.setOutputList(outputList);
JudgeInfo judgeInfo = new JudgeInfo();
judgeInfo.setTime(maxTime);
// 要借助第三方库来获取内存占用,非常麻烦,此处不做实现
// judgeInfo.setMemory();
executeCodeResponse.setJudgeInfo(judgeInfo);
return executeCodeResponse;
}
/**
* 5、删除文件
* @param userCodeFile
* @return
*/
public boolean deleteFile(File userCodeFile) {
if (userCodeFile.getParentFile() != null) {
String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();
boolean del = FileUtil.del(userCodeParentPath);
System.out.println("删除" + (del ? "成功" : "失败"));
return del;
}
return true;
}
/**
* 6、获取错误响应
*
* @param e
* @return
*/
private ExecuteCodeResponse getErrorResponse(Throwable e) {
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
executeCodeResponse.setOutputList(new ArrayList<>());
executeCodeResponse.setMessage(e.getMessage());
// 表示代码沙箱错误
executeCodeResponse.setStatus(2);
executeCodeResponse.setJudgeInfo(new JudgeInfo());
return executeCodeResponse;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# 定义子类的写法
/**
* Java 原生代码沙箱实现(直接复用模板方法)
*/
@Component
public class JavaNativeCodeSandbox extends JavaCodeSandboxTemplate {
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
return super.executeCode(executeCodeRequest);
}
}
2
3
4
5
6
7
8
9
10
11
# docker模版写法
package com.lanqiao.lanqiaodemosandbox;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.ArrayUtil;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.async.ResultCallback;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import com.lanqiao.lanqiaodemosandbox.model.ExecuteCodeRequest;
import com.lanqiao.lanqiaodemosandbox.model.ExecuteCodeResponse;
import com.lanqiao.lanqiaodemosandbox.model.ExecuteMessage;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
public class JavaDockerCodeSandbox extends JavaCodeSandboxTemplate {
private static final long TIME_OUT = 5000L;
private static final Boolean FIRST_INIT = true;
public static void main(String[] args) {
JavaDockerCodeSandbox javaNativeCodeSandbox = new JavaDockerCodeSandbox();
ExecuteCodeRequest executeCodeRequest = new ExecuteCodeRequest();
executeCodeRequest.setInputList(Arrays.asList("1 2", "1 3"));
String code = ResourceUtil.readStr("testCode/simpleComputeArgs/Main.java", StandardCharsets.UTF_8);
// String code = ResourceUtil.readStr("testCode/unsafeCode/RunFileError.java", StandardCharsets.UTF_8);
// String code = ResourceUtil.readStr("testCode/simpleCompute/Main.java", StandardCharsets.UTF_8);
executeCodeRequest.setCode(code);
executeCodeRequest.setLanguage("java");
ExecuteCodeResponse executeCodeResponse = javaNativeCodeSandbox.executeCode(executeCodeRequest);
System.out.println(executeCodeResponse);
}
/**
* 3、创建容器,把文件复制到容器内
* @param userCodeFile
* @param inputList
* @return
*/
@Override
public List<ExecuteMessage> runFile(File userCodeFile, List<String> inputList) {
String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();
// 获取默认的 Docker Client
DockerClient dockerClient = DockerClientBuilder.getInstance().build();
// 拉取镜像
String image = "openjdk:8-alpine";
if (FIRST_INIT) {
PullImageCmd pullImageCmd = dockerClient.pullImageCmd(image);
PullImageResultCallback pullImageResultCallback = new PullImageResultCallback() {
@Override
public void onNext(PullResponseItem item) {
System.out.println("下载镜像:" + item.getStatus());
super.onNext(item);
}
};
try {
pullImageCmd
.exec(pullImageResultCallback)
.awaitCompletion();
} catch (InterruptedException e) {
System.out.println("拉取镜像异常");
throw new RuntimeException(e);
}
}
System.out.println("下载完成");
// 创建容器
// 使用 dockerClient 创建容器命令,指定要使用的镜像
CreateContainerCmd containerCmd = dockerClient.createContainerCmd(image);
// 创建主机配置对象,用于设置容器的资源限制和权限
HostConfig hostConfig = new HostConfig();
// 设置容器的内存限制为 100MB
hostConfig.withMemory(100 * 1000 * 1000L);
// 设置容器的交换内存为 0,禁用交换内存
hostConfig.withMemorySwap(0L);
// 限制容器只能使用 1 个 CPU 核心
hostConfig.withCpuCount(1L);
// 设置安全选项,使用 seccomp 配置来限制容器的系统调用
hostConfig.withSecurityOpts(Arrays.asList("seccomp=安全管理配置字符串"));
// 将主机的 userCodeParentPath 目录挂载到容器的 /app 目录
hostConfig.setBinds(new Bind(userCodeParentPath, new Volume("/app")));
// 执行容器创建命令,配置各种参数:
CreateContainerResponse createContainerResponse = containerCmd
.withHostConfig(hostConfig) // 设置主机配置
.withNetworkDisabled(true) // 禁用网络访问
.withReadonlyRootfs(true) // 将根文件系统设置为只读
.withAttachStdin(true) // 允许附加标准输入
.withAttachStderr(true) // 允许附加标准错误输出
.withAttachStdout(true) // 允许附加标准输出
.withTty(true) // 分配一个伪 TTY
.exec(); // 执行创建命令
// 打印容器创建的响应信息
System.out.println(createContainerResponse);
String containerId = createContainerResponse.getId();
// 启动容器
dockerClient.startContainerCmd(containerId).exec();
// docker exec keen_blackwell java -cp /app Main 1 3
// 执行命令并获取结果
// 创建一个列表来存储所有执行结果
List<ExecuteMessage> executeMessageList = new ArrayList<>();
// 遍历每个输入参数
for (String inputArgs : inputList) {
// 创建一个计时器对象,用于记录代码执行时间
StopWatch stopWatch = new StopWatch();
// 将输入参数字符串按空格分割成数组
String[] inputArgsArray = inputArgs.split(" ");
// 构建完整的命令数组:
// - "java": 使用 Java 运行程序
// - "-cp": 指定类路径
// - "/app": 类路径为容器中的 /app 目录
// - "Main": 要运行的主类名
// - inputArgsArray: 附加用户的输入参数
String[] cmdArray = ArrayUtil.append(new String[]{"java", "-cp", "/app", "Main"}, inputArgsArray);
// 创建在容器中执行命令的请求:
ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
.withCmd(cmdArray) // 设置要执行的命令数组
.withAttachStderr(true) // 附加标准错误输出
.withAttachStdin(true) // 附加标准输入
.withAttachStdout(true) // 附加标准输出
.exec(); // 执行命令创建
// 打印创建的执行命令信息
System.out.println("创建执行命令:" + execCreateCmdResponse);
ExecuteMessage executeMessage = new ExecuteMessage();
final String[] message = {null};
final String[] errorMessage = {null};
long time = 0L;
// 判断是否超时
final boolean[] timeout = {true};
String execId = execCreateCmdResponse.getId();
ExecStartResultCallback execStartResultCallback = new ExecStartResultCallback() {
@Override
public void onComplete() {
// 如果执行完成,则表示没超时
timeout[0] = false;
super.onComplete();
}
@Override
public void onNext(Frame frame) {
StreamType streamType = frame.getStreamType();
if (StreamType.STDERR.equals(streamType)) {
errorMessage[0] = new String(frame.getPayload());
System.out.println("输出错误结果:" + errorMessage[0]);
} else {
message[0] = new String(frame.getPayload());
System.out.println("输出结果:" + message[0]);
}
super.onNext(frame);
}
};
final long[] maxMemory = {0L};
// 获取占用的内存
StatsCmd statsCmd = dockerClient.statsCmd(containerId);
ResultCallback<Statistics> statisticsResultCallback = statsCmd.exec(new ResultCallback<Statistics>() {
@Override
public void onNext(Statistics statistics) {
System.out.println("内存占用:" + statistics.getMemoryStats().getUsage());
maxMemory[0] = Math.max(statistics.getMemoryStats().getUsage(), maxMemory[0]);
}
@Override
public void close() throws IOException {
}
@Override
public void onStart(Closeable closeable) {
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onComplete() {
}
});
statsCmd.exec(statisticsResultCallback);
try {
stopWatch.start();
dockerClient.execStartCmd(execId)
.exec(execStartResultCallback)
.awaitCompletion(TIME_OUT, TimeUnit.MICROSECONDS);
stopWatch.stop();
time = stopWatch.getLastTaskTimeMillis();
statsCmd.close();
} catch (InterruptedException e) {
System.out.println("程序执行异常");
throw new RuntimeException(e);
}
executeMessage.setMessage(message[0]);
executeMessage.setErrorMessage(errorMessage[0]);
executeMessage.setTime(time);
executeMessage.setMemory(maxMemory[0]);
executeMessageList.add(executeMessage);
}
return executeMessageList;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# 代码沙箱的开发API
@RestController("/")
public class MainController {
// 定义鉴权请求头和密钥
private static final String AUTH_REQUEST_HEADER = "auth";
private static final String AUTH_REQUEST_SECRET = "secretKey";
@Resource
private JavaNativeCodeSandbox javaNativeCodeSandbox;
@GetMapping("/health")
public String healthCheck() {
return "ok";
}
/**
* 执行代码
*
* @param executeCodeRequest
* @return
*/
@PostMapping("/executeCode")
ExecuteCodeResponse executeCode(@RequestBody ExecuteCodeRequest executeCodeRequest, HttpServletRequest request,
HttpServletResponse response) {
// 基本的认证
String authHeader = request.getHeader(AUTH_REQUEST_HEADER);
if (!AUTH_REQUEST_SECRET.equals(authHeader)) {
response.setStatus(403);
return null;
}
if (executeCodeRequest == null) {
throw new RuntimeException("请求参数为空");
}
return javaNativeCodeSandbox.executeCode(executeCodeRequest);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 调用远程代码沙箱
/**
* 远程代码沙箱(实际调用接口的沙箱)
*/
public class RemoteCodeSandbox implements CodeSandbox {
//定义鉴权请求头和密钥
private static final String AUTH_REQUST_HEADER = "auth";
private static final String AUTH_REQUEST_SECRET = "secretKey";
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
System.out.println("远程代码沙箱");
String url = "http://localhost:8090/executeCode";
String json = JSONUtil.toJsonStr(executeCodeRequest);
String responseStr = HttpUtil.createPost(url)
.header(AUTH_REQUST_HEADER,AUTH_REQUEST_SECRET)
.body(json)
.execute()
.body();
if (StringUtils.isBlank(responseStr)) {
throw new BusinessException(ErrorCode.API_REQUEST_ERROR, "executeCode remoteSandbox error, message = " + responseStr);
}
return JSONUtil.toBean(responseStr, ExecuteCodeResponse.class);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 调用安全性
如果服务不做任何的权限校验,直接发到公网,是不安全的
1)调用方与服务提供方之间约定一个字符串(最好加密)
@RestController("/")
public class MainController {
// 定义鉴权请求头和密钥
private static final String AUTH_REQUEST_HEADER = "auth";
private static final String AUTH_REQUEST_SECRET = "secretKey";
@Resource
private JavaNativeCodeSandbox javaNativeCodeSandbox;
@GetMapping("/health")
public String healthCheck() {
return "ok";
}
/**
* 执行代码
*
* @param executeCodeRequest
* @return
*/
@PostMapping("/executeCode")
ExecuteCodeResponse executeCode(@RequestBody ExecuteCodeRequest executeCodeRequest, HttpServletRequest request,
HttpServletResponse response) {
// 基本的认证
String authHeader = request.getHeader(AUTH_REQUEST_HEADER);
if (!AUTH_REQUEST_SECRET.equals(authHeader)) {
response.setStatus(403);
return null;
}
if (executeCodeRequest == null) {
throw new RuntimeException("请求参数为空");
}
return javaNativeCodeSandbox.executeCode(executeCodeRequest);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 远程代码沙箱(实际调用接口的沙箱)
*/
public class RemoteCodeSandbox implements CodeSandbox {
//定义鉴权请求头和密钥
private static final String AUTH_REQUST_HEADER = "auth";
private static final String AUTH_REQUEST_SECRET = "secretKey";
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
System.out.println("远程代码沙箱");
String url = "http://localhost:8090/executeCode";
String json = JSONUtil.toJsonStr(executeCodeRequest);
String responseStr = HttpUtil.createPost(url)
.header(AUTH_REQUST_HEADER,AUTH_REQUEST_SECRET)
.body(json)
.execute()
.body();
if (StringUtils.isBlank(responseStr)) {
throw new BusinessException(ErrorCode.API_REQUEST_ERROR, "executeCode remoteSandbox error, message = " + responseStr);
}
return JSONUtil.toBean(responseStr, ExecuteCodeResponse.class);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 跑通项目流程
/**
* 提交题目
*
* @param questionSubmitAddRequest
* @param request
* @return resultNum 本次点赞变化数
*/
@PostMapping("/question_submit/do")
public BaseResponse<Long> doQuestionSubmit(@RequestBody QuestionSubmitAddRequest questionSubmitAddRequest,
HttpServletRequest request) {
if (questionSubmitAddRequest == null || questionSubmitAddRequest.getQuestionId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
final User loginUser = userService.getLoginUser(request);
long questionSubmitId = questionSubmitService.doQuestionSubmit(questionSubmitAddRequest, loginUser);
return ResultUtils.success(questionSubmitId);
}
/**
* 分页获取列表(仅管理员)
*
* @param
* @return
*/
@PostMapping("/question_submit/list/page")
public BaseResponse<Page<QuestionSubmitVO>> listQuestionSubmitByPage(@RequestBody QuestionSubmitQueryRequest questionSubmitQueryRequest,
HttpServletRequest request) {
long current = questionSubmitQueryRequest.getCurrent();
long size = questionSubmitQueryRequest.getPageSize();
//从数据库中查询原始的题目提交分页信息
Page<QuestionSubmit> questionSubmitPage = questionSubmitService.page(new Page<>(current, size),
questionSubmitService.getQueryWrapper(questionSubmitQueryRequest));
final User loginUser = userService.getLoginUser(request);
return ResultUtils.success(questionSubmitService.getQuestionSubmitVOPage(questionSubmitPage,loginUser));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 最终完整代码
package cn.lanqiao.lanqiaocodesandbox;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.util.ArrayUtil;
import cn.lanqiao.lanqiaocodesandbox.model.ExecuteCodeRequest;
import cn.lanqiao.lanqiaocodesandbox.model.ExecuteCodeResponse;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.lanqiao.lanqiaocodesandbox.model.ExecuteMessage;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.async.ResultCallback;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import org.springframework.stereotype.Component;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
@Component
public class JavaDockerCodeSandbox extends JavaCodeSandboxTemplate {
private static final long TIME_OUT = 5000L;
private static final Boolean FIRST_INIT = true;
public static void main(String[] args) {
JavaDockerCodeSandbox javaNativeCodeSandbox = new JavaDockerCodeSandbox();
ExecuteCodeRequest executeCodeRequest = new ExecuteCodeRequest();
executeCodeRequest.setInputList(Arrays.asList("1 2", "1 3"));
String code = ResourceUtil.readStr("testCode/simpleComputeArgs/Main.java", StandardCharsets.UTF_8);
// String code = ResourceUtil.readStr("testCode/unsafeCode/RunFileError.java", StandardCharsets.UTF_8);
// String code = ResourceUtil.readStr("testCode/simpleCompute/Main.java", StandardCharsets.UTF_8);
executeCodeRequest.setCode(code);
executeCodeRequest.setLanguage("java");
ExecuteCodeResponse executeCodeResponse = javaNativeCodeSandbox.executeCode(executeCodeRequest);
System.out.println(executeCodeResponse);
}
/**
* 3、创建容器,把文件复制到容器内
* @param userCodeFile
* @param inputList
* @return
*/
@Override
public List<ExecuteMessage> runFile(File userCodeFile, List<String> inputList) {
String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();
// 获取默认的 Docker Client
DockerClient dockerClient = DockerClientBuilder.getInstance().build();
// 拉取镜像
String image = "openjdk:8-alpine";
if (FIRST_INIT) {
PullImageCmd pullImageCmd = dockerClient.pullImageCmd(image);
PullImageResultCallback pullImageResultCallback = new PullImageResultCallback() {
@Override
public void onNext(PullResponseItem item) {
System.out.println("下载镜像:" + item.getStatus());
super.onNext(item);
}
};
try {
pullImageCmd
.exec(pullImageResultCallback)
.awaitCompletion();
} catch (InterruptedException e) {
System.out.println("拉取镜像异常");
throw new RuntimeException(e);
}
}
System.out.println("下载完成");
// 创建容器
CreateContainerCmd containerCmd = dockerClient.createContainerCmd(image);
HostConfig hostConfig = new HostConfig();
hostConfig.withMemory(100 * 1000 * 1000L);
hostConfig.withMemorySwap(0L);
hostConfig.withCpuCount(1L);
// hostConfig.withSecurityOpts(Arrays.asList("seccomp=安全管理配置字符串"));
hostConfig.setBinds(new Bind(userCodeParentPath, new Volume("/app")));
CreateContainerResponse createContainerResponse = containerCmd
.withHostConfig(hostConfig)
.withNetworkDisabled(true)
.withReadonlyRootfs(true)
.withAttachStdin(true)
.withAttachStderr(true)
.withAttachStdout(true)
.withTty(true)
.exec();
System.out.println(createContainerResponse);
String containerId = createContainerResponse.getId();
// 启动容器
dockerClient.startContainerCmd(containerId).exec();
// docker exec keen_blackwell java -cp /app Main 1 3
// 执行命令并获取结果
List<ExecuteMessage> executeMessageList = new ArrayList<>();
for (String inputArgs : inputList) {
StopWatch stopWatch = new StopWatch();
String[] inputArgsArray = inputArgs.split(" ");
String[] cmdArray = ArrayUtil.append(new String[]{"java", "-cp", "/app", "Main"}, inputArgsArray);
ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
.withCmd(cmdArray)
.withAttachStderr(true)
.withAttachStdin(true)
.withAttachStdout(true)
.exec();
System.out.println("创建执行命令:" + execCreateCmdResponse);
ExecuteMessage executeMessage = new ExecuteMessage();
final String[] message = {null};
final String[] errorMessage = {null};
long time = 0L;
// 判断是否超时
final boolean[] timeout = {true};
String execId = execCreateCmdResponse.getId();
ExecStartResultCallback execStartResultCallback = new ExecStartResultCallback() {
@Override
public void onComplete() {
// 如果执行完成,则表示没超时
timeout[0] = false;
super.onComplete();
}
@Override
public void onNext(Frame frame) {
StreamType streamType = frame.getStreamType();
if (StreamType.STDERR.equals(streamType)) {
errorMessage[0] = new String(frame.getPayload()).trim();
System.out.println("输出错误结果:" + errorMessage[0]);
} else {
message[0] = new String(frame.getPayload()).trim();
System.out.println("输出结果:" + message[0]);
}
super.onNext(frame);
}
};
final long[] maxMemory = {0L};
// 获取占用的内存
StatsCmd statsCmd = dockerClient.statsCmd(containerId);
ResultCallback<Statistics> statisticsResultCallback = statsCmd.exec(new ResultCallback<Statistics>() {
@Override
public void onNext(Statistics statistics) {
// System.out.println("内存占用:" + statistics.getMemoryStats().getUsage());
maxMemory[0] = Math.max(statistics.getMemoryStats().getUsage(), maxMemory[0]);
}
@Override
public void close() throws IOException {
}
@Override
public void onStart(Closeable closeable) {
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onComplete() {
}
});
statsCmd.exec(statisticsResultCallback);
try {
stopWatch.start();
dockerClient.execStartCmd(execId)
.exec(execStartResultCallback)
.awaitCompletion(TIME_OUT, TimeUnit.SECONDS);
stopWatch.stop();
time = stopWatch.getLastTaskTimeMillis();
statsCmd.close();
} catch (InterruptedException e) {
System.out.println("程序执行异常");
throw new RuntimeException(e);
}
executeMessage.setMessage(message[0]);
executeMessage.setErrorMessage(errorMessage[0]);
executeMessage.setTime(time);
executeMessage.setMemory(maxMemory[0]);
executeMessageList.add(executeMessage);
}
return executeMessageList;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package cn.lanqiao.lanqiaocodesandbox;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.lanqiao.lanqiaocodesandbox.model.ExecuteCodeRequest;
import cn.lanqiao.lanqiaocodesandbox.model.ExecuteCodeResponse;
import cn.lanqiao.lanqiaocodesandbox.model.ExecuteMessage;
import cn.lanqiao.lanqiaocodesandbox.model.JudgeInfo;
import cn.lanqiao.lanqiaocodesandbox.utils.ProcessUtils;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Java 代码沙箱模板方法的实现
*/
@Slf4j
public abstract class JavaCodeSandboxTemplate implements CodeSandbox{
private static final String GLOBAL_CODE_DIR_NAME= "tmpCode";
private static final String GLOBAL_JAVA_CLASS_NAME= "Main.java";
private static final Long TIME_OUT = 5000L;
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
//在这里使用安全管
// System.setSecurityManager(new MySecurityManager());
/**
* 1.使用Java代码原生判题(不使用任何框架实现)
* 2.使用docker沙箱判题
*
* 1) 获取到用户的代码,然后将用户的代码保存为文件
* 2) 编译代码,得到class文件
* 3) 执行代码,得到输出结果
* 4) 收集整理输出结果
* 5) 文件清理
* 6) 错误处理,提高程序的健壮性
*/
String code = executeCodeRequest.getCode();//用户提交的代码
String language = executeCodeRequest.getLanguage();//用户提交的语言
List<String> inputList = executeCodeRequest.getInputList();//用户题目的输入用例
//判断用户提交的代码中有没有黑名单的代码
// FoundWord foundWord = WORD_TREE.matchWord(code);
// if (foundWord != null){
// //如果不为空,说明有这个参数
// System.out.println("包含禁止词:"+foundWord.getFoundWord());
// return null;
// }
//1) 获取到用户的代码,然后将用户的代码保存为文件
File userCodeFile = saveCodeToFile(code);
//2) 编译代码,得到class文件
ExecuteMessage compileFileExecuteMessage = compileFile(userCodeFile);
System.out.println(compileFileExecuteMessage);
//3) 执行代码,得到输出结果
List<ExecuteMessage> executeMessageList = runFile(userCodeFile, inputList);
//4) 收集整理输出结果
ExecuteCodeResponse outputResponse = getOutputResponse(executeMessageList);
//5) 文件清理
boolean b = deleteFile(userCodeFile);
if (!b){
log.error("deleteFile error,userCodeFilePath = {}",userCodeFile.getAbsolutePath());
}
return outputResponse;
}
/**
* 1.将用户的代码保存为文件
* @param code
*/
public File saveCodeToFile(String code){
//获取到当前用户的工作目录
String userDir = System.getProperty("user.dir");
//File.separator 是为了兼容不同的系统的 \
String globalCodePathName = userDir + File.separator + GLOBAL_CODE_DIR_NAME;
//判断全局代码目录是否存在,没有则新建
if (!FileUtil.exist(globalCodePathName)){
FileUtil.mkdir(globalCodePathName);
}
//将用户提交的代码隔离
String userCodeParentPath = globalCodePathName+File.separator+ UUID.randomUUID();
//真正的用户路径
String userCodePath = userCodeParentPath + File.separator + GLOBAL_JAVA_CLASS_NAME;
//直接写入到程序中
File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8);
return userCodeFile;
}
/**
* 2.编译代码
* @param userCodeFile
*/
public ExecuteMessage compileFile(File userCodeFile){
String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsoluteFile());
try {
Process compileProcess = Runtime.getRuntime().exec(compileCmd);
ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
if (executeMessage.getExitValue() != 0){
throw new RuntimeException("编译错误");
}
return executeMessage;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 3.执行文件,获取执行结果列表
* @param userCodeFile
* @param inputList
*/
public List<ExecuteMessage> runFile(File userCodeFile, List<String> inputList){
String absolutePath = userCodeFile.getParentFile().getAbsolutePath();
//首先拿到输入用例进行遍历
List<ExecuteMessage> executeMessageList = new ArrayList<>();//用来存储输出信息
for(String inputArgs:inputList){
//如果需要更严格的内存限制的话,需要去系统层面设置,而不是设置JVM内存控制
//Linux系统,cgroup这个技术 实现对某个cpu或者内存的资源分配
String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s Main %s", absolutePath, inputArgs);
try {
Process runProcess = Runtime.getRuntime().exec(runCmd);
//开启线程
new Thread(()->{
try {
Thread.sleep(TIME_OUT);
System.out.println("超时了,中断");
runProcess.destroy();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(runProcess, "运行");
System.out.println(executeMessage);
executeMessageList.add(executeMessage);
} catch (IOException e) {
throw new RuntimeException("执行错误",e);
}
}
return executeMessageList;
}
/**
* 4.获取输出结果
* @param executeMessageList
*/
public ExecuteCodeResponse getOutputResponse(List<ExecuteMessage> executeMessageList){
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
List<String> outputList = new ArrayList<>();
//模拟一个最大值,便于判断是否超时
long maxTime = 0;
//模拟一个最大的内存值,获取最大的就行
long maxMemory = 0;
for (ExecuteMessage executeMessage : executeMessageList) {
String errorMessage = executeMessage.getErrorMessage();
if (StrUtil.isNotBlank(errorMessage)){
executeCodeResponse.setMessage(errorMessage);
//用户提交的代码执行存在错误
executeCodeResponse.setStatus(3);
break;
}
outputList.add(executeMessage.getMessage());
Long time = executeMessage.getTime();//程序执行时间
if (time!=null){
maxTime = Math.max(maxTime, time);
}
Long memory = executeMessage.getMemory();
if (memory!=null){
maxMemory = Math.max(maxMemory, memory);//取最大的内存
}
}
//正常运行结束
if (outputList.size() == executeMessageList.size()){
executeCodeResponse.setStatus(1);
}
executeCodeResponse.setOutputList(outputList);
JudgeInfo judgeInfo = new JudgeInfo();
judgeInfo.setMessage(outputList);
//要借助第三方库来进行实现,非常麻烦就不实现了,当然你可以自己去网上搜索一下
judgeInfo.setMemory(maxMemory / 1024); //将字节 b 转换成kb
judgeInfo.setTime(maxTime);
executeCodeResponse.setJudgeInfo(judgeInfo);
return executeCodeResponse;
}
/**
* 5. 删除文件
* @param userCodeFile
* @return
*/
public boolean deleteFile(File userCodeFile){
if (userCodeFile.getParentFile()!=null){
String absolutePath = userCodeFile.getParentFile().getAbsolutePath();
boolean del = FileUtil.del(absolutePath);
System.out.println("删除"+(del?"成功":"失败"));
return del;
}
return true;
}
//6) 错误处理,提高程序的健壮性
/**
* 获取错误响应
*/
private ExecuteCodeResponse getErrorResponse(Throwable e){
ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
executeCodeResponse.setOutputList(new ArrayList<>());
executeCodeResponse.setMessage(e.getMessage());
//设置状态码为2 说明错误
executeCodeResponse.setStatus(2);
executeCodeResponse.setJudgeInfo(new JudgeInfo());
return executeCodeResponse;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222