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

使用OptaPlanner时未初始化影子变量未过滤引发空指针异常

关于OptaPlanner影子变量同步导致分数计算NullPointerException的问题

使用OptaPlanner的影子变量时,分数计算过程中抛出NullPointerException。根据文档,未赋值的规划变量会被自动过滤,但问题依然存在。需求是:当ComposeCourseClazz的规划变量变更时,同步更新CourseClazz的规划变量。

代码实现

规划实体

@PlanningEntity
@EqualsAndHashCode(of = "id")
public class ComposeCourseClazz {
   
   @PlanningId
   private String id;

   @PlanningVariable(valueRangeProviderRefs = {"slots"})
   private Timeslot slot;

   @ValueRangeProvider(id = "slots")
   private List<Timeslot> slotList;
   
   private List<CourseClazz> ccList;
   // 省略其他代码
}

@EqualsAndHashCode(of = "id")
@PlanningEntity
public class CourseClazz {

    @PlanningId
    private String id;

    @ShadowVariable(variableListenerClass = SlotUpdatingVariableListener.class,
            sourceEntityClass = ComposeCourseClazz.class, sourceVariableName = "slot")
    private Timeslot slot;
    // 省略其他代码
}

变量监听器

public class SlotUpdatingVariableListener implements VariableListener<CourseSchedule, ComposeCourseClazz> {
  @Override
    public void afterVariableChanged(ScoreDirector<CourseSchedule> scoreDirector, ComposeCourseClazz composeCourseClazz) {
        updateSlotForCC(scoreDirector, composeCourseClazz);
        
    }

    @Override
    public void afterEntityAdded(ScoreDirector<CourseSchedule> scoreDirector, ComposeCourseClazz composeCourseClazz) {
        updateSlotForCC(scoreDirector, composeCourseClazz);
    }
    protected void updateSlotForCC(ScoreDirector<CourseSchedule> scoreDirector, ComposeCourseClazz composeCourseClazz) {
        Timeslot slot = composeCourseClazz.getSlot();
        for (CourseClazz cc : composeCourseClazz.getCcList()) {
            scoreDirector.beforeVariableChanged(cc, "slot");
            cc.setSlot(slot);
            scoreDirector.afterVariableChanged(cc, "slot");
        }
    }
}

异常信息

org.drools.modelcompiler.constraints.ConstraintEvaluationException: Error evaluating constraint '' in  and in more rules
    at org.drools.modelcompiler.constraints.LambdaConstraint.isAllowed(LambdaConstraint.java:184) ~[drools-model-compiler-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.core.phreak.PhreakAccumulateNode.propagateResult(PhreakAccumulateNode.java:665) ~[drools-core-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.core.phreak.PhreakGroupByNode.evaluateResultConstraints(PhreakGroupByNode.java:75) ~[drools-core-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.core.phreak.PhreakAccumulateNode.doNode(PhreakAccumulateNode.java:109) ~[drools-core-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.core.phreak.RuleNetworkEvaluator.switchOnDoBetaNode(RuleNetworkEvaluator.java:593) ~[drools-core-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.core.phreak.RuleNetworkEvaluator.evalBetaNode(RuleNetworkEvaluator.java:562) ~[drools-core-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.core.phreak.RuleNetworkEvaluator.evalNode(RuleNetworkEvaluator.java:389) ~[drools-core-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.core.phreak.RuleNetworkEvaluator.innerEval(RuleNetworkEvaluator.java:349) ~[drools-core-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.core.phreak.RuleNetworkEvaluator.evalStackEntry(RuleNetworkEvaluator.java:247) ~[drools-core-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.core.phreak.RuleNetworkEvaluator.outerEval(RuleNetworkEvaluator.java:190) ~[drools-core-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.core.phreak.RuleNetworkEvaluator.evaluateNetwork(RuleNetworkEvaluator.java:143) ~[drools-core-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.core.phreak.RuleExecutor.evaluateNetwork(RuleExecutor.java:221) ~[drools-core-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.core.phreak.RuleExecutor.evaluateNetworkIfDirty(RuleExecutor.java:231) ~[drools-core-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.core.phreak.RuleExecutor.evaluateNetworkAndFire(RuleExecutor.java:76) ~[drools-core-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.core.concurrent.AbstractGroupEvaluator.evaluateAndFire(AbstractGroupEvaluator.java:46) ~[drools-core-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.kiesession.agenda.DefaultAgenda.fireLoop(DefaultAgenda.java:624) ~[drools-kiesession-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.kiesession.agenda.DefaultAgenda.internalFireAllRules(DefaultAgenda.java:571) ~[drools-kiesession-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.kiesession.agenda.DefaultAgenda.fireAllRules(DefaultAgenda.java:563) ~[drools-kiesession-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.kiesession.session.StatefulKnowledgeSessionImpl.internalFireAllRules(StatefulKnowledgeSessionImpl.java:1088) ~[drools-kiesession-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.kiesession.session.StatefulKnowledgeSessionImpl.fireAllRules(StatefulKnowledgeSessionImpl.java:1079) ~[drools-kiesession-8.44.0.Final.jar:8.44.0.Final]
    at org.drools.kiesession.session.StatefulKnowledgeSessionImpl.fireAllRules(StatefulKnowledgeSessionImpl.java:1071) ~[drools-kiesession-8.44.0.Final.jar:8.44.0.Final]
    at org.optaplanner.constraint.streams.drools.DroolsConstraintStreamScoreDirector.calculateScore(DroolsConstraintStreamScoreDirector.java:70) ~[optaplanner-constraint-streams-drools-9.44.0.Final.jar:9.44.0.Final]
    at org.optaplanner.core.impl.solver.recaller.BestSolutionRecaller.solvingStarted(BestSolutionRecaller.java:50) ~[optaplanner-core-impl-9.44.0.Final.jar:9.44.0.Final]
    at org.optaplanner.core.impl.solver.AbstractSolver.solvingStarted(AbstractSolver.java:65) ~[optaplanner-core-impl-9.44.0.Final.jar:9.44.0.Final]
    at org.optaplanner.core.impl.solver.DefaultSolver.solvingStarted(DefaultSolver.java:225) ~[optaplanner-core-impl-9.44.0.Final.jar:9.44.0.Final]
    at org.optaplanner.core.impl.solver.DefaultSolver.solve(DefaultSolver.java:192) ~[optaplanner-core-impl-9.44.0.Final.jar:9.44.0.Final]
    at org.optaplanner.core.impl.solver.DefaultSolverJob.call(DefaultSolverJob.java:109) ~[optaplanner-core-impl-9.44.0.Final.jar:9.44.0.Final]
    at java.base/java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:317) ~[na:na]
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[na:na]
    at java.base/java.lang.Thread.run(Thread.java:1589) ~[na:na]

已尝试的操作

  • 在评分函数中过滤未赋值的影子变量
  • 新增惩罚未赋值影子变量的约束

但问题仍未解决,需进一步排查。

排查与解决建议

  1. 补全变量监听器的生命周期方法
    当前监听器仅处理了变量变更后和实体添加后的场景,缺少变量变更前、实体移除前的处理,可能导致影子变量状态不一致。补充以下方法:
@Override
public void beforeVariableChanged(ScoreDirector<CourseSchedule> scoreDirector, ComposeCourseClazz composeCourseClazz) {
    // 清空关联CourseClazz的slot,避免旧值残留
    List<CourseClazz> ccList = composeCourseClazz.getCcList();
    if (ccList == null) return;
    for (CourseClazz cc : ccList) {
        scoreDirector.beforeVariableChanged(cc, "slot");
        cc.setSlot(null);
        scoreDirector.afterVariableChanged(cc, "slot");
    }
}

@Override
public void beforeEntityRemoved(ScoreDirector<CourseSchedule> scoreDirector, ComposeCourseClazz composeCourseClazz) {
    beforeVariableChanged(scoreDirector, composeCourseClazz);
}
  1. 确保影子变量初始化安全
    在CourseClazz的构造方法中,将slot字段初始化为null,避免未初始化的空指针风险。

  2. 严格检查评分约束中的空值
    异常显示约束评估时出错,需确保所有涉及影子变量的约束都先判空:

// 错误示例:直接调用影子变量的方法
constraintFactory.from(CourseClazz.class)
    .penalize("Invalid slot", HardSoftScore.ONE_HARD, cc -> cc.getSlot().getDayOfWeek())

// 正确示例:先判断影子变量非空
constraintFactory.from(CourseClazz.class)
    .filter(cc -> cc.getSlot() != null)
    .penalize("Invalid slot", HardSoftScore.ONE_HARD, cc -> cc.getSlot().getDayOfWeek())
  1. 校验关联列表的非空性
    在updateSlotForCC方法中,先判断composeCourseClazz.getCcList()是否为null,避免遍历空列表时抛出异常:
protected void updateSlotForCC(ScoreDirector<CourseSchedule> scoreDirector, ComposeCourseClazz composeCourseClazz) {
    Timeslot slot = composeCourseClazz.getSlot();
    List<CourseClazz> ccList = composeCourseClazz.getCcList();
    if (ccList == null) return;
    for (CourseClazz cc : ccList) {
        scoreDirector.beforeVariableChanged(cc, "slot");
        cc.setSlot(slot);
        scoreDirector.afterVariableChanged(cc, "slot");
    }
}
  1. 验证影子变量的配置正确性
    检查@ShadowVariable注解的sourceVariableName、sourceEntityClass是否拼写正确,确保监听器类被OptaPlanner正确识别。

内容的提问来源于stack exchange,提问作者vidbin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 03:25:56