背景
JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion
当时开发的时候报了这个错。
看报错就知道是因为,json的相互依赖。
比如A 中有一个属性是 B , 而B中也有一个属性是A。 这将造成了json序列化的时候,报相互依赖的异常。
而正好,当前的代码就是这么干的。
@Entity
public class DivisionalWorksType {
@ApiModelProperty("评分项目模版")
@OneToMany(mappedBy = "divisionalWorksType")
@JsonView(ScoringItemTemplateJsonView.class)
List scoringItemTemplates = new ArrayList<>();
}
@Entity
public class ScoringItemTemplate {
@ManyToOne
@ApiModelProperty("所属分布工程模版类型")
@JsonView(DivisionalWorksTypeJsonView.class)
private DivisionalWorksType divisionalWorksType;
}
序列化
可能还有的人不太清楚序列化。
什么是序列化与反序列化?
Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程。
为什么要实现对象的序列化和反序列化?
- 我们创建的Java对象被存储在Java堆中,当程序运行结束后,这些对象会被JVM回收。但在现实的应用中,可能会要求在程序运行结束之后还能读取这些对象,并在以后检索数据,这时就需要用到序列化。
- 当Java对象通过网络进行传输的时候。因为数据只能够以二进制的形式在网络中进行传输,因此当把对象通过网络发送出去之前需要先序列化成二进制数据,在接收端读到二进制数据之后反序列化成Java对象。
spring boot中如何实现的序列化?
答案是:通过Serializable
接口
可以发现,该接口是空的。
实际上,Serializable
这个标志接口是给java虚拟机参考的,java虚拟机看到这个接口之后,会为该类自动生成一个序列化版本号。
有人感到疑惑:实际在开发中,有时候我们的类并没有实现Serializable接口。但是为什么仍然能通过网络传输?
答案也许是:类里的属性的基本类型,都基本实现了Serializable接口
可以看到,很多的java类型都实现了Serializable接口。 这时候,就可以很快地将该对象里的属性序列化。
同时Spring框架帮我们做了另一些一些事情:Spring并不是直接把Object进行网络传输,而是先把Object通过序列化转换成json格式的字符串,然后再进行传输的。
这里的序列化也可以表示为: 序列化就是将对象转换成Json格式的字符串,反序列化就是逆过程,将Json串转换成对象。
Spring框架如何将数据转换成Json?
通常,我们使用的时候,只需要在Controller类中做如下定义:
@RestController
@RequestMapping("User")
public class UserController {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public User save(@RequestBody User user) {
return this.userService.save(user);
}
}
在 Controller 中使用 @ResponseBody
注解即可返回 Json 格式的数据,
@ResponseBody
的作用,其实是将Controller的方法返回的对象通过转换为指定格式之后,写入到response对象的body区,通常用来返回json或者xml数据
而 @RestController 注解包含了 @ResponseBody
注解,所以默认情况下, @RestController即可将返回的数据结构转换成Json格式。
所以,我们使用@RestController注解即可。
这些注解之所以可以进行 Json 与 Java类 之间的相互转换,就是因为HttpMessageConverter发挥着作用。
HttpMessageConverter
前端发来请求后,
1.调用HttpInputMessage从输入流中获取Json字符串
2.在HttpMessageConverter中把Json转换为接口需要的形参类型。
3.在HttpMessageConverter将Json转换为Java实体类
至于HttpMessageConverter 怎么处理,这里就不展开说明了
解决问题
回到背景问题。
当时去谷歌之后有两个比较方便的方法:
1.使用@JsonManagedReference
和@JsonBackReference
注解。
将@JsonManagedReference
注释添加到父级模型,即父级的getter方法
@JsonManagedReference
public List getDivisionalWorksTypes() {
return divisionalWorksTypes;
}
再将@JsonBackReference
添加到子级模型
@JsonBackReference
public DivisionalWorksTemplate getDivisionalWorksTemplate() {
return divisionalWorksTemplate;
}
2.使用@JsonIgnore
注解
@JsonIgnore
public List getDivisionalWorksTypes() {
return divisionalWorksTypes;
}
原理:
@JsonManagedReference
:管理引用的一方,可以理解为具有引用的一方,被这个注解的属性序列化时会正常获取@JsonBackReference
:反向引用,可以理解为此属性为反向引用,被这个注解的属性序列化时会忽略
一个用于父级角色,另一个用于子级角色:
用了这两个注解后,作用即: 一方不被序列化
再来看@JsonIgnore
@JsonIgnore并不是为解决无限递归问题而设计的,它只是忽略了注释属性不被序列化或反序列化。但是,如果字段之间存在双向链接,则由于@JsonIgnore会忽略带注释的属性,因此可以避免无限递归。
后话
当时用了@JsonManagedReference 和 @JsonBackReference 这对注解后,便没有再报错了。
但是之后删除这两个注解后,也并不会报错。
或许是因为后来加上了 @JsonView 注解,控制输入输出后的json。
entity:
@ApiModelProperty("分布工程类型")
@OneToMany(mappedBy = "divisionalWorksTemplate")
@JsonView(DivisionalWorksTypesJsonView.class)
List divisionalWorksTypes = new ArrayList<>();
controller:
@GetMapping("page")
@JsonView(PageJsonView.class)
public Page page(
@RequestParam(required = false) String name,
@SortDefault.SortDefaults(@SortDefault(sort = "id", direction = Sort.Direction.DESC)) Pageable pageable
) {
return this.divisionalWorksTemplateService.page(name, pageable);
}
private class PageJsonView implements DivisionalWorksTemplate.DivisionalWorksTypesJsonView, DivisionalWorksType.ScoringItemTemplateJsonView {
}