JsonView:解决JPA表关系递归溢出以及jackson序列化问题

·JPA ·Hibernate ·ORM

我在使用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年壬寅虎年,心想事成、开心如意!