使用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后,可自动规避此类类加载冲突问题。
验证优先级
- 优先升级Quarkus到3.2.0.Final及以上版本,这是最直接的修复方式
- 若升级受阻,尝试调整类加载配置
- 前两者无效时,再考虑切换到Timefold
内容的提问来源于stack exchange,提问作者greyhairredbear
相关产品推荐
相关产品推荐

