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

Spring Boot MVC项目CRUD控制器单元测试报StackOverflowError原因排查

Spring Boot MVC 单元测试栈溢出问题定位与修复

你看到的满屏Logback类的报错栈,根本不是故障根源,只是Logback在打印已经无限递归的异常时的连锁反应,真正的问题出在测试打桩逻辑和控制器调用流程的错配上。

完整触发链路

顺着你的控制器代码走一遍测试执行流程,就能顺清楚栈溢出的来龙去脉:

  • 测试发起DELETE /pessoas/89L请求,进入控制器删除方法
  • 控制器第一步调用pessoaRepository.getOne(89L),但你写测试时完全没给这个方法打桩,Mockito对无打桩的方法默认返回null
  • 控制器拿到null值后,继续执行pessoaRepository.delete(null)
  • 你之前给delete方法设置的打桩规则是「收到你测试里手动新建的那个id=89的Pessoa对象时,执行无操作逻辑」,现在实际传的是null,根本匹配不上这个规则
  • 当入参不匹配打桩规则时,Mockito会抛出参数不匹配异常,异常信息里需要对比预期入参和实际入参的差异,就要把你作为打桩参数传入的Pessoa对象序列化打印。如果你的Pessoa实体存在双向关联(比如Pessoa关联Endereco,Endereco又反向关联Pessoa),调用toString时就会无限循环拼接关联属性,直接占满JVM方法栈
  • 后续Logback捕获到这个已经无限递归的异常时,会反复递归构造异常代理、拼接异常栈文本,最终你看到的报错栈全是Logback相关方法,很容易误导人去调整日志配置。

具体修复方法

1. 补全所有被调用方法的打桩逻辑

对照控制器的真实调用流程,把所有会触发的Repository方法都做打桩,不要漏写。比如这个删除接口的正确测试实现:

@Test
public void deletarPessoa_quandoDelete() throws Exception {
    Long alvoId = 89L;
    Pessoa pessoa = new Pessoa();
    pessoa.setNome("Test Nome");
    pessoa.setId(alvoId);

    // 严格按照控制器的调用顺序打桩
    when(pessoaRepository.getOne(alvoId)).thenReturn(pessoa);
    doNothing().when(pessoaRepository).delete(pessoa);

    mockMvc.perform(delete("/pessoas/" + alvoId)
            .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isNoContent());
}

这个调整的本质是让Mockito完全模拟出控制器预期拿到的参数,从根源上避免参数不匹配、对象为null的问题。

2. 加通用防护避免同类问题重复出现

  • 检查所有JPA实体类的toString()实现,对双向关联的字段做截断,只打印关联对象的ID值,不要全量展开打印关联对象的所有属性,从根源切断双向引用导致的递归
  • 单元测试开启Mockito严格桩校验模式,只要出现漏打桩、打桩入参和实际调用不匹配的情况,Mockito会直接抛出明确的定位异常,不会等到递归栈溢出才暴露问题
  • 给Logback配置异常栈的最大打印深度,就算碰到超长的异常栈,也不会因为反复拼接栈文本占满内存,配置参考:
<conversionRule conversionWord="ex" converterClass="ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter" />
<property name="LOG_MAX_STACK_DEPTH" value="100" />

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.28 15:45:04