从 OptaPlanner 7.x 到 8.x,我们只改了 3 个地方

从 OptaPlanner 7.x 到 8.x,我们只改了 3 个地方 一次“受控恐慌”后的冷静迁移一、起因一次并不严重的“事故”事情是从测试环境的一句日志开始的。那天上午测试同学反馈APS 排产偶尔点完按钮页面一直转圈。​后台日志里没有明显的Exception但有一个线程长时间处于WAITING状态。重启服务后问题消失。再跑几次又复现但概率很低。我们第一反应是“是不是 JDK 17 OptaPlanner 7.x Drools 的老问题终于爆了”老实说当时心里是有点慌的。毕竟 APS 是生产核心模块谁也不想背一个“排产算不出来”的锅。二、决策不是“大升级”而是“小修补”在翻了一圈 OptaPlanner 的 issue 和社区讨论后我们发现这个问题在 8.x 里被反复提及但没有明确的“已修复”标记7.x 官方早已停止维护但 8.x 的 API 变动看起来“可控”于是我们定了一个原则不动业务逻辑只动“不得不改”的地方。换句话说这不是一次“架构升级”而是一次“打补丁式的迁移”。三、真正改动的只有这 3 个地方事后统计我们一共改了3 个文件加起来不到 50 行代码。1️⃣ApsConstraintProviderpenalize() 变得“更数学了”这是我们最早碰到的编译错误。7.x 的老代码private Constraint capabilityConflict(ConstraintFactory constraintFactory) { return constraintFactory.from(Task.class) .filter(t - { if (t.getResourceBo() null) { return false; } if (CollUtil.isEmpty(t.getResourceBo().getCapabilityIds())) { return true; } else { return !t.getResourceBo().getCapabilityIds() .containsAll(t.getCapabilityIds()); } }) .penalize(Capability conflict, HardSoftScore.ofHard(10)); }8.x 的新写法private Constraint capabilityConflict(ConstraintFactory constraintFactory) { return constraintFactory.from(Task.class) .filter(t - { if (t.getResourceBo() null) { return false; } if (CollUtil.isEmpty(t.getResourceBo().getCapabilityIds())) { return true; } else { return !t.getResourceBo().getCapabilityIds() .containsAll(t.getCapabilityIds()); } }) .penalize(HardSoftScore.ONE_HARD) .multiply(10); }我们在 Code Review 时是怎么解释的我当时跟同事说“以前是‘一次性扣 10 分’现在是‘先扣 1 分再放大 10 倍’。”听起来像文字游戏但其实语义更清晰了ONE_HARD是一个单位惩罚.multiply(10)是权重系数以后如果要把权重放到配置中心这个写法会非常自然.multiply(penaltyConfig.getCapabilityConflictWeight())这不是语法糖这是可维护性的提升。2️⃣StartTimeUpdatingVariableListener从“地下”走到“地上”这个改动是最让我舒服的一个。7.x 的 importimport org.optaplanner.core.impl.domain.variable.listener.VariableListener; import org.optaplanner.core.impl.score.director.ScoreDirector;8.x 的 importimport org.optaplanner.core.api.domain.variable.VariableListener; import org.optaplanner.core.api.score.director.ScoreDirector;为什么这是个好信号因为impl这个包名在 Java 世界里通常意味着一句话“别碰我我随时可能变。”我们之前写VariableListener心里多少有点“偷用内部 API”的不安。现在它被挪到api包说明了一件事OptaPlanner 承认你写的这种监听器是合法扩展。这对我们这种“自己维护影子变量”的 APS 系统来说是个定心丸。3️⃣ScoreDirector字符串 → Lambda最实用这可能是日常开发中感知最强的一个变化。7.x 的写法scoreDirector.beforeVariableChanged(task, planStartTime); task.setPlanStartTime(newTime); scoreDirector.afterVariableChanged(task, planStartTime);8.x 的写法scoreDirector.beforeVariableChanged(task, Task::getPlanStartTime); task.setPlanStartTime(newTime); scoreDirector.afterVariableChanged(task, Task::getPlanStartTime);我们为什么喜欢这个改动举个真实的例子有一次我们重构Task类把字段从planStartTime改成了startTime。在 7.x 时代这类改动非常危险Java 编译器不知道你在字符串里写的是什么只有运行时才会抛异常单元测试稍不注意就漏掉但在 8.xIDE 直接标红重构字段名lambda 自动跟着改编译期就把问题拦住了我当时在群里发了一句“就冲这个 lambda这趟升级就值了。”四、迁移过程中的两个“小插曲”插曲一一开始我们以为要改很多约束打开ApsConstraintProvider看到十几个约束方法心里咯噔一下。结果发现过滤逻辑一个都没变只有penalize()的写法变了而且 IDE 的批量替换 正则就能搞定大半插曲二担心 Shadow Variable 会不会崩我们有多个影子变量CustomShadowVariable担心 8.x 会有行为变化。实际验证下来监听器触发时机一致计算逻辑无误求解结果在多个数据集上完全一致五、我们怎么验证“没改坏”迁移完成后我们没有立刻上线而是做了三轮验证回归测试用生产脱敏数据跑 100 次求解对比 7.x 与 8.x 的求解结果和分数构成性能对比平均求解时间波动在 ±5% 以内GC 行为无明显变化异常注入模拟并发提交、服务重启验证 SolverManager 行为是否一致最终结论是这次升级没有改变 APS 的任何业务语义。六、回头看这次迁移的价值是什么如果只是为了“修那个偶发线程卡死”我们其实可以只加 JVM 参数。但我们还是做了 8.x 的迁移原因有三把“隐患”往前挪了一步7.x 是死胡同8.x 至少还有社区参考为 Timefold 铺路8.x 的 API 更接近 Timefold将来迁移成本更低团队信心大家亲手改过知道“哪里会变、哪里不变”七、一句话总结从 7.x 到 8.x不是一次“冒险”而是一次“低成本、高确定性的技术避险”。如果你也在犹豫要不要升我的建议是JDK 17 下能跑就先别动真要动就只动“必须动”的地方动完之后一定要验证而不是假设