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

使用OptaPlanner与Kotlin时QuarkusTest出现ClassCastException

OptaPlanner GIZMO类加载导致Quarkus测试时ClassCastException

问题场景

使用OptaPlanner 9.37.0.Final、Quarkus 3.1.3.Final和Kotlin 1.8.21开发车辆路径规划(VRP)应用时,quarkusDev运行正常,但执行@QuarkusTest时抛出类加载相关的ClassCastException:

java.lang.ClassCastException: class com.cargonexx.vehiclerouting.domain.LoadJob$OptaPlanner$MemberAccessor$Field$arrivalTime cannot be cast to class org.optaplanner.core.impl.domain.common.accessor.MemberAccessor (com.cargonexx.vehiclerouting.domain.LoadJob$OptaPlanner$MemberAccessor$Field$arrivalTime is in unnamed module of loader 'OptaPlanner Gizmo SolutionCloner ClassLoader' @611d16c9; org.optaplanner.core.impl.domain.common.accessor.MemberAccessor is in unnamed module of loader io.quarkus.bootstrap.classloading.QuarkusClassLoader @319988b0)
    at org.optaplanner.core.impl.domain.common.accessor.gizmo.GizmoMemberAccessorImplementor.createInstance(GizmoMemberAccessorImplementor.java:118)
    at org.optaplanner.core.impl.domain.common.accessor.gizmo.GizmoMemberAccessorImplementor.createAccessorFor(GizmoMemberAccessorImplementor.java:112)
    at org.optaplanner.core.impl.domain.common.accessor.gizmo.GizmoMemberAccessorFactory.buildGizmoMemberAccessor(GizmoMemberAccessorFactory.java:40)
    at org.optaplanner.core.impl.domain.common.accessor.MemberAccessorFactory.buildMemberAccessor(MemberAccessorFactory.java:38)
    at org.optaplanner.core.impl.domain.common.accessor.MemberAccessorFactory.lambda$buildAndCacheMemberAccessor$0(MemberAccessorFactory.java:125)
    at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708)
    at org.optaplanner.core.impl.domain.common.accessor.MemberAccessorFactory.buildAndCacheMemberAccessor(MemberAccessorFactory.java:124)
    at org.optaplanner.core.impl.domain.entity.descriptor.EntityDescriptor.processPlanningVariableAnnotation(EntityDescriptor.java:239)
    at org.optaplanner.core.impl.domain.entity.descriptor.EntityDescriptor.processAnnotations(EntityDescriptor.java:154)
    at org.optaplanner.core.impl.domain.solution.descriptor.SolutionDescriptor.buildSolutionDescriptor(SolutionDescriptor.java:106)
    at org.optaplanner.core.impl.solver.DefaultSolverFactory.buildSolutionDescriptor(DefaultSolverFactory.java:147)
    at org.optaplanner.core.impl.solver.DefaultSolverFactory.<init>(DefaultSolverFactory.java:69)
    at org.optaplanner.core.api.solver.SolverFactory.create(SolverFactory.java:106)
    at org.optaplanner.quarkus.bean.DefaultOptaPlannerBeanProvider.solverFactory(DefaultOptaPlannerBeanProvider.java:46)
    ...(省略剩余堆栈跟踪)

相关领域类定义如下:

LoadJob(PlanningEntity)

@PlanningEntity
class LoadJob {
    @PlanningId lateinit var id: String

    @InverseRelationShadowVariable(sourceVariableName = "tour")
    var vehicle: Vehicle? = null

    @PreviousElementShadowVariable(sourceVariableName = "tour")
    var previousLoadJob: LoadJob? = null

    @NextElementShadowVariable(sourceVariableName = "tour")
    var nextLoadJob: LoadJob? = null

    @JvmField
    @ShadowVariable(
        variableListenerClass = ArrivalTimeUpdateListener::class,
        sourceVariableName = "vehicle"
    )
    @ShadowVariable(
        variableListenerClass = ArrivalTimeUpdateListener::class,
        sourceVariableName = "previousLoadJob"
    )
    var arrivalTime: LocalDateTime? = null
}

Vehicle(PlanningEntity)

@PlanningEntity
class Vehicle {
    @PlanningId lateinit var planningId: String

    @PlanningListVariable lateinit var tour: MutableList<LoadJob>
}

TourPlan(PlanningSolution)

@PlanningSolution
class TourPlan {
    lateinit var id: String

    @PlanningEntityCollectionProperty
    lateinit var vehicles: List<Vehicle>

    @ValueRangeProvider
    @ProblemFactCollectionProperty
    lateinit var loadJobs: List<LoadJob>

    @PlanningScore
    var score: HardMediumSoftLongScore? = null

    @ConstraintConfigurationProvider
    var constraintConfiguration: TourPlanConstraintConfiguration = TourPlanConstraintConfiguration()

    constructor() // required for optaplanner
}

已知前提:

  • 切换到REFLECTION领域访问方式可解决问题,但性能不符合需求,希望统一使用GIZMO
  • 推测Quarkus测试阶段的类加载机制是问题根源,关联Quarkus Issue #34099和PR #34681
  • 暂未测试Timefold替代方案

解决方案与修复思路

1. 升级Quarkus版本

Quarkus PR #34681针对类加载器隔离冲突问题做了修复,该修复已合并到Quarkus 3.2.0.Final及以上版本。升级到3.2.x或更高版本,可直接解决GIZMO生成类与Quarkus测试类加载器的兼容性问题。

2. 调整Quarkus测试类加载配置

在application.properties中添加测试专属配置,强制OptaPlanner的GIZMO生成类使用Quarkus主类加载器:

# 仅在测试环境生效
%test.quarkus.optaplanner.gizmo.class-loader=parent-first

注:需确认所用OptaPlanner Quarkus扩展版本支持该配置参数,部分旧版本可能需要调整参数名。

3. 临时规避:隔离测试与生产配置(不推荐)

若无法升级版本,可通过构建工具的profile功能,为测试阶段临时切换到REFLECTION访问方式,避免影响生产环境性能:

<!-- Maven示例 -->
<profiles>
    <profile>
        <id>test</id>
        <properties>
            <optaplanner.domain.access-type>REFLECTION</optaplanner.domain.access-type>
        </properties>
    </profile>
</profiles>

4. 替换为Timefold

Timefold作为OptaPlanner的分支,对Quarkus集成做了针对性优化,且完全兼容OptaPlanner的API。替换依赖为Timefold后,可自动规避此类类加载冲突问题。

验证优先级

  1. 优先升级Quarkus到3.2.0.Final及以上版本,这是最直接的修复方式
  2. 若升级受阻,尝试调整类加载配置
  3. 前两者无效时,再考虑切换到Timefold

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 12:17:04