这个SSM框架的项目管理系统是我在带学生团队做课程设计时的实战产物。当时我们需要一个能同时满足教师端作业布置、学生端代码提交、自动查重和评分统计的轻量级平台。市面上现成的系统要么功能过剩要么扩展性太差,最终决定基于SSM(Spring+SpringMVC+MyBatis)这套经典组合来自主开发。
选择SSM不是偶然——Spring的IOC容器让模块解耦变得简单,AOP完美处理了日志和事务这些横切关注点;SpringMVC的注解驱动模式比Struts2的XML配置更符合当下开发习惯;MyBatis的SQL灵活性正好应对教育场景中多变的数据统计需求。实测下来,这套技术栈在开发效率、性能表现和后期维护上达到了很好的平衡。
在技术选型阶段我们对比过几种方案:
@Controller注解比Servlet的doGet/doPost更直观Mapper.xml比Hibernate的HQL更贴近学生数据库课程基础系统采用标准三层架构:
@RequestMapping定义RESTful接口java复制@Controller
@RequestMapping("/assignment")
public class AssignmentController {
@Autowired
private AssignmentService assignmentService;
@GetMapping("/{id}")
public String getDetail(@PathVariable Integer id, Model model) {
model.addAttribute("assignment",
assignmentService.getById(id));
return "assignment_detail";
}
}
java复制@Service
@Transactional
public class SubmitServiceImpl implements SubmitService {
@Override
public boolean submitCode(Submission submission) {
// 1. 保存提交记录
submissionMapper.insert(submission);
// 2. 触发查重检测
plagiarismCheckService.check(submission);
// 3. 更新作业状态
return assignmentMapper.updateStatus(submission.getAssId()) > 0;
}
}
xml复制<select id="selectByCondition" resultMap="AssignmentResult">
SELECT * FROM assignments
<where>
<if test="courseId != null">
AND course_id = #{courseId}
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="keyword != null">
AND title LIKE CONCAT('%',#{keyword},'%')
</if>
</where>
ORDER BY deadline DESC
</select>
这是系统最具挑战的部分,我们采用"指纹比对+结构分析"双保险方案:
java复制public class FingerprintGenerator {
public static Set<String> generate(String code) {
// 1. 标准化处理(去除注释/空格/换行)
String normalized = code.replaceAll("//.*|/\\*.*?\\*/|\\s", "");
// 2. 滑动窗口取哈希
Set<String> fingerprints = new HashSet<>();
int k = 20; // 窗口大小
for (int i = 0; i <= normalized.length() - k; i++) {
String window = normalized.substring(i, i + k);
fingerprints.add(DigestUtils.md5Hex(window));
}
return fingerprints;
}
}
sql复制<!-- 在Mapper中定义查重查询 -->
<select id="checkSimilarity" parameterType="map" resultType="double">
SELECT COUNT(DISTINCT fingerprint) * 1.0 /
(SELECT COUNT(DISTINCT fingerprint)
FROM code_fingerprints
WHERE submission_id IN (#{id1},#{id2}))
FROM code_fingerprints
WHERE submission_id IN (#{id1},#{id2})
GROUP BY fingerprint
HAVING COUNT(*) > 1
</select>
通过规则引擎实现可配置的评分策略:
java复制// 评分规则配置示例
@Bean
public RuleEngine gradingRuleEngine() {
RulesEngineParameters params = new RulesEngineParameters()
.skipOnFirstAppliedRule(true);
RulesEngine engine = new DefaultRulesEngine(params);
engine.registerRule(new RuleBuilder()
.name("SyntaxCheckRule")
.when(facts -> facts.get("errorCount") == 0)
.then(facts -> facts.add("score", 20))
.build());
engine.registerRule(new RuleBuilder()
.name("TestCasePassRule")
.when(facts -> facts.get("passRate") >= 0.8)
.then(facts -> facts.add("score", 50))
.build());
return engine;
}
采用Redis二级缓存解决高并发查询:
java复制@Cacheable(value = "assignments", key = "#id")
public Assignment getById(Integer id) {
return assignmentMapper.selectById(id);
}
@CacheEvict(value = "assignments", key = "#assignment.id")
public void update(Assignment assignment) {
assignmentMapper.updateById(assignment);
}
作业批改场景下的批量插入改进:
java复制// 原始单条插入(2000条数据需要12秒)
public void insertSubmissions(List<Submission> list) {
for (Submission s : list) {
submissionMapper.insert(s);
}
}
// 优化后的批量插入(2000条数据仅需0.8秒)
public void batchInsert(List<Submission> list) {
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
SubmissionMapper mapper = session.getMapper(SubmissionMapper.class);
for (int i = 0; i < list.size(); i++) {
mapper.insert(list.get(i));
if (i % 500 == 0 || i == list.size() - 1) {
session.commit();
session.clearCache();
}
}
} finally {
session.close();
}
}
在初期版本中遇到过事务不生效的问题,主要发生在:
当参数类型为Map时,XML中必须使用_parameter引用:
xml复制<!-- 错误写法 -->
<if test="mapParam.key != null">
AND column = #{mapParam.key}
</if>
<!-- 正确写法 -->
<if test="_parameter.containsKey('key')">
AND column = #{key}
</if>
统一时区处理能避免很多问题:
java复制@Configuration
public class DateConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}
通过Profile实现环境隔离:
properties复制# application-dev.properties
spring.datasource.url=jdbc:mysql://localhost:3306/pms_dev
logging.level.root=DEBUG
# application-prod.properties
spring.datasource.url=jdbc:mysql://prod-db:3306/pms_prod
logging.level.root=WARN
SpringBoot Actuator的定制化配置:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
probes:
enabled: true
metrics:
enabled: true
这个系统在投入实际教学使用后,平均每周处理300+次代码提交,查重准确率达到92%,比原先手动批改效率提升5倍以上。最大的收获是验证了SSM框架在中小型教育系统中的适用性——既保持了技术栈的简洁性,又通过合理的架构设计满足了教学管理的核心需求。