使用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]
已尝试的操作
- 在评分函数中过滤未赋值的影子变量
- 新增惩罚未赋值影子变量的约束
但问题仍未解决,需进一步排查。
排查与解决建议
- 补全变量监听器的生命周期方法
当前监听器仅处理了变量变更后和实体添加后的场景,缺少变量变更前、实体移除前的处理,可能导致影子变量状态不一致。补充以下方法:
@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); }
确保影子变量初始化安全
在CourseClazz的构造方法中,将slot字段初始化为null,避免未初始化的空指针风险。严格检查评分约束中的空值
异常显示约束评估时出错,需确保所有涉及影子变量的约束都先判空:
// 错误示例:直接调用影子变量的方法 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())
- 校验关联列表的非空性
在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"); } }
- 验证影子变量的配置正确性
检查@ShadowVariable注解的sourceVariableName、sourceEntityClass是否拼写正确,确保监听器类被OptaPlanner正确识别。
内容的提问来源于stack exchange,提问作者vidbin
相关产品推荐
相关产品推荐

