JsonView:解决JPA表关系递归溢出以及jackson序列化问题
我在使用Spring Data JPA(Hibernate),当我使用@OneToOne或其他注解时,返回给客户端抛出了jackson递归溢出异常:
nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError); ........ ........。很显然,这是在实体A中一对一实体B,而实体B反过来也一对一实体A,从而导致jackson序列化时不断递归的问题。于是我尝试使用@JsonBackReference注解解决此问题,如:
@Entity @Getter @Setter @NoArgsConstructor public class A { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; private String name; @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "b_id") private B b; } // 实体B @Entity @Getter @Setter @NoArgsConstructor public class B { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; private String name; @OneToOne(cascade = CascadeType.ALL, mappedBy = "b") @JsonBackReference private A a; }可以看到,在B中使用了@JsonBackReference注解,这确实会避免循环依赖,因为json序列化时,Jackson忽略了B实体中的引用字段a。当我如果用B实体做主控去查询关联的A呢?此方式直接就忽略了引用字段a的解析,显然不可以!于是我寻求google,发现了另一个注解@JsonIdentityInfo,它需要在相关实体类上标注,让Jackson避免多余的递归依赖。
(generator =ObjectIdGenerators.PropertyGenerator.class, property = "id") public class A { ... } (generator =ObjectIdGenerators.PropertyGenerator.class, property = "id") public class B { ... }但此注解在@OneToMany或@ManyToOne上将有不期望的返回结果:如果A和B是多对多关系,那么A类中必然有关联的B类的列表对象字段,反之亦然:
/**实体A*/ @ManyToMany @JoinTable(name = "a_b", joinColumns = @JoinColumn(name = "a_id"), inverseJoinColumns = @JoinColumn(name = "b_id")) private List<B> bList;上述情况使用@JsonIdentityInfo虽然能够正常返回,但是bList中只有首个元素能够显示关联的B的信息,之后的元素将只会显示B类对象的id,即显示这样的效果:
{ "id": 1, "name": "a1", "bList":[ {"id": 1, "name": "b1"}, "2", "3" ....... ] }这显然有问题,因为我只想:用A实体查就显示A实体,并且显示B实体的id和name就行,B实体中泛型A的列表不要在往下循环依赖了!反之亦然!因此我Google了一圈,尝试使用@JsonIgnoreProperties。@JsonIgnoreProperties非常好,如果你在一对一的关系中,也就是实体表中依赖关系不多的情况下可以使用,它能够按需过滤掉相关对象中的某些字段!比如我想过滤学生对应的学校的所有学生属性:
@Entity @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class Student { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @JsonIgnoreProperties({"students"}) private School school; }我查学生,当然只想获取此学生的信息以及他关联的学校的信息,他关联的学校关联的所有学生信息拿来有什么用?因此此注解可以忽略某个引用字段中的某些字段不被序列化!但是此注解用在列表字段上将无用:
@JsonIgnoreProperties({"name"}) private List<School> schools;我想忽略学校列表中每个学校的校名,可schools它是个列表对象,jackson只能知道单一的school对象,因此List接口中没有name属性!若此注解可以解决此问题,相比对我来说是最佳方案!虽然我在google中看到有人回答可以使用此注解解决序列化问题并且有效(我使用Spring Jpa 2.1以上版本,版本兼容无误)可是同样的操作对我来说无效,因此我不得不把目光转向了@JsonView。使用JsonView,在实体类中归类当前接口你想返回的数据,完美解决了递归依赖且数据有误的问题:
public class BaseJsonViewer { // 你甚至可以定义一个父接口,让父接口包含一些共用的字段 public interface BaseJsonView { } public interface AJsonView extends BaseJsonView{ } public interface BJsonView extends BaseJsonView{ } } // 实体A public class A { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @JsonView({BaseJsonViewer.AJsonView.class}) private long id; @OneToOne(cascade = CascadeType.ALL) @JsonView(BaseJsonViewer.AJsonView.class) private B b; } // 实体B public class B { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @JsonView({BaseJsonViewer.AJsonView.class}) private long id; @OneToOne(cascade = CascadeType.ALL) private A a; }大家注意看,我在实体B中的引用字段a没有添加@JsonView({BaseJsonViewer.AJsonView.class})注解,这将让jackson忽略B.a,但是A中的所有以及A关联的B的所有(不包括a)都将被解析。在控制层,务必添加接口需要返回的jsonview:
/** * Description: 根据id获取A。 * 返回AJsonView包含的字段 */ @GetMapping("/{id}") @JsonView(BaseJsonViewer.AJsonView.class) public A get(@PathVariable("id") Integer id) { return aService.findAById(id); }JsonView灵活度很高,想返回什么就返回什么(序列化),不影响反序列化。唯一缺点是,实体类每个字段都要声明属于哪一个jsonview。若一个实体有多个关联关系(一对多,多对多.......),将导致杂乱无章。还有一个方案,使用JsonFilter,不过我不想使用。它需要WriteAsString然后进行过滤一遍,再封装返回,若一条条数据(List)嵌套那,时间开销将很大!而且我认为,这循环依赖的问题应该从jpa层面解决,而不是用jackson。jpa查询出来就已经是循环依赖了,因此jpa我目前认为很费时且做为一个ORM框架比隔壁C#的ORM(如EF Core)要笨。或者自己写逻辑去关联,那我还需要jpa导航查询干什么呢!定义一堆表关系又何必。
来自:Java
更新于2022-09-14 21:49:17 发表于2022-09-13 22:19:27
更新于2022-09-14 21:49:17 发表于2022-09-13 22:19:27
发表您的评论