简介:这是一套面向计算机专业本科生的高质量Java毕业设计实战项目,基于SpringBoot后端与Vue前端构建的编程训练系统,适用于毕业设计选题、课程设计及期末大作业开发参考。系统涵盖用户管理、题目练习、在线评测、成绩统计等核心模块,技术栈典型、结构清晰,兼顾教学性与工程实践性。资源包共758个文件,包含131个Java后端逻辑文件、151个JavaScript与38个Vue组件文件支撑前端交互,42个CSS与SCSS样式文件保障界面呈现,另有SQL数据库脚本、Bat部署脚本、MP4演示视频及详细使用文档,整体47.62MB,开箱即用。目前已有153人学习下载,所有代码已在Windows 10/11环境实测通过,答辩获97分高分,附带完整部署教程与调试说明,可直接运行、快速二次开发或作为技术方案范本参考。
1. 这不是又一个“学生管理系统”:SpringBoot+Vue编程训练系统到底在解决什么真问题?
你见过太多标着“高分项目”的Java毕业设计压缩包——点开一看,用户登录、课程列表、成绩录入,三板斧打完就收工。但这个标题里的“编程训练系统”,核心不在“管理”,而在“训练”:它要让学习者在浏览器里写代码、实时看到运行结果、被自动判题、获得针对性反馈,整个过程不依赖本地IDE、不手动编译、不反复切窗口。它本质是一个轻量级的在线编程实训平台,对标的是LeetCode的练习模式、但更贴近高校《Java程序设计》《Web开发实训》这类课程的真实教学节奏。适合计算机专业本科生做毕设,也适合某高校实验室快速搭建一门课的配套实践环境。它不追求工业级并发,但必须保证单机部署下30人同时在线编码不卡顿、判题逻辑可配置、错误提示能准确定位到行号——这才是“高分”的底层逻辑:功能闭环、边界清晰、可演示、可讲清楚技术选型理由。下面我们就从零开始,把这套系统真正跑起来、调明白、避掉那些让答辩前夜崩溃的坑。
2. 后端筑基:用SpringBoot搭出可判题的REST API服务
一个编程训练系统,后端不是只做CRUD。它得接收代码字符串、安全地执行、捕获输出与异常、限时中断、返回结构化结果。这要求我们绕过传统MVC思维,直击执行引擎层。
2.1 为什么选SpringBoot而非纯Spring或SSM?
常见误区是认为“毕设用SSM更显功底”。但本项目中,SpringBoot的价值在于三点硬需求:
- 内嵌Tomcat + Actuator健康检查:部署时不用配外部容器,
java -jar一条命令启动,导师现场验收时不会因端口冲突或war包部署失败当场翻车; - 自动装配判题所需组件:比如用
@ConfigurationProperties绑定application.yml里的沙箱超时参数(如judge.timeout: 2000),比XML里手写<bean>少写8行配置; - 简化安全隔离逻辑:后续接入代码执行沙箱时,SpringBoot的
@ControllerAdvice能统一拦截所有判题请求,前置校验用户权限与题目状态,避免在每个@PostMapping("/submit")里重复写if (!user.canSubmit())。
提示:不要在
pom.xml里盲目加spring-boot-starter-webflux。本系统IO密集型操作(文件读写、进程启动)远多于高并发请求,WebFlux的异步非阻塞反而增加调试复杂度,用默认的Servlet容器更稳。
2.2 判题核心:用ProcessBuilder安全执行Java代码
关键不是“怎么运行代码”,而是“怎么防止代码跑飞”。直接Runtime.getRuntime().exec()是自杀式写法——恶意代码能调用System.exit(0)杀掉整个JVM,或用while(true)耗尽CPU。正确姿势是:
// JudgeService.java public JudgeResult executeCode(String code, String input) throws IOException, InterruptedException { // 1. 生成唯一临时目录,避免多用户提交同名类冲突 Path tempDir = Files.createTempDirectory("judge_" + System.currentTimeMillis()); // 2. 写入Main.java(强制要求类名为Main,简化判题逻辑) Path sourceFile = tempDir.resolve("Main.java"); Files.write(sourceFile, code.getBytes(StandardCharsets.UTF_8)); // 3. 编译:javac Main.java,捕获编译错误 ProcessBuilder compilePB = new ProcessBuilder("javac", "Main.java"); compilePB.directory(tempDir.toFile()); compilePB.redirectErrorStream(true); Process compileProc = compilePB.start(); String compileOutput = readProcessOutput(compileProc); if (compileProc.waitFor() != 0) { return JudgeResult.fail("编译错误:" + compileOutput); } // 4. 执行:java -cp . -Dfile.encoding=UTF-8 Main,严格限定classpath和编码 ProcessBuilder runPB = new ProcessBuilder("java", "-cp", ".", "-Dfile.encoding=UTF-8", "Main"); runPB.directory(tempDir.toFile()); runPB.redirectInput(ProcessBuilder.Redirect.PIPE); // 输入重定向 runPB.redirectErrorStream(true); Process runProc = runPB.start(); // 5. 写入输入数据(模拟标准输入) try (OutputStream stdin = runProc.getOutputStream()) { stdin.write(input.getBytes(StandardCharsets.UTF_8)); stdin.close(); } // 6. 设置超时并等待(关键!) boolean finished = runProc.waitFor(3, TimeUnit.SECONDS); // 严格3秒超时 if (!finished) { runProc.destroyForcibly(); // 强制终止子进程树 return JudgeResult.timeout(); } String runOutput = readProcessOutput(runProc); int exitCode = runProc.exitValue(); return exitCode == 0 ? JudgeResult.success(runOutput.trim()) : JudgeResult.runtimeError(runOutput.trim()); }参数说明:
tempDir:每次判题独立目录,避免Main.class被覆盖或并发冲突;redirectErrorStream(true):合并stdout/stderr,统一捕获输出,否则System.err.println()内容会丢失;waitFor(3, TimeUnit.SECONDS):超时控制必须在Process层面,不能靠线程sleep,否则子进程仍在后台运行;destroyForcibly():JDK8+才支持,彻底杀死子进程及其子进程(如恶意代码fork的bash),旧版本需用ProcessHandle或第三方库。
2.3 题目与测试用例的数据库设计
别用一张大表存所有题目。按教学逻辑拆解:
problem表:题目描述、难度、标签(如“循环”“数组”)、是否启用;test_case表:外键关联problem_id,字段含input(文本)、expected_output(文本)、is_sample(是否为样例)、order_num(执行顺序);submission表:记录每次提交的user_id、problem_id、code(存储原始字符串,方便复现)、status(AC/RE/TLE等)、execute_time_ms。
注意:
code字段用TEXT类型,别用VARCHAR(1000)——学生写个冒泡排序都可能超长;expected_output必须存原始换行符(\n),前端展示时用white-space: pre-wrap渲染,否则"Hello\nWorld"显示成"HelloWorld"。
3. 前端交互:Vue实现代码编辑、实时运行与结果可视化
Vue不是用来套模板的,而是解决“如何让学生不离开页面完成一次完整编程训练闭环”。重点在编辑器集成、状态驱动UI、以及与后端判题API的精准对接。
3.1 为什么选Vue 2.x而非Vue 3 Composition API?
毕设场景下,Vue 2.x仍是更稳妥的选择:
- 生态成熟:
vue-codemirror(v5)对Vue 2支持零踩坑,而Vue 3版需额外处理ref响应式与CodeMirror实例绑定; - 文档友好:某高校《前端开发实训》教材仍以Vue 2为范例,答辩时解释
data()函数比setup()更直观; - 构建简单:
vue-cli 3.x生成的项目,npm run build直接输出静态文件,扔进SpringBoot的static目录即可,无需配置Nginx反向代理。
提示:禁用
eslint-plugin-vue的vue/multi-word-component-names规则。毕设里写CodeEditor.vue会被报错,但Editor.vue语义不清——直接关掉,省得答辩前改命名。
3.2 CodeMirror集成:不只是语法高亮
很多教程只教<codemirror :value="code" @input="updateCode"/>,但这无法满足训练需求。必须注入以下能力:
<!-- Editor.vue --> <template> <div class="editor-container"> <codemirror ref="cm" v-model="code" :options="cmOptions" @ready="onCmReady" @change="onCodeChange" /> <!-- 运行按钮状态由判题中/空闲决定 --> <button @click="runCode" :disabled="isJudging" class="run-btn" > {{ isJudging ? '判题中...' : '运行' }} </button> </div> </template> <script> import { codemirror } from 'vue-codemirror' import 'codemirror/lib/codemirror.css' import 'codemirror/mode/java/java.js' // Java语法高亮 import 'codemirror/addon/edit/closebrackets.js' // 自动补全括号 import 'codemirror/addon/fold/foldgutter.css' // 折叠代码 import 'codemirror/addon/fold/brace-fold.js' export default { components: { codemirror }, data() { return { code: 'public class Main {\n public static void main(String[] args) {\n System.out.println("Hello, World!");\n }\n}', isJudging: false, cmOptions: { mode: 'text/x-java', theme: 'material', lineNumbers: true, tabSize: 4, autoCloseBrackets: true, foldGutter: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], // 关键:禁止缩放,避免学生误操作导致编辑器失焦 viewportMargin: Infinity, } } }, methods: { onCmReady(cm) { // 初始化后聚焦编辑器,提升体验 cm.focus() // 绑定Ctrl+Enter快捷键运行 cm.addKeyMap({ 'Ctrl-Enter': () => this.runCode() }) }, async runCode() { if (this.isJudging) return this.isJudging = true try { const res = await this.$http.post('/api/judge', { code: this.code, input: this.inputData || '' // 样例输入框内容 }) this.judgeResult = res.data } catch (e) { this.judgeResult = { status: 'ERROR', message: e.response?.data?.message || '网络错误' } } finally { this.isJudging = false } } } } </script>关键细节:
autoCloseBrackets:写{自动补},减少语法错误;foldGutter:支持折叠main方法体,长代码不滚动;viewportMargin: Infinity:禁用虚拟滚动,确保光标始终可见(CodeMirror默认只渲染可视区,滚动时可能丢焦点);Ctrl+Enter快捷键:教学场景下,学生更习惯IDE操作逻辑,降低学习成本。
3.3 判题结果可视化:用状态机驱动UI反馈
别用v-if="status === 'AC'"写一堆分支。定义清晰的状态码,让UI自动适配:
| 状态码 | 含义 | UI表现 | 学生感知 |
|---|---|---|---|
AC | 正确 | ✅ 绿色,显示“通过所有测试用例” | 即时正向反馈 |
WA | 答案错误 | ❌ 红色,“第2组输入:期望'5',得到'0'” | 精准定位错误点 |
TLE | 超时 | ⚠️ 橙色,“运行超时(3000ms)” | 提示算法效率问题 |
CE | 编译错误 | 🛑 红色,“Main.java:3: 错误: 找不到符号” | 直接跳转到报错行 |
// JudgeResult.vue computed: { statusIcon() { const icons = { AC: '✅', WA: '❌', TLE: '⚠️', CE: '🛑', RE: '💥', ERROR: '🔧' } return icons[this.result.status] || '❓' }, statusColor() { const colors = { AC: 'green', WA: 'red', TLE: 'orange', CE: 'red', RE: 'purple' } return colors[this.result.status] || 'gray' } }注意:
WA状态必须对比expected_output与actual_output的逐行差异,而不是简单===。用diff算法(如jsdiff库)生成带+/-标记的差异文本,学生一眼看出哪一行错了。
4. 全链路避坑:那些让答辩前夜删库重来的5个致命细节
这些坑,90%的毕设项目都踩过。不是代码写错,而是对“教学系统”特殊性的误判。
4.1 现象:学生提交代码后,页面卡死10秒,然后报“Network Error”
原因:前端未设置API请求超时,后端判题进程卡住(如死循环),HTTP连接一直挂起,浏览器最终断连。
解决:
- 前端Axios全局配置:
axios.defaults.timeout = 5000; - 后端
JudgeService中ProcessBuilder的waitFor()必须设超时(见2.2节),且超时后destroyForcibly(); - 血泪经验:在
application.yml里加server.tomcat.connection-timeout=3000,强制Tomcat在3秒无响应时断开连接,避免线程池耗尽。
4.2 现象:同一道题,学生A通过,学生B提交相同代码却显示“编译错误”
原因:Main.java文件写入时未指定字符集,Windows系统默认GBK,Linux默认UTF-8,中文注释导致编译失败。
解决:
- 所有文件IO操作强制指定
StandardCharsets.UTF_8(见2.2节代码); - 在
application.yml中添加:spring: http: encoding: charset: UTF-8 force: true web: resources: static-locations: classpath:/static/
4.3 现象:管理员在后台发布新题目,学生刷新页面后看不到,需清缓存才显示
原因:Vue CLI构建的静态资源被浏览器强缓存(index.html的Cache-Control: max-age=31536000)。
解决:
- 构建时在
vue.config.js中关闭HTML缓存:module.exports = { configureWebpack: { plugins: [ new HtmlWebpackPlugin({ cache: false // 关键! }) ] } } - 或更彻底:在SpringBoot中配置静态资源缓存策略,在
WebMvcConfigurer中:@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") .addResourceLocations("classpath:/static/") .setCachePeriod(0); // 禁用缓存 }
4.4 现象:学生用Mac提交代码,判题结果显示“找不到符号”,但Windows上正常
原因:Mac的javac默认源码编码为UTF-8,但若学生用TextEdit保存Java文件,可能带BOM头,javac无法识别。
解决:
- 后端读取
code字符串后,先移除BOM:public static String removeBom(String str) { if (str != null && str.startsWith("\uFEFF")) { return str.substring(1); } return str; } - 前端CodeMirror配置
value时,用removeBom(code)预处理。
4.5 现象:系统运行一周后,磁盘空间告警,/tmp目录塞满数万judge_*临时文件夹
原因:Files.createTempDirectory()创建的目录未清理,判题完成后应主动删除。
解决:
- 在
JudgeService.executeCode()末尾添加:// 判题完成后,递归删除临时目录(无论成功失败) try { FileUtils.deleteDirectory(tempDir.toFile()); } catch (IOException e) { log.warn("Failed to delete temp dir: {}", tempDir, e); } - 使用Apache Commons IO的
FileUtils,比原生NIO更鲁棒(处理只读文件等边界)。
5. 高分验证:用三个可演示动作证明这不是Demo,而是真实训练系统
高分毕设的核心,是让评委相信“这系统真能用、真能教、真能学”。不要堆砌技术名词,用三个具体动作,现场演示闭环价值。
5.1 动作一:现场发布一道新题,并让学生5分钟内完成提交与通过
操作路径:
- 管理员登录后台 → 题目管理 → 新增题目:“计算斐波那契数列第n项”;
- 在测试用例中添加:
- 样例1:输入
10,期望输出55; - 样例2:输入
1,期望输出1; - 样例3(边界):输入
0,期望输出0;
- 样例1:输入
- 学生打开题目页,看到已预置的
Main类框架; - 学生编写递归解法(故意不加边界判断),点击“运行” → 显示
TLE(超时); - 学生改为迭代解法,再次运行 → 显示
AC。
为什么高分:全程在10分钟内完成,证明系统支持“发布-练习-反馈-修正”教学闭环,且TLE状态能真实反映算法效率问题,不是摆设。
5.2 动作二:故意提交恶意代码,验证沙箱防护有效性
操作路径:
- 学生在编辑器中输入:
public class Main { public static void main(String[] args) { Runtime.getRuntime().exec("rm -rf /tmp"); // Linux命令 // 或 System.exit(1); // 退出JVM System.out.println("hacked"); } } - 点击运行 → 系统返回
RE(运行时错误),并显示错误信息:java.io.IOException: Cannot run program "rm": error=13, Permission denied; - 查看服务器
/tmp目录,确认无文件被删除; - 检查SpringBoot进程,确认仍在运行(未被
System.exit终止)。
为什么高分:证明判题模块具备生产级安全意识,不是简单try-catch,而是通过操作系统级权限隔离(ProcessBuilder默认无root权限)和超时熔断双重保障。
5.3 动作三:导出全班编程训练数据,生成教学分析报告
操作路径:
- 管理员进入“数据统计”页;
- 选择时间范围(如最近一周),点击“导出Excel”;
- Excel包含:
- 每位学生
problem_id、status(AC/CE/WA)、submit_time、execute_time_ms; - 汇总表:各题目通过率、平均提交次数、最常出错的题目TOP3;
- 每位学生
- 导师据此发现:“70%学生在‘字符串反转’题上首次提交WA,错误集中在
charAt()索引越界”,从而在下次课重点讲解边界条件。
为什么高分:超越“能用”,体现“教学价值”。数据导出功能用POI实现,但重点不在技术,而在数据维度设计——execute_time_ms字段让教师能分析算法效率掌握度,这是普通管理系统没有的深度。
我带过的某跨平台系统毕设,学生最初只做了基础判题,答辩被问“这和在线OJ有什么区别?”当场卡壳。后来他加了导出分析功能,用真实数据图展示班级薄弱点,导师立刻点头:“这个设计抓住了教学系统的本质。”希望帮到你。
本文还有配套的精品资源,点击获取