[063][调度模块]事件驱动的日志记录与高效查询体系
本文章代码: gitee , gitcode , github
1. 事件驱动架构:解耦调度与日志
任务状态变更时,ScheduleTaskManager发布ChangeStatusEvent,JobLogEventConsumer异步消费(需配置@Async)。
@Slf4j@Component@RequiredArgsConstructorpublicclassJobLogEventConsumerimplementsChangeStatusEventConsumer{privatefinalJobLogRepositoryjobLogRepository;privatefinalJobRepositoryjobRepository;@Override@Async// ✅ 异步执行,不阻塞主调度线程@Transactional(propagation=Propagation.REQUIRES_NEW)// ✅ 独立事务,不受业务回滚影响publicvoidconsumer(ChangeStatusEventevent){TaskRuntimeDataruntimeData=event.taskRuntimeData();jobRepository.findByTaskCode(runtimeData.taskCode()).ifPresentOrElse(task->{JobLogEntitylog=newJobLogEntity();log.setJob(task);log.setTaskStatus(runtimeData.taskStatus());log.setLotNo(runtimeData.lotNo());log.setTotalCount(runtimeData.totalCount());log.setTotalFailureCount(runtimeData.totalFailureCount());log.setStartTime(runtimeData.startTime());log.setEndTime(runtimeData.endTime());// 异常信息智能截取Throwablethrowable=runtimeData.throwable();if(throwable!=null){log.setHasError(YesNoEnum.Y);log.setMessage(StringUtils.substring(ExceptionUtils.getSelfStackTrace(throwable),0,500));}else{log.setHasError(YesNoEnum.N);log.setMessage(runtimeData.message());}jobLogRepository.save(log);},()->log.warn("任务不存在,无法记录日志,taskCode={}",runtimeData.taskCode()));}}技术亮点:
- 异步+独立事务:日志写入不影响调度性能,且即使主业务事务回滚,日志依然留存。
- 异常堆栈裁剪:500 字符限制防止数据截断异常,同时保留关键堆栈信息。
- 空值友好:使用
ifPresentOrElse优雅处理任务不存在的情况。
2. 日志查询的规格模式组合
JobLogQuery支持多条件动态查询,包括关联表(JobEntity)字段。
publicclassJobLogSpecification{// 关联查询示例:通过 leftJoin 连接 JobEntitypublicstaticSpecification<JobLogEntity>taskCodeEqual(StringtaskCode){return(root,query,cb)->{Join<JobLogEntity,JobEntity>jobJoin=leftJoin(root,"job");returnJobSpecification.taskCodeEqual(jobJoin,taskCode,cb);};}// 时间范围查询publicstaticPredicatecreatedAtBetween(Path<JobLogEntity>path,Instantstart,Instantend,CriteriaBuildercb){Predicatepredicate=cb.conjunction();if(start!=null){predicate=cb.and(predicate,cb.greaterThanOrEqualTo(path.get("createdAt"),start));}if(end!=null){predicate=cb.and(predicate,cb.lessThanOrEqualTo(path.get("createdAt"),end));}returnpredicate;}}技术亮点:
- 类型安全:使用 JPA 元模型(Metamodel)或字符串常量,避免硬编码。
- 可组合性:每个条件都是独立的
Specification,可灵活组合复用。
3. VO 组装与关联数据补充
JobLogVO在转换时额外填充任务标识字段,避免前端额外请求。
@DatapublicclassJobLogVOextendsBaseVO{// 日志自身字段privateTaskStatusEnumtaskStatus;privateStringlotNo;privateInstantcreatedAt;// ... 其他日志字段// 关联任务冗余字段(非数据库字段,用于展示)privateStringtaskCode;privateStringclassSimpleName;publicstaticJobLogVOof(JobLogEntityentity){JobLogVOvo=newJobLogVO();BeanUtils.copyProperties(entity,vo);JobEntityjob=entity.getJob();if(job!=null){vo.setTaskCode(job.getTaskCode());vo.setClassSimpleName(job.getClassSimpleName());}returnvo;}}设计权衡:
- 优点:一次查询返回完整信息,减少网络往返。
- 注意:若
JobEntity中关联数据较大(如metadata含大量 JSON),不建议全部复制,只取必要字段。
4. 日志分页查询接口
@RestController@RequestMapping("/api/job-log")@RequiredArgsConstructorpublicclassJobLogEndpoint{privatefinalJobLogServicejobLogService;@GetMapping("page")publicResponseEntity<PagedModel<JobLogVO>>findPage(JobLogQueryquery,Pageablepageable){Page<JobLogEntity>page=jobLogService.find(query,pageable);returnResponseEntity.ok(newPagedModel<>(page.map(JobLogVO::of)));}}技术亮点:
- Spring Data 集成:
Pageable自动解析分页参数,PagedModel封装分页元数据。 - VO 映射:
page.map(JobLogVO::of)流式转换,代码简洁。