JUnit4迁移至JUnit5触发Spark Task not serializable异常排查
问题根因
从报错栈可以直接定位到序列化失败的元凶是org.mockito.internal.junit.DefaultStubbingLookupListener:这个类是Mockito对接JUnit5时,在严格桩模式下自动给测试实例注册的内部监听器,本身没有实现序列化接口。
你的测试类实现了Serializable接口,而JUnit5默认每个测试方法创建全新的测试类实例,MockitoExtension在初始化测试实例时,会把上述不可序列化的监听器绑定到测试对象上。当你在测试类的非静态方法vcfFileAndHeaderToString中编写Spark的map转换闭包时,Spark为了把闭包分发到Executor执行,会尝试序列化闭包依赖的所有外部对象——这里就包含了当前测试类的实例,连带绑定在实例上的Mockito监听器也会被纳入序列化范围,直接触发序列化异常。
JUnit4环境下运行正常,是因为旧版Mockito针对JUnit4的Runner/Rule不会注册这个不可序列化的监听器,不存在这个额外的非序列化字段绑定。
修复方案
按优先级从高到低选择即可:
- 方案1:将涉及Spark闭包的方法改为静态方法,切断闭包对测试类实例的引用
把vcfFileAndHeaderToString改成静态方法,将方法依赖的SparkSession作为入参传入。静态方法不存在外部类this引用,Spark序列化闭包时不会捕获测试类实例,自然不会触发Mockito相关组件的序列化:
调用时传入当前的sparkSession实例即可:private static Dataset<String> vcfFileAndHeaderToString(VCFHeaderAndRows headerAndRows, SparkSession sparkSession) { List<String> columns = Collections.singletonList(headerAndRows.getHeader().get(headerAndRows.getHeader().size() - 1)); Dataset<String> columnsDS = sparkSession.sqlContext().createDataset(columns, Encoders.STRING()); Dataset<String> rows = headerAndRows.getRows() .map((MapFunction<VCFRow, String>) x -> x.getRow(), Encoders.STRING()); return columnsDS.union(rows).coalesce(1); }Dataset<String> result = vcfFileAndHeaderToString(exporter.runExport(defaultParameters), sparkSession); - 方案2:关闭Mockito严格桩模式,避免生成不可序列化的监听器
通过@MockitoSettings注解将测试类的桩严格级别设置为宽松模式,Mockito就不会自动注册DefaultStubbingLookupListener:
注意这个方案会关闭Mockito的未使用桩检测、多余调用检测能力,可能遗漏测试编写问题,优先选择方案1。@ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) public class ExportLogicTest implements Serializable { // 原有业务代码无需修改 } - 方案3:将所有需要在Spark Executor端执行的闭包逻辑抽离到单独实现了
Serializable接口的工具类中,不要在测试类内部定义会被分发到Executor的闭包,从根源避免测试类实例被Spark闭包捕获。
内容的提问来源于stack exchange,提问作者Abdusoli
相关产品推荐
相关产品推荐

