如何计算依赖前置任务的任务起止日期(Spring Boot)
Spring Boot 依赖型任务日期自动推算接口实现方案
核心实现逻辑
严格匹配给定业务规则落地:
- 无依赖根任务(
isDependent = false)直接读取数据库存储的起止日期作为基准值 - 有前置依赖的任务,开始日期直接取前置任务的结束日期,再结合任务自身的工期属性计算自身结束日期
- 沿依赖链向上递归直到命中根任务,过程中增加缓存、循环依赖校验避免性能问题和栈溢出
- 全量任务一次性加载到内存,减少递归过程中的数据库IO
分层代码实现
1. 日期推算通用组件
单独抽离递归推算逻辑,和业务逻辑解耦,内置缓存和循环依赖校验:
import java.time.LocalDate; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import org.springframework.stereotype.Component; @Component public class TaskDateCalculator { // 任务ID -> [开始日期, 结束日期] 缓存,避免重复递归推算 private final Map<Long, LocalDate[]> dateCache = new ConcurrentHashMap<>(); // 单次请求递归链路访问标记,防止循环依赖导致栈溢出 private final ThreadLocal<Set<Long>> visitPath = ThreadLocal.withInitial(HashSet::new); public LocalDate[] calculate(Long taskId, Function<Long, Task> taskGetter) { // 命中缓存直接返回 if (dateCache.containsKey(taskId)) { return dateCache.get(taskId); } Set<Long> currentVisits = visitPath.get(); // 检测到循环依赖直接抛错 if (currentVisits.contains(taskId)) { throw new RuntimeException("任务依赖存在循环引用,异常任务ID:" + taskId); } currentVisits.add(taskId); Task current = taskGetter.apply(taskId); LocalDate start; LocalDate end; if (Boolean.FALSE.equals(current.getIsDependent())) { // 根任务直接取库内存储的日期,做空校验 if (current.getStartDate() == null || current.getEndDate() == null) { throw new RuntimeException("根任务日期配置缺失,异常任务ID:" + taskId); } start = current.getStartDate(); end = current.getEndDate(); } else { // 递归查询前置任务日期 LocalDate[] preDates = calculate(current.getPreTaskId(), taskGetter); start = preDates[1]; // 结束日期根据自身工期推算,可根据业务实际存储的工期字段调整取值逻辑 end = start.plusDays(current.getDurationDays()); } LocalDate[] result = new LocalDate[]{start, end}; dateCache.put(taskId, result); // 清理链路标记 currentVisits.remove(taskId); if (currentVisits.isEmpty()) { visitPath.remove(); } return result; } // 单请求处理完成后清空缓存,避免跨请求数据干扰 public void clearCache() { dateCache.clear(); } }
2. Service层改造
原有查询逻辑基础上,增加日期推算步骤,全量加载任务减少DB查询:
import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import javax.annotation.Resource; import org.springframework.stereotype.Service; @Service public class TaskServiceImpl implements TaskService { @Resource private TaskRepository taskRepository; @Resource private TaskDateCalculator dateCalculator; @Resource private TaskMapper taskMapper; @Override public List<TaskDTO> listTasks() { try { // 一次性查询所有任务,转成ID映射供递归查询 List<Task> allTasks = taskRepository.findAll(); Map<Long, Task> taskMap = allTasks.stream() .collect(Collectors.toMap(Task::getId, t -> t)); List<TaskDTO> result = new ArrayList<>(); for (Task task : allTasks) { LocalDate[] dates = dateCalculator.calculate(task.getId(), taskMap::get); TaskDTO dto = taskMapper.toDTO(task); dto.setStartDate(dates[0]); dto.setEndDate(dates[1]); result.add(dto); } return result; } finally { dateCalculator.clearCache(); } } }
3. 其余层适配说明
- Model层:确保
Task实体包含id、isDependent、preTaskId(前置任务ID)、startDate、endDate、durationDays(工期/天)字段,其中依赖型任务的startDate、endDate允许为null - Mapper层:保留原有实体转DTO的映射逻辑即可,无需额外修改,推算出的日期会在Service层填充到DTO
- Repository层:无需修改,保留原有全量查询、单ID查询的基础方法即可
- Controller层:无需修改,保持原有调用Service返回结果的逻辑即可
边界场景处理
- 循环依赖:通过单次请求的访问链路标记,遍历到重复ID时直接抛出异常,避免无限递归栈溢出
- 性能损耗:全量任务预加载+结果缓存,整体推算时间复杂度为O(n),无重复DB查询和重复计算
- 配置异常:根任务缺失日期、依赖链指向不存在的任务时直接抛出明确异常,方便排查配置问题
内容的提问来源于stack exchange,提问作者Cleiton Freitas
相关产品推荐
相关产品推荐

