为何@JsonIgnore能修复同一实体父子关系的JSON无限递归问题?
最近在给Trash实体类做父子关联的时候踩了个坑:当我发起GET请求获取带有子实体的对象时,直接触发了栈溢出错误,看报错信息是无限递归导致的。后来在parent字段上加了@JsonIgnore注解就搞定了,这里给大家拆解下问题根源和这个注解的修复原理。
问题重现的代码和报错
实体类代码
@Entity @Table(name = "trash") public class Trash { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank private String username; @NotBlank private String message; private Long likes; @ManyToOne(cascade = CascadeType.ALL,fetch = FetchType.LAZY) @JsonIgnore // 加了这个注解解决问题 private Trash parent; @OneToMany(cascade = CascadeType.ALL, mappedBy="parent", orphanRemoval=true, fetch = FetchType.LAZY) private Collection<Trash> children; }
报错信息
2019-07-19 10:29:55.867 ERROR 14156 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: com.example.litter.model.Trash["parent"]->com.example.litter.model.Trash$HibernateProxy$lBPNIlf1["children"]->org.hibernate.collection.internal.PersistentBag[0]->com.example.litter.model.Trash["parent"]->...重复多次...->com.example.litter.model.Trash["parent"])] with root cause java.lang.StackOverflowError: null
原问题的根源
这个问题的核心是双向关联导致的JSON序列化无限递归:
- 我们的
Trash实体是双向关联:父对象通过children字段关联子对象,子对象通过parent字段关联父对象; - 当Spring MVC要把
Trash对象转换成JSON返回给前端时,会用Jackson遍历对象的所有字段进行序列化:- 先序列化一个父
Trash对象,处理到children字段时,会逐个序列化每个子Trash对象; - 序列化子
Trash对象时,又会处理它的parent字段,回到父Trash对象; - 父对象再次被序列化时,又会处理
children字段,再次进入子对象的序列化,循环往复没有终止;
- 先序列化一个父
- 这个递归过程会持续消耗栈内存,直到栈空间耗尽,就抛出了
StackOverflowError,报错信息里的引用链也清晰展示了这个循环路径。
@JsonIgnore注解的修复原理
@JsonIgnore是Jackson JSON库提供的核心注解,它的作用很直接:告诉JSON序列化器和反序列化器,完全忽略被标注的字段。
- 当我们给
parent字段加上这个注解后,Jackson在序列化Trash对象时,会跳过parent字段的处理:- 序列化父对象时,正常处理
children集合,生成子对象的JSON; - 序列化子对象时,不会去处理它的
parent字段,也就不会回到父对象触发循环;
- 序列化父对象时,正常处理
- 这就彻底打断了递归链,序列化过程可以正常完成,自然不会再出现栈溢出的问题。
小补充:如果需要在JSON中保留parent的部分信息,也可以用@JsonManagedReference(标记父方的children字段)和@JsonBackReference(标记子方的parent字段)组合,它们会处理双向关联的序列化,避免递归,同时保留关联信息。不过如果不需要返回parent字段,@JsonIgnore是最简单高效的方案。
内容的提问来源于stack exchange,提问作者Eric Tercasio

