[063][调度模块]事件驱动的日志记录与高效查询体系
本文章代码: gitee , gitcode , github
1. 事件驱动架构:解耦调度与日志
任务状态变更时,ScheduleTaskManager 发布 ChangeStatusEvent,JobLogEventConsumer 异步消费(需配置 @Async)。
@Slf4j
@Component
@RequiredArgsConstructor
public class JobLogEventConsumer implements ChangeStatusEventConsumer {
private final JobLogRepository jobLogRepository;
private final JobRepository jobRepository;
@Override
@Async // ✅ 异步执行,不阻塞主调度线程
@Transactional(propagation = Propagation.REQUIRES_NEW) // ✅ 独立事务,不受业务回滚影响
public void consumer(ChangeStatusEvent event) {
TaskRuntimeData runtimeData = event.taskRuntimeData();
jobRepository.findByTaskCode(runtimeData.taskCode())
.ifPresentOrElse(
task -> {
JobLogEntity log = new JobLogEntity();
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());
// 异常信息智能截取
Throwable throwable = 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)字段。
public class JobLogSpecification {
// 关联查询示例:通过 leftJoin 连接 JobEntity
public static Specification<JobLogEntity> taskCodeEqual(String taskCode) {
return (root, query, cb) -> {
Join<JobLogEntity, JobEntity> jobJoin = leftJoin(root, "job");
return JobSpecification.taskCodeEqual(jobJoin, taskCode, cb);
};
}
// 时间范围查询
public static Predicate createdAtBetween(Path<JobLogEntity> path,
Instant start, Instant end,
CriteriaBuilder cb) {
Predicate predicate = 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));
}
return predicate;
}
}
技术亮点:
- 类型安全:使用 JPA 元模型(Metamodel)或字符串常量,避免硬编码。
- 可组合性:每个条件都是独立的
Specification,可灵活组合复用。
3. VO 组装与关联数据补充
JobLogVO 在转换时额外填充任务标识字段,避免前端额外请求。
@Data
public class JobLogVO extends BaseVO {
// 日志自身字段
private TaskStatusEnum taskStatus;
private String lotNo;
private Instant createdAt;
// ... 其他日志字段
// 关联任务冗余字段(非数据库字段,用于展示)
private String taskCode;
private String classSimpleName;
public static JobLogVO of(JobLogEntity entity) {
JobLogVO vo = new JobLogVO();
BeanUtils.copyProperties(entity, vo);
JobEntity job = entity.getJob();
if (job != null) {
vo.setTaskCode(job.getTaskCode());
vo.setClassSimpleName(job.getClassSimpleName());
}
return vo;
}
}
设计权衡:
- 优点:一次查询返回完整信息,减少网络往返。
- 注意:若
JobEntity中关联数据较大(如metadata含大量 JSON),不建议全部复制,只取必要字段。
4. 日志分页查询接口
@RestController
@RequestMapping("/api/job-log")
@RequiredArgsConstructor
public class JobLogEndpoint {
private final JobLogService jobLogService;
@GetMapping("page")
public ResponseEntity<PagedModel<JobLogVO>> findPage(JobLogQuery query, Pageable pageable) {
Page<JobLogEntity> page = jobLogService.find(query, pageable);
return ResponseEntity.ok(new PagedModel<>(page.map(JobLogVO::of)));
}
}
技术亮点:
- Spring Data 集成:
Pageable自动解析分页参数,PagedModel封装分页元数据。 - VO 映射:
page.map(JobLogVO::of)流式转换,代码简洁。