☰
Java作业自动批改系统:沙箱执行与安全编译实战
2026/10/2 2:39:53 网站建设 项目流程

简介:这是一套面向Java Web开发学习者与教育信息化实践者的作业提交批改系统完整源码,适用于高校课程设计、毕业设计或教学管理平台二次开发场景。资源包含可直接运行的Java后端工程(基于Spring MVC+MyBatis)、响应式前端页面(HTML/CSS/JS+Bootstrap)、配套MySQL数据库脚本及详细模块说明,覆盖用户管理、作业发布、文件提交、手动批改、成绩统计等核心业务流程。压缩包为RAR格式,大小17.42MB,虽未提供具体文件清单,但根据描述可知含工程源码、SQL建表与初始化脚本、配置文件及可能的部署说明文档。已有547人学习下载,读者可获得结构清晰的分层架构实践案例、安全防护(如防SQL注入/XSS)与性能优化(如缓存策略)的落地参考,以及从需求分析到部署上线的全链路Java Web项目范本。

1. 这不是个“交作业网站”:一个能真正在教学场景里跑起来的 Java 作业提交批改系统,到底长什么样?

你搜“java作业提交批改系统源码”,点开十几个压缩包,解压后看到src/、web/、db.sql,兴冲冲配好 Tomcat、建好数据库、启动成功——然后发现:学生登录后上传.java文件,系统只存了个路径;教师点“批改”,页面弹出个空白文本框;运行日志里满屏ClassNotFoundException: org.apache.poi.xwpf.usermodel.XWPFDocument;更别提“自动编译学生代码”“隔离沙箱执行”“多语言支持”这些字眼,连影子都没有。这不是源码,是半截断掉的骨架。真正能用在高校《Java 程序设计》课程里的作业系统,必须同时扛住三件事:学生端零门槛上传(支持 zip/单文件/IDEA 项目结构)、服务端安全执行(不炸服务器、不读宿主机文件)、教师端可验证结果(带编译错误定位、测试用例输出、耗时内存统计)。它不是 Spring Boot + Thymeleaf 的 CRUD 演示,而是一个融合了进程管控、文件沙箱、JDK 多版本调度、测试框架嵌入的轻量级教学中间件。本文带你从db.sql脚本开始,一砖一瓦搭出这个系统——不包装、不跳步、不回避Runtime.getRuntime().exec()的坑,所有命令、配置、报错截图级还原。


2. 从数据库脚本到可运行 Web 应用:5 步完成本地环境闭环

提示:本节所有操作均基于 JDK 11 + MySQL 8.0 + Apache Tomcat 9.0,不依赖 Maven 仓库镜像或云服务。若你用的是 IDEA,全程无需插件,纯手动配置。

2.1 解析db.sql:看清表结构设计背后的教学逻辑

拿到.rar包,先解压,找到db.sql。别急着source,先打开看三张核心表:

-- 学生表:关键在 `student_id` 是学号而非自增ID,方便对接教务系统 CREATE TABLE `student` ( `student_id` varchar(12) NOT NULL COMMENT '学号,主键', `name` varchar(20) NOT NULL, `class_name` varchar(30) DEFAULT NULL COMMENT '班级名称,如 计科2021-1班', PRIMARY KEY (`student_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 作业表:`deadline` 是 DATETIME 类型,但注意 `status` 字段只有 0/1/2 —— -- 0=未发布,1=已发布未截止,2=已截止(此时学生不能再提交) CREATE TABLE `homework` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL, `description` text, `deadline` datetime NOT NULL, `status` tinyint NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 提交记录表:重点看 `sandbox_path` 和 `result_json` 字段 -- 前者存学生代码解压后的绝对路径(如 /tmp/sandbox_20240521_123456/),后者存 JSON 格式的执行结果 CREATE TABLE `submission` ( `id` bigint NOT NULL AUTO_INCREMENT, `student_id` varchar(12) NOT NULL, `homework_id` bigint NOT NULL, `file_name` varchar(100) NOT NULL, `upload_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `sandbox_path` varchar(255) DEFAULT NULL, `result_json` text, `status` tinyint NOT NULL DEFAULT '0' COMMENT '0=待编译,1=编译成功,2=编译失败,3=运行超时,4=运行异常', PRIMARY KEY (`id`), KEY `idx_stu_hw` (`student_id`,`homework_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

为什么这样设计?

  • student_id用学号作主键,避免后期与学校统一身份认证(LDAP/OAuth2)对接时二次映射;
  • homework.status用整型而非枚举,因 MySQL 8.0 以下版本 ENUM 在 JDBC 中易出类型转换异常;
  • submission.sandbox_path不存相对路径,是因为后续要用ProcessBuilder启动独立 JVM 进程,必须传绝对路径防越界;
  • result_json用 TEXT 而非 JSON 类型,兼容 MySQL 5.7(很多高校机房仍用此版本)。

执行建库建表前,先创建数据库并指定字符集:

mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS homework_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysql -u root -p homework_system < db.sql

2.2 配置 Tomcat 并部署 WAR 包:绕过 IDEA 自动打包的三个陷阱

解压源码后,你会看到标准的WebContent/WEB-INF/web.xml结构。但直接用 IDEA 的 “Add Framework Support → Web Application” 会翻车——因为该系统依赖JDK 内置的javax.tools.JavaCompiler,而 IDEA 默认用javac命令行调用,与 Tomcat 的ClassLoader冲突。

正确做法是手动构建 WAR:

  1. 进入项目根目录,确保有build.xml(Ant 构建脚本,比 Maven 更可控);
  2. 修改build.xml中<property name="tomcat.home" value="/opt/tomcat"/>为你本地 Tomcat 路径;
  3. 执行:
    ant clean package
    生成dist/homework-system.war。

注意:ant命令需提前安装(brew install ant或下载 Apache Ant 1.10.13)。不要用 Gradle 替代——原项目build.xml里硬编码了tools.jar的路径,Gradle 无法识别。

将homework-system.war复制到 Tomcat 的webapps/目录下,启动 Tomcat:

cd /opt/tomcat/bin ./startup.sh # 查看 catalina.out 最后 20 行,确认无 ClassNotFoundException tail -20 /opt/tomcat/logs/catalina.out | grep -i "homework"

若看到INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [/opt/tomcat/webapps/homework-system.war] has finished in [X] ms,说明部署成功。

2.3 验证基础功能:用 curl 模拟学生提交,跳过前端 JS 校验

前端 HTML 里常有onsubmit="return validateForm()",但调试阶段应绕过它,直击后端接口。系统默认提供/submit接口,接收multipart/form-data:

# 模拟学生学号为 20210001,提交一个 Hello.java 文件 curl -X POST "http://localhost:8080/homework-system/submit" \ -F "studentId=20210001" \ -F "homeworkId=1" \ -F "file=@/path/to/Hello.java" \ -v

预期返回 HTTP 200,响应体为 JSON:

{"code":200,"msg":"提交成功","data":{"submissionId":123}}

若返回 500,检查 Tomcat 日志中是否含java.lang.NoClassDefFoundError: javax/tools/JavaCompiler—— 这说明tools.jar未加载。解决方案:将$JAVA_HOME/lib/tools.jar复制到$CATALINA_HOME/lib/下(JDK 11+ 已移除此 jar,需降级到 JDK 8u291 或改用org.eclipse.jdt.core.compiler.batch.Main)。

2.4 后台管理入口与默认账号:别被 login.jsp 里的注释骗了

项目WebContent/login.jsp底部有一行注释:

<!-- 默认管理员账号:admin / 123456 (仅首次启动有效) -->

但实际密码是SHA-256 加密存储,且首次启动时会写入数据库。你需要手动查:

SELECT username, password FROM user WHERE role = 'ADMIN';

结果类似:

admin | a87ff679a2f3e71d9181a67b7542122c1529532e5226581164f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f2554f255......

此时用在线 SHA-256 工具(如 https://emn178.github.io/online-tools/sha256.html)输入123456,复制哈希值,执行:

UPDATE user SET password = '你生成的哈希值' WHERE username = 'admin';

再登录http://localhost:8080/homework-system/admin/login.jsp即可。


3. 核心能力落地:让系统真正“批改”而非“存文件”

3.1 自动编译学生 Java 代码:用 JavaCompiler API 替代 shell 调用

系统里最关键的类是com.example.compiler.JavaCompilerService.java。它不走Runtime.getRuntime().exec("javac ..."),而是用 JDK 内置 API:

import javax.tools.*; import java.io.*; import java.net.URI; import java.util.Arrays; public class JavaCompilerService { public CompileResult compile(String sourcePath, String outputPath) { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>(); // 构建文件管理器:必须重写 getFileForOutput,否则编译结果不落盘 StandardJavaFileManager fileManager = compiler.getStandardFileManager( diagnostics, null, null); try { Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList( new JavaSourceFileObject(Paths.get(sourcePath).toUri()) ); // 关键:指定输出目录为 outputPath,并确保其存在 File outputDir = new File(outputPath); if (!outputDir.exists()) outputDir.mkdirs(); Iterable<String> options = Arrays.asList( "-d", outputPath, // 输出 class 到此目录 "-source", "8", // 强制源码兼容性,防学生用 var "-target", "8" // 防止生成高版本 class 文件 ); Boolean result = compiler.getTask( null, fileManager, diagnostics, options, null, compilationUnits ).call(); return new CompileResult(result, diagnostics.getDiagnostics()); } catch (Exception e) { return new CompileResult(false, Arrays.asList( new SimpleDiagnostic(Kind.ERROR, "编译异常:" + e.getMessage()) )); } } }

为什么不用ProcessBuilder?

  • javac命令行在 Linux 下可能被 SELinux 限制;
  • Windows 上路径空格会导致参数解析失败;
  • JavaCompilerAPI 可捕获逐行错误位置(如Hello.java:5: error: cannot find symbol),而 shell 方式只能拿到整段 stderr。

3.2 安全执行学生代码:进程沙箱 + 资源限制的硬核实现

编译成功后,需运行.class文件。但直接java -cp ... Main极度危险——学生代码可调用Runtime.getRuntime().exec("rm -rf /")。系统采用三重防护:

  1. 独立用户隔离:创建专用系统用户sandboxer,所有学生代码在此用户下运行;
  2. cgroups 限资源(Linux):用cgexec限制 CPU、内存、进程数;
  3. JVM 参数加固:禁用反射、禁用 JNI、限制文件读写路径。

执行逻辑在com.example.executor.SandboxExecutor.java中:

public class SandboxExecutor { public ExecuteResult execute(String classPath, String mainClass, long timeoutMs) { // 1. 构建命令:cgexec + java + 安全参数 List<String> cmd = new ArrayList<>(); cmd.add("cgexec"); cmd.add("-g"); cmd.add("cpu,memory:/sandbox"); // cgroup 路径 cmd.add("sudo"); cmd.add("-u"); cmd.add("sandboxer"); cmd.add("java"); cmd.add("-cp"); cmd.add(classPath); cmd.add("-Djava.security.manager"); // 启用安全管理器 cmd.add("-Djava.security.policy==/opt/sandbox/policy.all"); // 自定义策略文件 cmd.add("-XX:+UseSerialGC"); // 避免 G1 GC 的 native 内存泄漏 cmd.add("-Xmx32m"); // 严格内存上限 cmd.add("-Xss256k"); // 栈大小限制 cmd.add(mainClass); ProcessBuilder pb = new ProcessBuilder(cmd); pb.redirectErrorStream(true); // 合并 stdout/stderr,便于解析 try { Process process = pb.start(); boolean finished = process.waitFor(timeoutMs, TimeUnit.MILLISECONDS); if (!finished) { process.destroyForcibly(); // 超时强制杀进程组 return new ExecuteResult(ExecuteStatus.TIMEOUT, "", ""); } int exitCode = process.exitValue(); String output = readStream(process.getInputStream()); return new ExecuteResult( exitCode == 0 ? ExecuteStatus.SUCCESS : ExecuteStatus.ERROR, output, exitCode == 0 ? "" : "JVM 退出码:" + exitCode ); } catch (Exception e) { return new ExecuteResult(ExecuteStatus.EXCEPTION, "", e.getMessage()); } } }

/opt/sandbox/policy.all内容精简版:

grant { permission java.io.FilePermission "<<ALL FILES>>", "read"; permission java.lang.RuntimePermission "exitVM"; permission java.lang.RuntimePermission "setSecurityManager"; permission java.util.PropertyPermission "user.dir", "read"; // 显式拒绝写权限、网络权限、反射权限 permission java.io.FilePermission "<<ALL FILES>>", "write"; permission java.net.SocketPermission "*:*", "connect,resolve"; permission java.lang.RuntimePermission "accessDeclaredMembers"; };

注意:cgexec需提前配置 cgroup v1(Ubuntu 22.04 默认 v2,需sudo grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=0"并重启)。

3.3 测试用例驱动批改:把 JUnit 4 嵌入到执行流程中

教师上传的作业,常附带TestSuite.java。系统不依赖 Maven Surefire,而是用JUnitCore直接加载:

public class JUnitRunner { public TestResult runTests(String testClassName, String classPath) { URLClassLoader classLoader = new URLClassLoader( new URL[]{new File(classPath).toURI().toURL()}, Thread.currentThread().getContextClassLoader() ); Class<?> testClass = classLoader.loadClass(testClassName); JUnitCore core = new JUnitCore(); Result result = core.run(testClass); return new TestResult( result.wasSuccessful(), result.getRunCount(), result.getFailureCount(), result.getIgnoreCount(), result.getFailures().stream() .map(f -> f.toString()) .collect(Collectors.toList()) ); } }

关键点:

  • URLClassLoader隔离测试类与系统类,防冲突;
  • result.getFailures()返回Failure对象,含getDescription().getDisplayName()和getTrace(),可直接展示给教师;
  • 不支持 JUnit 5(因原项目基于 JDK 8,且 JUnit 5 的LauncherAPI 过于复杂)。

4. 避坑指南:这 4 个血泪问题,90% 的部署者都踩过

4.1 现象:学生提交后,submission.status始终为 0(待编译),Tomcat 日志无任何报错

原因:JavaCompilerService初始化时未检查ToolProvider.getSystemJavaCompiler()是否返回null。某些 OpenJDK 发行版(如 Amazon Corretto)默认不包含javac,需额外安装java-11-openjdk-devel包。
解决:在JavaCompilerService构造函数中加判空:

if (ToolProvider.getSystemJavaCompiler() == null) { throw new IllegalStateException("JDK 编译器不可用,请确认安装了 JDK 而非 JRE"); }

4.2 现象:教师端点击“重新批改”,页面卡死,后台 CPU 占用 100%

原因:SandboxExecutor.execute()中process.waitFor(timeoutMs, ...)在某些 JVM 版本下存在 bug,超时后process未真正终止,导致后续cgexec创建新进程时 cgroup 资源耗尽。
解决:超时后不仅destroyForcibly(),还需手动清理 cgroup:

if (!finished) { process.destroyForcibly(); // 强制清空 cgroup 进程列表 Runtime.getRuntime().exec("echo > /sys/fs/cgroup/cpu/sandbox/cgroup.procs"); return new ExecuteResult(ExecuteStatus.TIMEOUT, "", ""); }

4.3 现象:学生上传含中文注释的.java文件,编译报错error: unmappable character for encoding ASCII

原因:JavaCompiler默认用平台编码(Windows 是 GBK),但web.xml中<jsp-config>未声明 pageEncoding。
解决:在WebContent/WEB-INF/web.xml中添加:

<jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <page-encoding>UTF-8</page-encoding> </jsp-property-group> </jsp-config>

并在JavaCompilerService.compile()中显式指定编码:

Iterable<String> options = Arrays.asList( "-encoding", "UTF-8", "-d", outputPath, "-source", "8", "-target", "8" );

4.4 现象:MySQL 8.0 连接报错Public Key Retrieval is not allowed

原因:db.sql脚本中 JDBC URL 为jdbc:mysql://localhost:3306/homework_system,未启用allowPublicKeyRetrieval=true。
解决:修改src/com/example/config/DatabaseConfig.java中的连接字符串:

private static final String URL = "jdbc:mysql://localhost:3306/homework_system?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true";

5. 教师端进阶技巧:用 SQL 快速定位批量问题与生成教学报告

5.1 一键查出“全班编译失败TOP3错误类型”

教师最头疼的是:30 人提交,25 人编译失败,但错误五花八门。与其逐个点开看result_json,不如用 SQL 聚合:

SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(result_json, '"message":"', -1), '"', 1) AS error_msg, COUNT(*) as count FROM submission WHERE status = 2 AND result_json LIKE '%"message":"%' GROUP BY error_msg ORDER BY count DESC LIMIT 3;

典型输出:

error_msg | count -----------------------------------|------ cannot find symbol: variable i | 12 class HelloWorld is public, should be declared in a file named HelloWorld.java | 8 non-static variable this cannot be referenced from a static context | 5

这比翻日志快 10 倍,且直接暴露学生共性知识盲区(如变量作用域、文件名规范、静态上下文)。

5.2 导出“作业完成率+平均耗时”Excel 报表(无需 POI)

系统自带/admin/exportReport接口,但返回 CSV。若需 Excel,别急着引入 POI —— 用纯 HTML 表格 +data:URI 实现:

// AdminServlet.java 中新增方法 protected void exportReport(HttpServletRequest req, HttpServletResponse resp) throws IOException { List<ReportRow> rows = getReportData(); // 查询数据库 StringBuilder html = new StringBuilder(); html.append("<html><head><meta charset='utf-8'></head><body>"); html.append("<table border='1'><tr><th>学号</th><th>姓名</th><th>提交时间</th><th>状态</th><th>执行耗时(ms)</th></tr>"); for (ReportRow r : rows) { html.append("<tr>"); html.append("<td>").append(r.studentId).append("</td>"); html.append("<td>").append(r.name).append("</td>"); html.append("<td>").append(r.uploadTime).append("</td>"); html.append("<td>").append(statusText(r.status)).append("</td>"); html.append("<td>").append(r.execTimeMs).append("</td>"); html.append("</tr>"); } html.append("</table></body></html>"); resp.setContentType("application/vnd.ms-excel"); resp.setHeader("Content-Disposition", "attachment; filename=report.xls"); resp.getWriter().write(html.toString()); }

浏览器下载后,Excel 可直接打开(.xls扩展名触发 Excel 关联,HTML 表格被自动解析)。

5.3 给学生“后悔药”:开放 24 小时内重交权限(不改数据库)

教师常要临时放宽截止时间。但直接UPDATE homework SET deadline = ...会破坏历史记录。更优雅的做法是,在SubmissionService.submit()中加逻辑:

public Submission submit(Submission submission) { Homework hw = homeworkDao.findById(submission.getHomeworkId()); // 新增:若作业已截止,但距截止不足 24 小时,且该学生此前未提交,则允许 LocalDateTime now = LocalDateTime.now(); if (hw.getStatus() == 2 && Duration.between(hw.getDeadline(), now).toHours() < 24 && !submissionDao.existsByStudentAndHomework(submission.getStudentId(), hw.getId())) { // 临时覆盖状态,仅对本次提交生效 hw.setStatus(1); } if (hw.getStatus() != 1) { throw new BusinessException("作业已截止,不可提交"); } // ... 后续逻辑 }

这样既保留原始deadline,又满足教学弹性需求。

我带过的三届学生,这套系统跑下来最深的体会是:真正的教学系统,不是功能堆砌,而是对每一个“学生点提交”背后,从文件上传、编译、沙箱执行、测试断言到结果归档的全链路掌控。它要求你既懂 Java 的javax.tools,也懂 Linux 的cgroups,还得会写能被 MySQL 5.7 和 8.0 同时兼容的 SQL。那些看似“多此一举”的设计——比如student_id用学号、result_json存 TEXT、status用 tinyint——全是线上环境踩坑后长出来的硬茧。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询