使用OptaPlanner时Chain Variable Listener出现NPE问题咨询
一、NPE根源分析
你遇到的空指针异常,核心原因是previousVisit.getDepartureTime()返回null,而previousVisit本身不为null。查看Visits类的getDepartureTime()实现:
@JsonProperty(access = JsonProperty.Access.READ_ONLY) public LocalDateTime getDepartureTime() { if (arrivalTime == null) { return null; } return getStartServiceTime().plusMinutes(55); }
当previousVisit的arrivalTime未被正确初始化时,departureTime就会返回null,后续调用departureTime.plusMinutes()自然抛出NPE。在你的VisitChainVariableListener逻辑中,处理链中某个visit时,其previousVisit的arrivalTime可能还没被变量监听器更新,导致依赖的departureTime为空。
二、是否需要使用Variable Change Listener?
你的场景属于静态路径规划(非实时动态调整日程),完全不需要自定义VariableListener。OptaPlanner内置了@ShadowVariable相关注解和时间递推的最佳实践,自定义监听器反而容易引发状态不一致问题。
当前实现的核心错误
- 冗余的自定义监听器:你已经通过
@PreviousElementShadowVariable和@NextElementShadowVariable维护了visit的前后关系,自定义监听器重复计算arrivalTime会导致状态不同步。 - 时间计算逻辑错误:监听器最后一行计算下一个节点的到达时间时,错误使用了当前
visit的患者位置,而非下一个节点的位置:
arrivalTime = departureTime.plusMinutes(getDrivingTime(departureCoordinates, visit.getMember().getMemberLocation().getCoordinates()));
此处应替换为nextVisit.getMember().getMemberLocation().getCoordinates(),否则后续所有节点的到达时间都会指向当前visit的位置。
3. 出发时间初始化不合理:Service中固定将医护人员的departureTime设置为当天12点,不符合业务逻辑(示例中医护人员7:40出发),会导致初始时间计算完全错误。
三、改进方案
1. 移除自定义监听器,用OptaPlanner内置注解实现时间递推
利用OptaPlanner的@AnchorShadowVariable和自定义内置风格的变量监听器,替代原有逻辑,确保状态一致性。
调整Visits类的Shadow变量定义
@PlanningEntity @Data public class Visits { // 保留原有字段,仅修改Shadow变量相关部分 @AnchorShadowVariable(sourceVariableName = "visits") public Providers getAssignedClinician() { return assignedClinician; } @PreviousElementShadowVariable(sourceVariableName = "visits") public Visits getPreviousVisit() { return previousVisit; } // 用内置风格的监听器计算arrivalTime @ShadowVariable( sourceVariableName = "previousVisit", variableListenerClass = ArrivalTimeUpdatingVariableListener.class ) public LocalDateTime getArrivalTime() { return arrivalTime; } // 修正departureTime的依赖逻辑,直接关联时间窗 public LocalDateTime getDepartureTime() { if (arrivalTime == null || timeWindow == null) { return null; } LocalDateTime startService = arrivalTime.isBefore(timeWindow.getStart()) ? timeWindow.getStart() : arrivalTime; return startService.plusMinutes(55); } // 修正startServiceTime的依赖,移除未定义的minStartTime public LocalDateTime getStartServiceTime() { if (arrivalTime == null || timeWindow == null) { return null; } return arrivalTime.isBefore(timeWindow.getStart()) ? timeWindow.getStart() : arrivalTime; } }
实现内置风格的ArrivalTimeUpdatingVariableListener
public class ArrivalTimeUpdatingVariableListener implements VariableListener<ProviderRouteSolution, Visits> { @Override public void afterVariableChanged(ScoreDirector<ProviderRouteSolution> scoreDirector, Visits visit) { updateArrivalTimeChain(scoreDirector, visit); } private void updateArrivalTimeChain(ScoreDirector<ProviderRouteSolution> scoreDirector, Visits startingVisit) { Visits currentVisit = startingVisit; Visits previousVisit = currentVisit.getPreviousVisit(); LocalDateTime departureTime; // 计算当前节点的到达时间 if (previousVisit == null) { departureTime = currentVisit.getAssignedClinician().getDepartureTime(); Long drivingTime = getDrivingTime( currentVisit.getAssignedClinician().getLocation().getCoordinates(), currentVisit.getMember().getMemberLocation().getCoordinates() ); LocalDateTime arrivalTime = departureTime.plusMinutes(drivingTime); updateVisitArrivalTime(scoreDirector, currentVisit, arrivalTime); } else { // 递归确保前序节点的时间已计算完成 if (previousVisit.getDepartureTime() == null) { updateArrivalTimeChain(scoreDirector, previousVisit); } departureTime = previousVisit.getDepartureTime(); Long drivingTime = getDrivingTime( previousVisit.getMember().getMemberLocation().getCoordinates(), currentVisit.getMember().getMemberLocation().getCoordinates() ); LocalDateTime arrivalTime = departureTime.plusMinutes(drivingTime); updateVisitArrivalTime(scoreDirector, currentVisit, arrivalTime); } // 更新后续节点 Visits nextVisit = currentVisit.getNextVisit(); if (nextVisit != null) { LocalDateTime nextArrival = calculateNextArrivalTime(currentVisit, nextVisit); if (!Objects.equals(nextVisit.getArrivalTime(), nextArrival)) { updateArrivalTimeChain(scoreDirector, nextVisit); } } } private LocalDateTime calculateNextArrivalTime(Visits currentVisit, Visits nextVisit) { if (currentVisit.getDepartureTime() == null) { return null; } Long drivingTime = getDrivingTime( currentVisit.getMember().getMemberLocation().getCoordinates(), nextVisit.getMember().getMemberLocation().getCoordinates() ); return currentVisit.getDepartureTime().plusMinutes(drivingTime); } private void updateVisitArrivalTime(ScoreDirector<ProviderRouteSolution> scoreDirector, Visits visit, LocalDateTime newArrivalTime) { if (!Objects.equals(visit.getArrivalTime(), newArrivalTime)) { scoreDirector.beforeVariableChanged(visit, "arrivalTime"); visit.setArrivalTime(newArrivalTime); scoreDirector.afterVariableChanged(visit, "arrivalTime"); } } }
2. 修正医护人员出发时间初始化
在Service中根据医护人员的可用时段设置出发时间,替代固定12点的逻辑:
// 替换原有departureTime设置代码 if (!clinicianSlots.isEmpty()) { ClinicianSlot firstSlot = clinicianSlots.get(0); // 按业务需求提前出发(比如示例中8点时段提前20分钟出发) provider.setDepartureTime(firstSlot.getStartTime().minusMinutes(20)); } else { // 无可用时段时设置默认值或抛出异常 provider.setDepartureTime(LocalDateTime.now(ZoneOffset.UTC)); }
3. 添加核心约束确保合规性
通过约束规则保证任务符合时间窗和医护人员可用时段:
@ConstraintProvider public class VisitScheduleConstraintProvider { // 任务服务时间必须在患者时间窗内 @Constraint public Constraint serviceWithinPatientTimeWindow(ConstraintFactory factory) { return factory.from(Visits.class) .filter(visit -> { LocalDateTime startService = visit.getStartServiceTime(); LocalDateTime departure = visit.getDepartureTime(); TimeWindow window = visit.getTimeWindow(); return startService == null || departure == null || window == null || startService.isBefore(window.getStart()) || departure.isAfter(window.getEnd()); }) .penalize("Service outside patient time window", HardSoftScore.ONE_HARD); } // 任务必须在医护人员可用时段内完成 @Constraint public Constraint serviceWithinClinicianAvailability(ConstraintFactory factory) { return factory.from(Visits.class) .join(Providers.class, Joiners.equal(Visits::getAssignedClinician)) .filter((visit, provider) -> { LocalDateTime startService = visit.getStartServiceTime(); LocalDateTime departure = visit.getDepartureTime(); return startService == null || departure == null || provider.getAvailableSlots().stream() .noneMatch(slot -> startService.isAfter(slot.getStartTime()) && departure.isBefore(slot.getEndTime())); }) .penalize("Service outside clinician availability", HardSoftScore.ONE_HARD); } }
四、其他优化建议
- 预计算驾驶时间矩阵:在
initializeDrivingTimeMatrix中提前计算所有地点间的驾驶时间,避免在监听器或约束中实时计算,提升性能。 - 统一时间类型:替换代码中的
Date和Calendar,全部使用LocalDateTime处理,避免时区问题。 - 添加调试日志:在变量监听器和约束中添加日志,便于追踪时间计算过程中的异常。
内容的提问来源于stack exchange,提问作者Edmanan Khandelwal

