【Spring】java 单体式应用微服务拆分过程 bug 修复:Json Infinite Recursion

【Spring】java 单体式应用微服务拆分过程 bug 修复:Json Infinite Recursion

Fre5h1nd Lv5

💡简介

  • 将单体式 Spring 应用拆分为微服务式应用过程中,遇到了一系列有关 Spring 框架的问题,将修复过程及相关问题信息分析过程记录下来,便于以后解决同类问题,也顺便提高自己对 Spring 框架的理解。

  • 前期还遇到Serialize 类序列化失败等问题,暂未记录下来,未来有机会再回顾并补充相关记录。

🧠问题1️⃣——Json Infinite Recursion

✏️问题描述

  • 项目中涉及多个循环引用的类,例如最典型的 PostUser,代码如下所示:
1
2
3
4
5
6
7
public class Post {
@ManyToOne
private User user;

...

}
1
2
3
4
5
6
7
public class User {
@OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.REMOVE)
private Collection<Post> posts = new ArrayList<>();

...

}
  • 问题也很明显,当压缩为 JSON 格式数据时,不断循环引用导致出错。

ℹ️报错信息

1
feign.codec.EncodeException: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: com.raysmond.blog.common.models.User["post"]->com.raysmond.blog.common.models.Post["seoData"]->com.raysmond.blog.common.models.User["post"]->com.raysmond.blog.common.models.Post["seoData"]->...)

🔨解决方法1️⃣——@JsonIgnore

  • 算是比较粗鲁的方法。
  • @JsonIgnore 能够在 JSON 序列化时将一些属性忽略[1],从而避免循环引用。
    • 具体而言,将注解标记在属性或者方法上,序列化生成的 JSON 数据中将不包含该属性,反序列化时也不会读取该属性。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      public class Post {
      @ManyToOne
      private User user; // 保持正常

      ...

      }
      1
      2
      3
      4
      5
      6
      7
      8
      public class User {
      @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.REMOVE)
      @JsonIgnore // 序列化时忽略
      private Collection<Post> posts = new ArrayList<>();

      ...

      }

🔨解决方法2️⃣——@JsonManagedReference 和 @JsonBackReference

  • 比上一种更合理的方法,不影响数据内容。

  • 通过将循环引用关系双方中的一方不序列化,来避免出现无限循环,使 Jackson 正常工作。[2][3]

  • 因此,Jackson 会获取引用的前向部分(被标记了 @JsonManagedReference 注解的部分),并将其转换为类似 json 的存储格式;这就是正常 marshalling 过程。然后,Jackson 会查找引用的后向部分(被标记了 @JsonBackReference 注解的部分),不对其进行序列化,这部分关系将在前向引用的反序列化(解Marshalling)过程中正常重新构建,不影响使用。

    • marshalling可译作集结、结集、编码、编组、编集、安整、数据打包、列集等,是计算机科学中把一个对象的内存表示变换为适合存储或发送的数据格式的过程。典型用于数据必须在一个程序的两个部分之间移动,或者必须从一个程序移动到另一个程序。Marshalling类似于序列化,可用于一个对象与一个远程对象通信。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      public class Post {
      @ManyToOne
      @JsonManagedReference
      private User user; // 保持正常

      ...

      }
      1
      2
      3
      4
      5
      6
      7
      8
      public class User {
      @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.REMOVE)
      @JsonBackReference // 序列化时忽略
      private Collection<Post> posts = new ArrayList<>();

      ...

      }
  • ⚠️注:仅对 OneToMany 或 OneToOne 情况适用,ManyToMany 下不适用。

🔨解决方法3️⃣——@JsonIdentityInfo

  • 最简单的一种方法(个人认为),因为不用手动设定父子关系,仅需给整个类进行注解标记即可。

  • 使 Jackson 只在第一次遇到时序列化为完整的对象,之后再遇到同一个对象,都以对象的标识符 ID(或其他属性)代替。这样就不会序列化每个对象都 “扫描 “一次,在多个相互关联的对象之间形成链式循环时非常有用(例如:订单 -> 订单行 -> 用户 -> 订单,如此反复)。[2]

    • @JsonIdentityInfo 相关参数定义[3]
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      public @interface JsonIdentityInfo
      {
      // Object ID 的 JSON 属性名
      public String property() default "@id";

      // Object ID 的生成器
      public Class<? extends ObjectIdGenerator<?>> generator();

      // 将 Object ID 解析回对象的解析器
      public Class<? extends ObjectIdResolver> resolver() default SimpleObjectIdResolver.class;

      public Class<?> scope() default Object.class;
      }
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = Post.class)
      public class Post {
      @ManyToOne
      @JsonManagedReference
      private User user; // 保持正常

      ...

      }
      1
      2
      3
      4
      5
      6
      7
      8
      @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = User.class)public class User {
      @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.REMOVE)
      @JsonBackReference // 序列化时忽略
      private Collection<Post> posts = new ArrayList<>();

      ...

      }
  • ⚠️注:

    • 后续发现当同一个对象被传输两次及以上时,会报错:
      1
      Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Already had POJO for id
  • 根据相关回答[5],问题出在同一个变量出现两次:

    The problem is that Jackson fails becouse the same entity is present two times.
    The solution is send only the plain ID in the second instance, not wrapped in object.

  • 由于没想好如何跟他一样在第二次传输时修改传输内容,最终选择采用第一种方案

  • 另外,发现一篇非常全面的好文[6],里面包含了各种有关 Jackson 序列化的小知识。

🚧ToDoList(也许会补上)

  1. Spring 框架基础原理
  2. Jackson 组件原理及相关注解作用
  3. Post not found: program_language/java-spring-bug-4xx 到底为啥报4xx相关错误
  4. Post not found: program_language/java-spring-bug-json-infinite-recursion json循环引用报错
  5. Serialize Json 报错

  • 希望这篇博客对你有帮助!如果你有任何问题或需要进一步的帮助,请随时提问。
  • 如果你喜欢这篇文章,欢迎动动小手 给我一个follow或star。

🗺参考文献

[1] 注解@JsonIgnore的使用方法及其效果

[2] Infinite Recursion with Jackson JSON and Hibernate JPA issue

[3] Jackson 的 JsonManagedReference 和 JsonBackReference 注解

[4] @JsonIdentityInfo 处理循环引用

[5] Serializing Already had POJO for id (java.lang.String)

[6] 14.5 使用Jackson序列化为JSON_XML_MessagePack

  • 标题: 【Spring】java 单体式应用微服务拆分过程 bug 修复:Json Infinite Recursion
  • 作者: Fre5h1nd
  • 创建于 : 2023-08-27 01:28:01
  • 更新于 : 2023-09-04 21:07:23
  • 链接: https://freshwlnd.github.io/2023/08/27/program_language/java/java-spring-bug-json-infinite-recursion/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论