ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于Spring Boot与MyBatis Plus构建高校代码作业查重系统

2026/9/4 18:18:19 拓冰建站 浏览量
基于Spring Boot与MyBatis Plus构建高校代码作业查重系统 简介本资源是一个面向高校计算机专业师生的代码作业查重系统后端源码工程聚焦课程教学中作业原创性核查的实际需求适用于Java Web开发学习者、课程设计实践者及教学管理系统开发者。压缩包共168个文件含95个Java核心业务类如TeacherController、StuWorkServiceImpl、21个Vue前端组件、16个MyBatis Plus映射XML、11个SQL建表与初始化脚本以及yml配置、JWT鉴权与JPlag查重依赖jar等关键文件整体仅2.36MB结构紧凑、模块职责清晰。已有126人下载学习可直接导入IDE运行完整覆盖用户角色管理学生/教师/管理员、作业布置与提交支持代码及PDF、基于JPlag的自动化查重、教师在线评分与反馈、小组作业分组管理等全流程功能配套Swagger接口文档与Redis缓存优化实践是Spring BootMyBatis Plus技术栈落地教学场景的典型参考案例。1. 项目背景与核心价值为什么我们需要一个代码作业查重系统在高校计算机相关专业的教学过程中编程作业是检验学生理解与实践能力的重要手段。然而一个长期困扰着教师们的难题是如何高效、准确地判断学生提交的代码是否存在抄袭或过度借鉴传统的人工比对方式面对动辄几十上百份的作业不仅耗时耗力而且极易因视觉疲劳和主观判断导致疏漏。更棘手的是学生们“借鉴”的手段也日趋“高明”从简单的变量名替换、注释删除到调整代码结构、改变循环方式使得肉眼比对几乎失效。正是在这样的背景下一个自动化的代码作业查重系统应运而生。它的核心价值远不止于“抓抄袭”这么简单。首先它极大地解放了教师的劳动力将教师从繁重的重复性劳动中解放出来使其能更专注于教学设计和个性化指导。其次它对学生形成了一种无形的威慑和正向引导鼓励独立思考与原创维护了学术诚信的底线。最后通过系统性的查重分析教师可以宏观地了解班级整体的代码风格、常见错误模式甚至发现某些具有共性的理解误区从而反哺教学。我之所以选择基于 Spring Boot 和 MyBatis Plus 来构建这样一个系统是经过深思熟虑的。Spring Boot 的“约定大于配置”理念让我们能快速搭建起一个健壮、可扩展的后端服务无需在繁琐的 XML 配置上耗费精力。而 MyBatis Plus 作为 MyBatis 的增强工具其强大的 CRUD 封装和条件构造器使得处理作业数据、学生信息、查重记录等实体操作变得异常简洁高效。这套技术栈成熟、稳定、社区活跃是快速实现业务逻辑的理想选择。接下来我将从零开始拆解这个系统的设计与实现分享其中的技术细节和踩过的坑。2. 系统架构设计与技术选型考量一个完整的代码作业查重系统远不止是运行一个相似度算法那么简单。它需要处理用户教师/学生管理、作业任务发布、代码文件上传、预处理、特征提取、相似度计算、结果呈现与持久化等一系列环节。因此一个清晰、松耦合的架构是项目成功的基石。2.1 整体架构分层我设计的系统采用了经典的三层架构并在此基础上根据业务特点做了细化表示层 (Presentation Layer)负责与用户交互。考虑到教师可能需要复杂的操作界面如批量导入学生、配置查重参数、查看详细报告而学生端相对简单主要是提交作业我选择了Thymeleaf模板引擎来构建后端渲染的管理页面。它语法自然与 Spring Boot 集成无缝能方便地展示动态数据和结果。对于更轻量的 API 交互未来可能的移动端扩展系统也预留了 RESTful API 接口使用RestController注解即可轻松提供 JSON 数据。业务逻辑层 (Service Layer)这是系统的“大脑”。它不关心数据如何存储也不关心页面如何渲染只专注于核心业务规则的实现。在这一层我定义了以下几个核心服务UserService: 处理用户认证、授权如区分教师和学生角色。AssignmentService: 管理作业的生命周期包括创建、发布、截止时间控制、状态更新。SubmissionService: 处理学生提交的代码文件包括文件存储、元信息记录。PlagiarismCheckService(核心): 协调整个查重流程。它调用CodePreprocessor代码预处理、FeatureExtractor特征提取器和SimilarityCalculator相似度计算器来完成核心算法并最终生成PlagiarismReport查重报告。数据访问层 (Data Access Layer)使用MyBatis Plus作为 ORM 框架负责所有与数据库的交互。它的强大之处在于通用 Mapper为UserAssignment,Submission,Report等实体生成基础的增删改查方法无需编写 XML 或注解开发效率极高。条件构造器在实现复杂查询时例如“查询某次作业中所有相似度高于80%的记录”使用QueryWrapper可以以面向对象的方式构建动态 SQL避免了 SQL 拼接的安全风险和繁琐。分页插件一键集成物理分页在查看大量查重结果列表时体验流畅。2.2 核心查重流程的组件化设计查重是核心必须设计得灵活、可扩展。我采用了“策略模式”的思想将流程分解为独立的组件上传代码 - 预处理 - 特征提取 - 相似度计算 - 报告生成预处理 (CodePreprocessor)原始代码包含很多“噪音”比如注释、空白行、格式化差异空格 vs 制表符、甚至变量名。预处理的目标就是消除这些不影响逻辑的差异。我的实现包括去除所有单行/多行注释、标准化空白字符、可选地进行变量/函数名归一化将所有用户自定义标识符替换为通用占位符如var1,func1。特征提取 (FeatureExtractor)将清洗后的代码转化为可比较的数学表示。常见方法有基于令牌序列将代码解析成令牌流如关键字、运算符、标识符、字面量比较两个序列的相似度。实现简单对结构调整有一定抵抗力。基于抽象语法树构建 AST比较树的结构。更能抓住代码的逻辑本质但实现复杂计算开销大。基于控制流图适用于更复杂的逻辑相似度判断。 在本系统中我优先实现了基于令牌序列的方法因为它平衡了效果和复杂度。使用 ANTLR 或 JavaParser 等工具可以生成特定语言如 Java, Python的令牌流。相似度计算 (SimilarityCalculator)有了特征表示就需要一个度量标准。我实现了两种经典算法最长公共子序列 (LCS)比较两个令牌序列找出最长的、相同的、顺序一致的子序列。相似度 LCS长度 / 较长序列的长度。它对代码的局部修改如插入、删除几行不敏感。余弦相似度将令牌序列视为词袋构建词频向量然后计算两个向量夹角的余弦值。这种方法更全局但对顺序不敏感。 在实际使用中可以同时计算多种相似度加权平均或取最大值作为最终结果以提高准确性。报告生成 (ReportGenerator)将计算出的相似度矩阵每份作业与其他所有作业的相似度转化为可读的报告。报告需要高亮显示高度相似的代码对并指出相似的具体位置行号。这里涉及到代码的差分比对算法我借鉴了文本比较工具如diff的思想用于在生成的 HTML 报告中直观展示。注意算法没有银弹。基于令牌的 LCS 方法对于简单的复制粘贴修改很有效但如果学生重写了算法逻辑例如将递归改为迭代即使功能相同令牌序列也可能截然不同。因此在系统设计上我将特征提取和相似度计算模块设计为可插拔的未来可以方便地接入 AST 比对等更高级的算法。3. 关键实现细节与 MyBatis Plus 的实战应用有了架构蓝图我们来看看如何用代码将其实现。这里我会聚焦几个关键模块并分享 MyBatis Plus 带来的效率提升。3.1 数据模型设计与 MP 的实体映射首先定义核心的数据库实体。以Submission作业提交实体为例import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; Data TableName(t_submission) // 指定表名 public class Submission { TableId(type IdType.AUTO) // 主键自增 private Long id; private Long assignmentId; // 关联的作业ID private Long studentId; // 提交的学生ID private String originalFilename; // 原始文件名 private String storedPath; // 服务器存储路径 private String language; // 编程语言用于选择对应的解析器 private String preprocessedContent; // 预处理后的代码内容可存储为文本或压缩后存储 private String tokenSequence; // 令牌序列可存储为JSON字符串 TableField(fill FieldFill.INSERT) // 插入时自动填充 private LocalDateTime submitTime; private Integer status; // 状态如0-已提交1-已预处理2-已分析 }使用 Lombok 的Data自动生成 getter/setter MyBatis Plus 的TableName,TableId,TableField注解清晰地定义了 ORM 映射关系。FieldFill.INSERT是一个非常好用的功能配合一个元对象处理器可以在插入数据时自动填充创建时间无需在业务代码中手动设置。对应的 Mapper 接口简单到令人惊讶import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; Mapper public interface SubmissionMapper extends BaseMapperSubmission { // 无需编写任何方法基础的CRUD已由BaseMapper提供 // 如果需要复杂查询再在此定义 }现在你可以在 Service 中直接使用submissionMapper.insert(submission)或submissionMapper.selectById(id)。这就是 MyBatis Plus 的“开箱即用”。3.2 复杂查询使用 QueryWrapper 构建动态条件查重系统的核心查询场景是“查找某次作业assignmentId中所有需要被查重的提交”。并且我们可能只想查询状态为“已预处理”的提交。用 MyBatis Plus 的QueryWrapper可以优雅地实现Service public class PlagiarismCheckServiceImpl implements PlagiarismCheckService { Autowired private SubmissionMapper submissionMapper; public ListSubmission getSubmissionsForCheck(Long assignmentId) { QueryWrapperSubmission queryWrapper new QueryWrapper(); queryWrapper.eq(assignment_id, assignmentId) .eq(status, 1) // 状态为“已预处理” .orderByAsc(id); // 按ID排序保证顺序一致 // 对应的SQL: SELECT * FROM t_submission WHERE assignment_id ? AND status 1 ORDER BY id ASC return submissionMapper.selectList(queryWrapper); } }如果查询条件更复杂比如需要联表查询学生姓名QueryWrapper也支持简单的leftJoin但对于复杂的多表关联我建议在 XML 中编写自定义 SQL或者使用 MyBatis Plus 的Select注解。MP 并没有剥夺你编写复杂 SQL 的能力它只是让简单的操作变得更简单。3.3 代码预处理与特征提取的实现这是算法的核心。我实现了一个JavaCodePreprocessor作为示例Component public class JavaCodePreprocessor implements CodePreprocessor { Override public String process(String rawCode) { if (rawCode null || rawCode.isEmpty()) { return ; } String processed rawCode; // 1. 移除所有单行和多行注释 processed removeComments(processed); // 2. 标准化空白字符将连续的空白空格、制表符、换行替换为单个空格 // 这一步会破坏代码结构仅用于某些简单比较。更佳实践是保留换行。 // processed processed.replaceAll(\\s, ); // 3. 移除所有空白行 processed Arrays.stream(processed.split(\\r?\\n)) .filter(line - !line.trim().isEmpty()) .collect(Collectors.joining(\n)); // 4. (可选) 变量/方法名归一化 - 这是一个简化示例实际需要解析AST // processed normalizeIdentifiers(processed); return processed.trim(); } private String removeComments(String code) { // 正则表达式移除注释是一个复杂问题对于教学项目简化处理 // 移除多行注释 /* ... */ String pattern1 /\\*[^*]*\\*(?:[^/*][^*]*\\*)*/; // 移除单行注释 // ... String pattern2 //.*; code code.replaceAll(pattern1, ); code code.replaceAll(pattern2, ); return code; } }对于特征提取生成令牌序列我使用了JavaParser这个库它比正则表达式更可靠Component public class JavaTokenExtractor implements FeatureExtractor { Override public ListString extractTokens(String processedCode) { ListString tokens new ArrayList(); try { ParseResultCompilationUnit parseResult new JavaParser().parse(ParseStart.COMPILATION_UNIT, Providers.provider(processedCode)); if (parseResult.isSuccessful() parseResult.getResult().isPresent()) { CompilationUnit cu parseResult.getResult().get(); cu.accept(new VoidVisitorAdapterVoid() { Override public void visit(com.github.javaparser.ast.expr.NameExpr n, Void arg) { tokens.add(IDENTIFIER); // 将所有标识符归一化 super.visit(n, arg); } Override public void visit(com.github.javaparser.ast.stmt.IfStmt n, Void arg) { tokens.add(IF); super.visit(n, arg); } // ... 遍历所有感兴趣的节点类型添加对应的令牌 }, null); } } catch (Exception e) { // 解析失败降级为基于简单分词 tokens fallbackTokenize(processedCode); } return tokens; } private ListString fallbackTokenize(String code) { // 简单的按空格和标点分词效果较差 return Arrays.asList(code.split(\\W)); } }3.4 相似度计算与批量处理优化当作业提交量很大时比如 N100两两比较的复杂度是 O(N²)需要计算大约 5000 次相似度。这是一个 CPU 密集型任务。在 Service 层我们必须考虑性能。Service public class PlagiarismCheckServiceImpl implements PlagiarismCheckService { Autowired private SimilarityCalculator similarityCalculator; Async // 使用Spring的Async实现异步执行避免阻塞HTTP请求 public void runCheckForAssignment(Long assignmentId) { ListSubmission submissions getSubmissionsForCheck(assignmentId); int size submissions.size(); // 初始化一个相似度矩阵 double[][] similarityMatrix new double[size][size]; for (int i 0; i size; i) { similarityMatrix[i][i] 1.0; // 自己和自己相似度为1 for (int j i 1; j size; j) { ListString tokensI submissions.get(i).getTokenList(); // 假设已反序列化 ListString tokensJ submissions.get(j).getTokenList(); double sim similarityCalculator.calculate(tokensI, tokensJ); similarityMatrix[i][j] sim; similarityMatrix[j][i] sim; // 对称矩阵 } } // 生成报告并保存到数据库 PlagiarismReport report generateReport(assignmentId, submissions, similarityMatrix); reportMapper.insert(report); } }这里我使用了Async注解将耗时的查重任务异步化。你需要在一个配置类上添加EnableAsync并配置一个任务执行器。这样当教师触发查重时系统会立即返回“任务已开始”然后在后台线程中执行计算计算完成后更新状态或发送通知。踩坑实录在早期版本中我同步执行查重导致上传大作业50人以上时HTTP请求超时用户体验极差。改为异步后前端可以通过轮询或 WebSocket 来获取任务进度和结果。另外存储tokenSequence时我最初直接存ListString的toString()后来发现反序列化麻烦且不标准。改为使用 Jackson 库序列化为 JSON 字符串存储读取时再反序列化更加规范。4. 前端展示与报告生成让结果一目了然算法计算出的相似度矩阵是冰冷的数据如何将其转化为教师能快速理解的报告是系统可用性的关键。我的设计是提供一个作业维度的查重概览页和一个代码对比详情页。4.1 概览页相似度矩阵与可疑对列表在 Thymeleaf 模板中我接收后端传来的ListSubmission和similarityMatrix渲染成一个可交互的表格。!-- 简化示例 -- table classtable table-bordered thead tr th学生/学生/th th th:eachs: ${submissions} th:text${s.studentName}张三/th /tr /thead tbody tr th:eachrow, rowStat: ${submissions} td th:text${row.studentName}李四/td td th:eachcol, colStat: ${submissions} span th:if${rowStat.index colStat.index}-/span a th:if${rowStat.index ! colStat.index} th:href{/report/detail(assignmentId${assignmentId}, subId1${row.id}, subId2${col.id})} th:classappend${similarityMatrix[rowStat.index][colStat.index] 0.8} ? text-danger : th:text${#numbers.formatDecimal(similarityMatrix[rowStat.index][colStat.index], 1, 2)} 0.75 /a /td /tr /tbody /table这个表格显示了所有学生两两之间的相似度并且将相似度高于0.8可配置的单元格用红色高亮。教师一眼就能看出哪些配对可疑。点击任意一个数字可以跳转到详细的代码对比页面。4.2 详情页并排代码对比与差异高亮详情页需要并排展示两份代码并高亮显示出相似的部分。这里我使用了现成的前端库DiffMatchPatch或jsdiff。但需要注意的是我们不能直接对比原始代码因为变量名、注释等干扰而是对比预处理后的令牌序列或归一化后的代码。后端接口提供两份预处理后的代码文本GetMapping(/api/code/compare) public MapString, String getCodeForCompare(RequestParam Long subId1, RequestParam Long subId2) { Submission s1 submissionService.getById(subId1); Submission s2 submissionService.getById(subId2); MapString, String result new HashMap(); result.put(code1, s1.getPreprocessedContent()); result.put(code2, s2.getPreprocessedContent()); // 也可以同时返回相似度 result.put(similarity, calculateSimilarity(s1, s2).toString()); return result; }前端通过 Ajax 获取数据后调用jsdiff库生成差异视图function renderDiff(code1, code2) { const diff JsDiff.diffLines(code1, code2); const display document.getElementById(diff-display); diff.forEach((part) { const color part.added ? green : part.removed ? red : grey; const span document.createElement(span); span.style.backgroundColor color; span.appendChild(document.createTextNode(part.value)); display.appendChild(span); }); }红色部分表示只在第一份代码中出现的行绿色表示只在第二份代码中出现的行灰色表示共有的行。这样雷同的代码块就会显示为连续的灰色区域一目了然。经验之谈直接对比行有时过于粗糙因为学生可能在同一行内做了修改。更精细的做法是使用diffWords或diffChars。但考虑到代码经过预处理变量名归一化行级对比在大多数情况下已经足够有效且性能更好。另一个关键是一定要在报告页面明确注明“本系统基于预处理后代码进行比对已忽略注释、空格及标准化变量名”避免教师或学生产生误解。5. 系统部署、配置与性能调优实战开发完成只是第一步让系统稳定、高效地运行起来同样充满挑战。5.1 多环境配置与文件存储策略Spring Boot 的application-{profile}.properties文件支持多环境配置。我通常准备application-dev.properties: 开发环境连接本地数据库开启详细的日志和 H2 控制台。application-test.properties: 测试环境连接测试服务器数据库。application-prod.properties: 生产环境配置连接池、关闭调试信息设置正确的文件存储路径。对于学生上传的源代码文件我建议不要直接存入数据库BLOB字段而是采用“数据库记录元信息 文件系统存储内容”的方式。在配置文件中指定一个存储根目录# application-prod.properties file.upload-dir/var/data/code-plagiarism/uploads在代码中使用Value注入并为每个上传生成一个唯一的文件名如 UUID避免冲突和目录遍历攻击。Service public class FileStorageService { Value(${file.upload-dir}) private String uploadDir; public String storeFile(MultipartFile file, Long assignmentId, Long studentId) throws IOException { Path userDir Paths.get(uploadDir, assignmentId.toString(), studentId.toString()); Files.createDirectories(userDir); // 创建目录 String fileName UUID.randomUUID() _ file.getOriginalFilename(); Path targetLocation userDir.resolve(fileName); Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING); return targetLocation.toString(); // 返回存储路径 } }5.2 数据库连接池与 MyBatis Plus 配置优化在生产环境数据库连接是宝贵资源。Spring Boot 默认使用 HikariCP我们需要在application-prod.properties中调整其参数spring.datasource.hikari.maximum-pool-size20 spring.datasource.hikari.minimum-idle10 spring.datasource.hikari.connection-timeout30000 spring.datasource.hikari.idle-timeout600000 spring.datasource.hikari.max-lifetime1800000对于 MyBatis Plus一些实用的配置可以放在MybatisPlusConfig配置类中Configuration MapperScan(com.yourpackage.mapper) // 扫描Mapper接口 public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 添加分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 添加乐观锁插件 (如果需要) // interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } Bean public ConfigurationCustomizer configurationCustomizer() { return configuration - configuration.setUseGeneratedKeys(true); // 使用生成的主键 } }5.3 查重任务的性能瓶颈与优化思路当作业量极大时例如上千份O(N²) 的复杂度是无法接受的。我们必须引入优化策略分治与抽样对于超大型作业可以先对所有代码进行轻量级的“指纹”计算如计算 N-gram 哈希快速筛选出可能相似的候选对只对这些候选对进行全量的、昂贵的 LCS 或 AST 比对。这类似于搜索引擎的去重原理。分布式计算将相似度矩阵的计算任务拆分成多个子任务分发到多台机器或同一个机器的多个线程上并行执行。可以使用 Spring 的Async配合自定义的ThreadPoolTaskExecutor也可以引入更专业的分布式计算框架如 Spark来处理海量数据。增量查重如果作业是陆续提交的没必要每次重新计算所有提交。可以只将新提交的代码与历史代码进行比对更新相似度矩阵。这需要设计更复杂的数据结构和更新逻辑。算法层面优化LCS 算法的标准动态规划实现是 O(m*n)。对于长代码序列可以使用 Hirschberg 等优化算法来降低空间复杂度或者使用基于贪心的近似算法来提速。在我的实现中对于百人级别的班级单机异步执行通常在几分钟内可以完成是可以接受的。但如果要面向全校公选课就必须考虑上述的优化方案了。5.4 日志、监控与异常处理一个健壮的系统离不开完善的日志和监控。我使用 SLF4J Logback并为查重服务单独配置日志级别!-- logback-spring.xml -- logger namecom.yourpackage.service.PlagiarismCheckService levelDEBUG/在关键步骤开始查重、完成预处理、完成计算、生成报告记录 INFO 日志在异常处记录 ERROR 日志并带上上下文信息。同时利用 Spring Boot Actuator 暴露/actuator/health和/actuator/metrics端点方便监控应用状态。对于文件上传、代码解析等容易出错的地方一定要做好异常处理给用户友好的提示而不是抛出堆栈信息。public void uploadSubmission(MultipartFile file, Long assignmentId) { try { // ... 存储和处理文件 } catch (IOException e) { log.error(文件存储失败作业ID: {}, 文件名: {}, assignmentId, file.getOriginalFilename(), e); throw new BusinessException(文件上传失败请重试或联系管理员。); } catch (CodeParseException e) { log.warn(代码解析失败可能不是有效的Java代码作业ID: {}, assignmentId, e); throw new BusinessException(无法解析上传的代码文件请确认文件格式正确。); } }从零开始构建一个代码作业查重系统是一次将软件工程理论付诸实践的绝佳旅程。它涵盖了 Web 后端开发、数据库设计、算法实现、性能优化和前端交互等多个方面。选择 Spring Boot 和 MyBatis Plus 作为基石让你能快速搭建出可靠的后台服务从而将主要精力集中在核心的查重算法和业务逻辑上。在实际使用中我最大的体会是没有完美的查重算法只有最适合当前场景的权衡。系统的价值不仅在于算法的准确性更在于其整体的易用性、稳定性和对教学流程的贴合度。例如提供一个“白名单”功能允许教师指定某些允许合作的作业或者提供一个“手动确认”环节让教师在系统标记出的可疑结果上进行最终裁决都是提升系统实用性的好办法。这个开源项目源码提供了一个完整的起点你可以根据自己学校的编程语言Python、C、作业特点进行定制和优化。希望这份详细的拆解能帮助你理解其中的门道甚至激发出更好的改进思路。编程的世界里解决真实问题的项目永远是最有生命力的。本文还有配套的精品资源点击获取