PO Serialization

看起来非常顺利,那我们试着通过 @RestController 将得到的实体传给前端吧:

序列化:就是将对象转化成字节序列的过程。

反序列化:就是讲字节序列转化成对象的过程。

@RestController
@RequiredArgsConstructor
public class studentController {
    private final StudentRepository studentRepository;
    private final TuitionRepository tuitionRepository;

    @GetMapping("/all/student")
    public List<Student> getAllStudent() {
        return studentRepository.findAllBy(Student.class);
    }

    @GetMapping("/all/tuition")
    public List<Tuition> getAllStudent() {
        return tuitionRepository.findAllBy(Tuition.class);
    }
}

不好了,前端请求 API 时后端抛出了 java.lang.StackOverflowError

这个异常其实是 Jackson 抛出的。由于 Student 里有 TuitionTuition 里有 Student,这样无限套娃,Jackson 的序列化就无限地递归处理下去直到崩溃了。

要处理这个问题也好办,有两种方法:

  • 方法一 @JsonBackReference @JsonManagedReference

    在 owning side 使用 @JsonBackReference,在 non-owning side 使用 @JsonManagedReference 注解。

    @Entity
    @Table(name = "student")
    public class Student {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
        private String name;
    
        @JsonManagedReference
        @OneToOne(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true, fetch=FetchType.LAZY)
        private Tuition tuition;
    
        /* Getters and setters */   
    }
    @Entity
    @Table(name = "tuition")
    public class Tuition {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
        private Double fee;
    
        @JsonBackReference
        @OneToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "student_id")
        private Student student;
    
        /* Getters and setters */    
    }

    序列化结果:

    • /all/student

      [
          {
              "id": 1,
              "name": "张三",
              "tuition": {
                  "id": 2,
                  "fee": 1145.14
              }
          }
      ]

      我们发现,方法一序列化 Student 时,成功将其内部的 tuition 序列化,并且内部 tuitionstudent 域被忽略,消除了循环引用。

    • /all/tuition

      [
          {
              "id": 2,
              "fee": 1145.14
          }
      ]

      很遗憾,tuition 的其他域虽然被成功序列化,其 student 域无法被实例化。

    总结:使用 @JsonBackReference @JsonManagedReference 确实可以解决循环引用的问题,但是序列化只有 non-owning side 会序列化内部的引用,并且随着数据库关系变得复杂,这种解决方法渐渐变得无法维护,思考谁是 owning side 容易把人绕晕。

  • 方法二 @JsonIdentityInfo

    方法二非常简单,只需要无脑地对所有实体加上 @JsonIdentityInfo 注解即可。

    @Entity
    @Table(name = "student")
    @JsonIdentityInfo(generator = ObjectIdGenerators.UUIDGenerator.class, property = "@id")
    public class Student {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
        private String name;
    
        @OneToOne(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
        private Tuition tuition;
    
        /* Getters and setters */   
    }
    @Entity
    @Table(name = "tuition")
    @JsonIdentityInfo(generator = ObjectIdGenerators.UUIDGenerator.class, property = "@id")
    public class Tuition {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
        private Double fee;
    
        @OneToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "student_id")
        private Student student;
    
        /* Getters and setters */    
    }

    序列化结果:

    • /all/student

      [
          {
              "@id": "13389f64-688b-4162-b030-1039133c15f6",
              "id": 1,
              "name": "张三",
              "tuition": {
                  "@id": "9b90d0bb-d267-4111-a262-bfca07e43f13",
                  "id": 2,
                  "fee": 1145.14,
                  "student": "13389f64-688b-4162-b030-1039133c15f6"
              }
          }
      ]
    • /all/tuition

      [
          {
              "@id": "5fc601fc-6eae-489b-a225-fb205f7c2d0c",
              "id": 2,
              "fee": 1145.14,
              "student": {
                  "@id": "adc4d249-de70-4070-ae33-dce1f1384574",
                  "id": 1,
                  "name": "张三",
                  "tuition": "5fc601fc-6eae-489b-a225-fb205f7c2d0c"
              }
          }
      ]

    可以看到,无论是 owning side 还是 non-owning side 序列化,都能完整地展现全部信息。并且,序列化得到的 JSON 中,不仅包含了原来的域,还新包含了 @id 域。每一次序列化,Jackson 用不同的 UUID 标记每个对象,这样就可以在遇到已经序列化的对象时停止序列化,而将那个域的值设置为引用的 UUID。

  • 方法三

    为什么要直接向前端返回 PO 呢?

    我们应该创建合适的 VO,将 PO 在 service 层转化为 VO 再返回给前端,这样不就好了。

最后更新于