0

0

Spring JPA多对多关系中Jackson无限递归问题的解决方案

霞舞

霞舞

发布时间:2025-11-09 19:23:01

|

761人浏览过

|

来源于php中文网

原创

spring jpa多对多关系中jackson无限递归问题的解决方案

本文旨在解决Spring JPA实体间双向多对多关系在Jackson序列化时导致的无限递归(StackOverflowError)问题。我们将详细介绍如何利用Jackson的`@JsonManagedReference`和`@JsonBackReference`注解来管理对象图的序列化,并结合Lombok的`@EqualsAndHashCode`和`@ToString`注解进一步优化实体行为,确保在数据获取和序列化过程中避免循环引用,从而生成结构清晰、可读性强的JSON数据。

理解无限递归问题

在使用Spring Data JPA构建实体关系时,特别是双向的@ManyToMany关联,当尝试将这些实体通过Jackson库序列化为JSON时,很容易遇到“无限递归”(Infinite Recursion)错误,表现为StackOverflowError。

问题根源: 以Project和Technology为例,它们之间存在一个双向的@ManyToMany关系:

  • Project实体中有一个Set assignedTechnologies。
  • Technology实体中有一个Set projects。

当Jackson尝试序列化一个Project对象时,它会遍历assignedTechnologies集合,序列化其中的每个Technology对象。在序列化Technology对象时,Jackson又会发现其内部的projects集合,并尝试序列化其中的Project对象,如此往复,形成一个无限循环,最终导致溢出。

// 示例:Project实体片段
public class Project {
    // ...
    @ManyToMany(mappedBy = "projects")
    private Set assignedTechnologies = new HashSet<>();
}

// 示例:Technology实体片段
public class Technology {
    // ...
    @ManyToMany
    @JoinTable(
            name = "projects_technologies",
            joinColumns = @JoinColumn(name="technology_id"),
            inverseJoinColumns = @JoinColumn(name="project_id")
    )
    private Set projects = new HashSet<>();
}

当执行projectRepository.findAll()并尝试将其返回给API时,Jackson会触发上述循环,产生如下错误:

Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: java.util.ArrayList[0]->com.example.technologyradar.model.Project["assignedTechnologies"])]

核心解决方案:Jackson注解

解决Jackson序列化无限递归问题的最常用且推荐的方法是使用@JsonManagedReference和@JsonBackReference注解。这两个注解用于标记关系的两端,告诉Jackson在序列化时如何处理循环引用。

  • @JsonManagedReference (管理端): 标记关系中“拥有”或“管理”另一端引用的字段。当序列化时,此字段会被正常序列化。
  • @JsonBackReference (回溯端): 标记关系中“被引用”的字段。当序列化时,此字段会被忽略,从而打破循环。

应用示例: 在Project和Technology的@ManyToMany关系中,我们可以选择其中一端作为管理端,另一端作为回溯端。通常,我们会选择在序列化时希望看到完整信息的实体作为管理端。

修改后的Project实体:

package com.example.technologyradar.model;

import com.fasterxml.jackson.annotation.JsonManagedReference; // 引入Jackson注解
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Project {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "native")
    @GenericGenerator(name="native", strategy = "native")
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "projects")
    @JsonManagedReference // 标记为管理端,正常序列化Technology集合
    private Set assignedTechnologies = new HashSet<>();
}

修改后的Technology实体:

package com.example.technologyradar.model;

import com.example.technologyradar.dto.constant.TechnologyStatus;
import com.fasterxml.jackson.annotation.JsonBackReference; // 引入Jackson注解
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Technology {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "native")
    @GenericGenerator(name="native", strategy = "native")
    private Long id;

    private String name;

    @Enumerated(EnumType.STRING)
    private TechnologyStatus technologyStatus;

    @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST, targetEntity = Category.class)
    @JoinColumn(name="category_id", referencedColumnName = "id", nullable = false)
    private Category category;

    @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST, targetEntity = Coordinate.class)
    @JoinColumn(name="coordinate_id", referencedColumnName = "id", nullable = false)
    private Coordinate coordinate;

    @ManyToMany
    @JoinTable(
            name = "projects_technologies",
            joinColumns = @JoinColumn(name="technology_id"),
            inverseJoinColumns = @JoinColumn(name="project_id")
    )
    @JsonBackReference // 标记为回溯端,在序列化Technology时忽略Project集合
    private Set projects = new HashSet<>();

}

通过上述修改,当序列化Project时,其assignedTechnologies会被完全序列化。当序列化assignedTechnologies中的Technology对象时,Technology内部的projects字段由于带有@JsonBackReference注解,将被Jackson忽略,从而有效避免了无限递归。

辅助优化:Lombok注解

除了Jackson的序列化注解,Lombok的@Data注解自动生成的equals()、hashCode()和toString()方法也可能在某些情况下(例如调试、日志输出或集合操作)导致类似的循环引用问题,即使不涉及JSON序列化。为了避免这种情况,我们可以对@Data注解进行精细控制。

光速写作
光速写作

AI打工神器,一键生成文章&PPT

下载

问题分析:@Data注解默认会为所有非静态字段生成equals()、hashCode()和toString()方法。如果这些方法在执行时递归地访问关联实体,同样会造成StackOverflowError。

解决方案: 使用@EqualsAndHashCode(of = "id")和@ToString(of = "id")注解,将equals()、hashCode()和toString()方法的生成范围限制在实体的ID字段上。这样,这些方法在比较或打印对象时将不会遍历其关联集合,从而避免潜在的递归。

修改后的Project实体(包含Lombok优化):

package com.example.technologyradar.model;

import com.fasterxml.jackson.annotation.JsonManagedReference;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode; // 引入Lombok注解
import lombok.NoArgsConstructor;
import lombok.ToString; // 引入Lombok注解
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(of = "id") // 只基于id生成equals和hashCode
@ToString(of = {"id", "name"}) // 只打印id和name字段
public class Project {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "native")
    @GenericGenerator(name="native", strategy = "native")
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "projects")
    @JsonManagedReference
    private Set assignedTechnologies = new HashSet<>();
}

修改后的Technology实体(包含Lombok优化):

package com.example.technologyradar.model;

import com.example.technologyradar.dto.constant.TechnologyStatus;
import com.fasterxml.jackson.annotation.JsonBackReference;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode; // 引入Lombok注解
import lombok.NoArgsConstructor;
import lombok.ToString; // 引入Lombok注解
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(of = "id") // 只基于id生成equals和hashCode
@ToString(of = {"id", "name", "technologyStatus"}) // 只打印id, name, technologyStatus字段
public class Technology {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "native")
    @GenericGenerator(name="native", strategy = "native")
    private Long id;

    private String name;

    @Enumerated(EnumType.STRING)
    private TechnologyStatus technologyStatus;

    @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST, targetEntity = Category.class)
    @JoinColumn(name="category_id", referencedColumnName = "id", nullable = false)
    private Category category;

    @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST, targetEntity = Coordinate.class)
    @JoinColumn(name="coordinate_id", referencedColumnName = "id", nullable = false)
    private Coordinate coordinate;

    @ManyToMany
    @JoinTable(
            name = "projects_technologies",
            joinColumns = @JoinColumn(name="technology_id"),
            inverseJoinColumns = @JoinColumn(name="project_id")
    )
    @JsonBackReference
    private Set projects = new HashSet<>();

}

通过这些Lombok注解的细化,即使在非序列化场景下,实体对象的行为也更加安全和可控。

其他处理策略

除了上述方法,还有几种策略可以处理JPA实体和JSON序列化:

  1. 使用@JsonIgnore: 最简单粗暴的方法是在关系的一端直接使用@JsonIgnore注解。这会完全阻止该字段的序列化。

    // 在Technology实体中
    @ManyToMany
    @JsonIgnore // 完全忽略projects字段的序列化
    private Set projects = new HashSet<>();

    优点: 简单快捷。 缺点: 可能会丢失部分需要在某些场景下序列化的数据。如果某个API确实需要Technology关联的Project信息,此方法就不适用。

  2. 数据传输对象(DTO): 将JPA实体与API响应解耦的最佳实践是使用DTO。DTO是专门为API响应设计的POJO,只包含客户端所需的数据,不包含JPA注解和复杂的关联关系。

    • 优点:
      • 完全控制API响应结构,与数据库实体分离。
      • 避免暴露不必要的内部实体细节。
      • 易于测试和维护。
    • 实现: 创建一个ProjectDTO和TechnologyDTO,并通过手动映射或使用ModelMapper、MapStruct等库将实体转换为DTO。
      // ProjectDTO 示例
      public class ProjectDTO {
          private Long id;
          private String name;
          private Set assignedTechnologies; // 包含简化版的Technology信息
          // 构造函数、getter/setter
      }
      // TechnologyDTO 示例
      public class TechnologyDTO {
          private Long id;
          private String name;
          private TechnologyStatus technologyStatus;
          // 不包含projects集合,或者只包含Project的ID/名称
          // 构造函数、getter/setter
      }

      在Service层将查询到的实体转换为DTO列表再返回。

  3. @JsonIdentityInfo: 当需要序列化整个对象图,并且希望Jackson能够识别并处理循环引用时,可以使用@JsonIdentityInfo。它会为每个对象生成一个唯一的标识符,并在遇到重复引用时只序列化该标识符。

    @Entity
    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
    public class Project {
        // ...
    }
    @Entity
    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
    public class Technology {
        // ...
    }

    优点: 序列化时保留了所有关联信息,且不会无限递归。 缺点: 生成的JSON可能会包含额外的@id字段,并且结构可能不如DTO直观。

总结

解决Spring JPA实体在Jackson序列化时遇到的无限递归问题,关键在于管理好双向关联的序列化行为。@JsonManagedReference和@JsonBackReference是处理这类问题的首选方案,它们提供了一种清晰且可控的方式来打破循环引用。同时,结合Lombok的@EqualsAndHashCode(of = "id")和@ToString(of = "id")注解,可以进一步增强实体在非序列化场景下的健壮性。对于更复杂的业务场景,采用数据传输对象(DTO)模式则是将API响应与持久层实体解耦的最佳实践,它能提供最大的灵活性和可维护性。根据具体的业务需求和对JSON结构的要求,选择最合适的策略至关重要。

相关专题

更多
spring框架介绍
spring框架介绍

本专题整合了spring框架相关内容,想了解更多详细内容,请阅读专题下面的文章。

98

2025.08.06

json数据格式
json数据格式

JSON是一种轻量级的数据交换格式。本专题为大家带来json数据格式相关文章,帮助大家解决问题。

402

2023.08.07

json是什么
json是什么

JSON是一种轻量级的数据交换格式,具有简洁、易读、跨平台和语言的特点,JSON数据是通过键值对的方式进行组织,其中键是字符串,值可以是字符串、数值、布尔值、数组、对象或者null,在Web开发、数据交换和配置文件等方面得到广泛应用。本专题为大家提供json相关的文章、下载、课程内容,供大家免费下载体验。

528

2023.08.23

jquery怎么操作json
jquery怎么操作json

操作的方法有:1、“$.parseJSON(jsonString)”2、“$.getJSON(url, data, success)”;3、“$.each(obj, callback)”;4、“$.ajax()”。更多jquery怎么操作json的详细内容,可以访问本专题下面的文章。

306

2023.10.13

go语言处理json数据方法
go语言处理json数据方法

本专题整合了go语言中处理json数据方法,阅读专题下面的文章了解更多详细内容。

74

2025.09.10

mysql标识符无效错误怎么解决
mysql标识符无效错误怎么解决

mysql标识符无效错误的解决办法:1、检查标识符是否被其他表或数据库使用;2、检查标识符是否包含特殊字符;3、使用引号包裹标识符;4、使用反引号包裹标识符;5、检查MySQL的配置文件等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

175

2023.12.04

Python标识符有哪些
Python标识符有哪些

Python标识符有变量标识符、函数标识符、类标识符、模块标识符、下划线开头的标识符、双下划线开头、双下划线结尾的标识符、整型标识符、浮点型标识符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

270

2024.02.23

java标识符合集
java标识符合集

本专题整合了java标识符相关内容,想了解更多详细内容,请阅读下面的文章。

250

2025.06.11

俄罗斯搜索引擎Yandex最新官方入口网址
俄罗斯搜索引擎Yandex最新官方入口网址

Yandex官方入口网址是https://yandex.com;用户可通过网页端直连或移动端浏览器直接访问,无需登录即可使用搜索、图片、新闻、地图等全部基础功能,并支持多语种检索与静态资源精准筛选。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

1

2025.12.29

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.1万人学习

C# 教程
C# 教程

共94课时 | 5.6万人学习

Java 教程
Java 教程

共578课时 | 39.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号