项目详情已经查到数据,接口却在写 JSON 时失败。点开返回对象,项目里有任务列表,每条任务又拿着所属项目。只要 Jackson 继续展开任务的 project 属性,它就会回到刚刚处理过的项目。
这个例子适用于 Spring Boot 3.5、JDK 17 或 21,以及使用 Jackson 2.x 写 JSON 的 Spring MVC 接口。@RestController 的返回值会交给消息转换器写入响应体,所以方法执行完毕之后,仍然可能在序列化阶段报错。Spring MVC 响应体文档 · Spring Boot JSON 文档
顺着异常里的属性路径查
外层出现 HttpMessageNotWritableException 时,展开异常链,找 Jackson 写到哪个属性失败。JsonMappingException 可以带上属性名和数组下标,日志里常以 through reference chain 显示。Spring 消息写入异常源码 · Jackson 映射异常源码
项目和任务互相引用时,属性路径会反复出现同一段。
Project.tasks[0]
→ Task.project
→ Project.tasks[0]
→ Task.project
→ …
在返回对象的位置打断点,取出第一条任务,对照 task.getProject() == project。这个表达式为真时,说明它们确实指向同一个项目实例。多层关系也用同样的办法,顺着字段和下标检查实例。
如果日志提示 `Document nesting depth›,再确认数据是否重复走回同一对象。Jackson 2.16 起提供写入深度限制,2.19 源码中的默认上限是 1000,项目可以按自己的配置调整。一个层级很深的树也可能触发这项限制,检查时要把属性路径和对象引用放在一起看。Jackson 写入深度限制源码
栈里如果一直重复的是 toString()、equals() 或 hashCode(),就回到对应的方法调用处。比如日志打印实体时先触发了 toString(),排查点就在日志调用与实体方法。
把接口需要的字段放进响应 DTO
假设项目详情页只用项目 ID、名称和任务的 ID、标题。可以用 ProjectResponse 定义这份响应,再把查到的项目映射进去。
下面的类放在能访问项目领域类型的位置。示例中的 Project 提供 getId()、getName() 和 getTasks(),任务列表已经加载且始终有值,无任务时返回空列表。任务元素提供 getId() 和 getTitle()。
import java.util.List;
public record ProjectResponse(
long id,
String name,
List<ProjectResponse.TaskItem> tasks
) {
public record TaskItem(long id, String title) {}
public static ProjectResponse from(Project project) {
List<TaskItem> tasks = project.getTasks().stream()
.map(task -> new TaskItem(task.getId(), task.getTitle()))
.toList();
return new ProjectResponse(project.getId(), project.getName(), tasks);
}
}
让项目详情接口返回这个 DTO。用项目 42 和任务 7 作为示例,默认字段名下的响应可以长这样。
{
"id": 42,
"name": "演示项目",
"tasks": [
{
"id": 7,
"title": "整理文档"
}
]
}
如果任务详情还需要显示项目名称,可以在任务响应里定义 ProjectRef(id, name),让这一层结束在项目摘要。两个接口各自按页面要用的字段组装。
任务数据需要额外查询时,在服务层显式取出本次响应要用的字段。列表请求尤其要留意每条记录是否触发一次关联查询。可以对照本站的关联加载排查步骤。MyBatis 一页数据查出几十条 SQL
已经用了引用注解的项目
Jackson 的 @JsonManagedReference 和 @JsonBackReference 是一对有方向的属性标记。用在这个模型时,可以将前者标在 Project.tasks,后者标在 Task.project,并使用匹配的名称,如 project-tasks。项目序列化时会展开任务列表,任务上的反向引用会从 JSON 中省略。Jackson 正向引用源码 · Jackson 反向引用源码
调整注解前,先查哪些接口也用这个任务类型。独立返回任务详情时,标了反向引用的 project 字段也会被省略。请求体反序列化如果复用同一类型,也要验证成对注解重建反向引用的行为。
用原来的接口验证响应
本地开发实例已经启动、项目 42 存在并且接口允许当前客户端访问时,可以用下面的 GET 请求检查状态和正文。端口、路径和项目 ID 按实际环境替换。
curl -i -sS --max-time 10 "http://127.0.0.1:8080/projects/42"
先确认响应体可以完整解析,再核对字段与层级。项目有任务时,检查 tasks[0] 下的字段。项目无任务时,检查 tasks 是否符合客户端约定,例如空数组 []。如果同时有任务详情接口,再确认它能按约定返回项目摘要。
服务端仍有消息写入异常时,回到同一次请求的属性路径,检查响应是否还包含实体、代理对象或其他会继续展开的字段。接口有统一异常处理时,也可以对照现有处理路径定位记录异常的位置。Spring Boot 全局异常处理
Spring Boot 返回 JSON 报循环引用怎么查
https://java.li/archives/spring-boot-json-circular-reference
评论