You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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相关注解和时间递推的最佳实践,自定义监听器反而容易引发状态不一致问题。

当前实现的核心错误

  1. 冗余的自定义监听器:你已经通过@PreviousElementShadowVariable和@NextElementShadowVariable维护了visit的前后关系,自定义监听器重复计算arrivalTime会导致状态不同步。
  2. 时间计算逻辑错误:监听器最后一行计算下一个节点的到达时间时,错误使用了当前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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 08:55:56