You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jackson序列化@ManyToMany仅数据库调用触发无限递归问题排查

Hibernate实体转POJO时仅数据库访问触发无限递归问题

问题背景

我用Hibernate EntityBeans实现数据库访问,通过Jackson ObjectMapper将实体对象转换为UI层使用的POJO,为此编写了分层测试:

  • 0层测试:简单对象转换(正常)
  • 1层测试:带关联结构的对象转换(正常)
  • 2层测试:逻辑与1层一致,但涉及持久化操作(触发无限递归错误)
  • 3层测试:暂不相关

我不想在@ManyToMany关联上使用@JsonBackreference和@JsonManagedReference,因为需要保留双向数据;而@ManyToOne和@OneToMany关联无此问题。希望先明确问题根源,而非直接编写自定义转换器。

相关类结构

Product实体类

@Entity
@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id", scope = Company.class)
public class Product extends ProductBase {

// 其他无关代码
    @Getter
    @Setter
    @ManyToOne
    @JoinColumn(name = "fk_manufacturer")
    private Company manufacturer;

    @Getter
    @Setter
    @ManyToMany(fetch = FetchType.LAZY, mappedBy = "supplierProducts")
    private Set<Company> supplier = new HashSet<>();
// 其他无关代码
}

ProductPOJO类

@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id", scope = Company.class)
public class ProductPojo extends ProductBase {

// 其他无关代码
    @Getter
    @Setter
    private CompanyPojo manufacturer;

    @Getter
    @Setter
    private Set<CompanyPojo> supplier = new HashSet<>();

// 其他代码
}

Company实体类

@Entity
@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id", scope = Product.class)
public class Company extends CompanyBase {
// 其他字段
    @Getter
    @Setter
    @Column
    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name = "fk_product_manufacturer")
    private Set<Product> manufacturerProducts = new HashSet<>();

    @Getter
    @Setter
    @Column
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "supplier_products",
            joinColumns = {@JoinColumn(name = "fk_company_id")},
            inverseJoinColumns = {@JoinColumn(name = "fk_product_id")})
    private Set<Product> supplierProducts = new HashSet<>();
// 其他字段
}

CompanyPOJO类

@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id", scope = Company.class)
public class CompanyPojo extends CompanyBase {
// 代码
    @Getter
    @Setter
    private Set<ProductPojo> manufacturerProducts = new HashSet<>();

    @Getter
    @Setter
    private Set<ProductPojo> supplierProducts = new HashSet<>();
// 其他代码...
}

错误信息

java.lang.IllegalArgumentException: Infinite recursion (StackOverflowError) (through reference chain: de.bloise.data.persistence.unities.Company["supplierProducts"]->org.hibernate.collection.internal.PersistentSet[0]->de.bloise.data.persistence.product.Product["supplier"]->org.hibernate.collection.internal.PersistentSet[0]->de.bloise.data.persistence.unities.Company["supplierProducts"]->.....

问题根源与解决思路

核心差异分析

两种测试场景的本质区别在于集合类型:

  • 无数据库访问时,使用的是普通HashSet,Jackson能通过@JsonIdentityInfo正确识别对象身份,第一次序列化后会记录对象ID,后续遇到同一对象时仅输出ID,避免递归。
  • 数据库访问后,Hibernate会把集合替换为PersistentSet(懒加载代理集合)。这个代理类在Jackson序列化时,会干扰@JsonIdentityInfo的身份识别逻辑——Jackson无法正确追踪代理集合内对象的已序列化状态,导致循环引用无法被终止,最终触发栈溢出。

另外,你的@JsonIdentityInfo配置存在错误:

  • Product实体的scope = Company.class配置错误,scope应该指向当前类(Product.class),用于区分不同类型对象的ID范围;同理Company实体的scope = Product.class也应改为Company.class。POJO类的scope也需对应修正(比如ProductPojo的scope应为ProductPojo.class),否则Jackson无法正确区分实体和POJO的对象身份,加剧递归问题。

解决步骤

  1. 修正@JsonIdentityInfo的scope配置
    每个类的scope都应指向自身:

    • Product实体:
      @JsonIdentityInfo(
              generator = ObjectIdGenerators.PropertyGenerator.class,
              property = "id", scope = Product.class)
      
    • Company实体:
      @JsonIdentityInfo(
              generator = ObjectIdGenerators.PropertyGenerator.class,
              property = "id", scope = Company.class)
      
    • ProductPojo:
      @JsonIdentityInfo(
              generator = ObjectIdGenerators.PropertyGenerator.class,
              property = "id", scope = ProductPojo.class)
      
    • CompanyPojo:
      @JsonIdentityInfo(
              generator = ObjectIdGenerators.PropertyGenerator.class,
              property = "id", scope = CompanyPojo.class)
      
  2. 处理Hibernate持久化集合
    有两种方式:

    • 手动转换:在实体转POJO时,将PersistentSet复制为普通HashSet,比如:
      productPojo.setSupplier(new HashSet<>(product.getSupplier()));
      
    • 使用Jackson Hibernate模块:引入jackson-datatype-hibernate5依赖,它能自动处理Hibernate的懒加载代理和持久化集合,正确识别对象身份,无需手动转换。
  3. 验证双向关联序列化
    修正后,Jackson会在首次序列化对象时输出完整数据,后续遇到同一对象时仅输出ID,既保留双向关联数据,又避免无限递归。

内容的提问来源于stack exchange,提问作者Tony B

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 00:05:24