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

Spring Boot中Jackson序列化循环引用问题求助(@JsonIgnoreProperties无效)

解决Jackson+Hibernate双向关联序列化循环引用问题

问题描述

使用Jackson序列化Hibernate实体时触发StackOverflowError,错误提示为Infinite recursion。实体间关联关系:Attendance关联单个Student和单个Lesson,Lesson关联多个Attendance,三者形成双向关联链。

报错日志

2023-05-28 19:39:09.548 DEBUG 15011 --- [nio-4800-exec-3] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
2023-05-28 19:39:09.549 ERROR 15011 --- [nio-4800-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: java.util.HashSet[0]->com.example.demo.domains.users.Student["attendance"]->org.hibernate.collection.internal.PersistentSet[0]->com.example.demo.domains.lessons.Attendance["lesson"]->com.example.demo.domains.lessons.PlainLesson["students"]->java.util.HashSet[0]->com.example.demo.domains.users.Student["attendance"]->org.hibernate.collection.internal.PersistentSet[0]->com.example.demo.domains.lessons.Attendance["lesson"]->com.example.demo.domains.lessons.PlainLesson["students"]->java.util.HashSet[0]->com.example.demo.domains.users.Student["attendance"]->org.hibernate.collection.internal.PersistentSet[0]->com.example.demo.domains.lessons.Attendance["lesson"]->com.example.demo.domains.lessons.PlainLesson["students"]->java.util.HashSet[0]->com.example.demo.domains.users.Student["attendance"]->org.hibernate.collection.internal.PersistentSet[0]->com.example.demo.domains.lessons.Attendance["lesson"]->com.example.demo.domains.lessons.PlainLesson["students"]->java.util.HashSet[0]->com.example.demo.domains.users.Student["attendance"]-> ... 
->com.example.demo.domains.lessons.PlainLesson["students"])] with root cause

java.lang.StackOverflowError: null
    at java.base/java.lang.ClassLoader.defineClass1(Native Method) ~[na:na]
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1013) ~[na:na]
    at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150) ~[na:na]
    at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:862) ~[na:na]
    at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:760) ~[na:na]
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:681) ~[na:na]
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:639) ~[na:na]
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) ~[na:na]
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521) ~[na:na]

已尝试方案

使用过@JsonIgnore、@JsonIgnoreProperties、@JsonBackReference、@JsonManagedReference、@JsonIdentityInfo注解,以及自定义序列化器/反序列化器,均未解决问题。

实体代码

Attendance

import com.example.demo.BaseEntity;
import com.example.demo.domains.users.Student;
import javax.persistence.*;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.*;


@Entity
@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Attendance extends BaseEntity {

    @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JsonIgnoreProperties(value = {"attendance"})
    Lesson lesson;

    @ManyToOne
    @JsonIgnoreProperties(value = {"lessons", "authorities", "roles", "events", "coachings", "competitionMember", "attendance", "student"})
    Student student;

    @OneToOne
    @JsonIgnore
    CanceledLesson canceledLesson;

    Boolean didAttend;
}

Lesson

package com.example.demo.domains.lessons;

import com.example.demo.domains.users.Student;
import com.example.demo.domains.users.Teacher;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import javax.persistence.*;
import lombok.*;
import lombok.experimental.SuperBuilder;
import org.springframework.format.annotation.DateTimeFormat;

import java.time.LocalDate;
import java.util.Set;
import java.util.stream.Collectors;


@Entity
@Setter
@Getter
@SuperBuilder
@NoArgsConstructor
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Lesson {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    Long id;
    @Enumerated(EnumType.STRING)
    @Column(name = "time", columnDefinition = "ENUM ('am0915', 'am1000', 'am1045', 'am1130', 'pm1205', 'pm0105', 'pm0150', 'pm0235', 'pm0320', 'pm0405', 'pm0450', 'pm0535', 'pm0620', 'pm0705', 'pm0750') not null", nullable = false)
    LessonTimes time;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd")
    public LocalDate date;

    @ManyToOne
    @JsonIgnoreProperties(value = {"lessons", "roles", "authorities", "events"})
    Teacher teacher;

    @OneToMany(mappedBy = "lesson")
    @JsonManagedReference
    @JsonIgnoreProperties(value = {"lesson"}, ignoreUnknown = true)
    @JsonIgnore
    private Set<Attendance> attendance;

    public Set<Student> getStudents() {
        return this.getAttendance().stream().map(Attendance::getStudent).collect(Collectors.toSet());
    }

    public void setStudents(Set<Student> studentSet) {
        var attendance = studentSet.stream().map(student -> Attendance.builder()
                .lesson(this)
                .student(student)
                .build()
        ).collect(Collectors.toSet());
        this.setAttendance(attendance);
    }
}

Student

package com.example.demo.domains.users;

import com.example.demo.domains.activities.coaching.Coaching;
import com.example.demo.domains.activities.competition.CompetitionMember;
import com.example.demo.domains.activities.event.Event;
import com.example.demo.domains.lessons.*;
import com.example.demo.security.user.JwtUser;
import com.example.demo.security.user.Position;
import com.example.demo.security.user.Role;
import com.fasterxml.jackson.annotation.*;
import javax.persistence.*;
import lombok.*;
import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.CreationTimestamp;

import java.sql.Timestamp;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

@Entity
@Table(name = "students")
@Getter
@Setter
@NoArgsConstructor
@SuperBuilder
@JsonIgnoreProperties(value = {"role", "canceledLessons", "authorities", "lessons", "events", "lessonCredits", "coachings", "competitionMember"}, allowGetters = true)
public class Student extends JwtUser {

    public Student(Long id) {
        super(id);
    }

    public Student(JwtUser jwtUser) {
        super(jwtUser);
    }

    {
        this.setRole(Set.of(Role.ROLE_USER));
        this.setPosition(Position.Student);
    }

    String phoneNumber;
    String telegram;
    String workAddress;
    String homeAddress;
    String danceLevel;

    @CreationTimestamp
    @Column(updatable = false)
    Timestamp registered;


    @OneToMany(mappedBy = "student")
    @JsonIgnoreProperties(value = {"student"})
    @JsonManagedReference
    Set<Attendance> attendance;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "student")
    private Set<NoteAboutStudent> noteAboutStudent;

    @ManyToMany
    @JsonIgnoreProperties(value = {"students"})
    @JoinTable(name = "students_coachings",
            joinColumns = @JoinColumn(name = "student_id"),
            inverseJoinColumns = @JoinColumn(name = "coaching_id"))
    Set<Coaching> coachings;

    @OneToMany(mappedBy = "student")
    @JsonIgnoreProperties(value = {"student"})
    Set<CompetitionMember> competitionMember;


    @ManyToMany
    @JoinTable(name = "students_events",
            joinColumns = @JoinColumn(name = "student_id"),
            inverseJoinColumns = @JoinColumn(name = "event_id"))
    Set<Event> events = new HashSet<>();

    @OneToMany(mappedBy = "student")
    @JsonIgnoreProperties(value = {"student"})
    Set<LessonCredit> lessonCredits;

    public void addCompetition(CompetitionMember cm) {
        if (this.competitionMember == null) {
            this.competitionMember = new HashSet<>();
        }
        cm.setStudent(this);
        this.competitionMember.add(cm);
    }

    public Set<Lesson> getLessons() {
        return this.getAttendance().stream().map(Attendance::getLesson).collect(Collectors.toSet());
    }
}

解决方案

核心问题定位

从报错引用链可以看出,循环递归路径为:
Student["attendance"] -> Attendance["lesson"] -> PlainLesson["students"] -> Student["attendance"]...
问题根源是自定义Getter方法被Jackson自动序列化:Lesson的getStudents()和Student的getLessons()是衍生属性(通过关联实体转换而来),Jackson会将其作为普通字段序列化,进而触发双向关联的无限循环。

具体修复步骤

  1. 忽略衍生属性的序列化
    给两个自定义Getter添加@JsonIgnore注解,阻止Jackson序列化这些衍生字段:

    // Lesson类中
    @JsonIgnore
    public Set<Student> getStudents() {
        return this.getAttendance().stream().map(Attendance::getStudent).collect(Collectors.toSet());
    }
    
    // Student类中
    @JsonIgnore
    public Set<Lesson> getLessons() {
        return this.getAttendance().stream().map(Attendance::getLesson).collect(Collectors.toSet());
    }
    
  2. 清理冗余注解
    移除实体中冲突的Jackson注解,比如Lesson的attendance字段同时使用了@JsonManagedReference、@JsonIgnoreProperties和@JsonIgnore,保留最直接的@JsonIgnore即可:

    @OneToMany(mappedBy = "lesson")
    @JsonIgnore
    private Set<Attendance> attendance;
    

替代方案(需保留衍生字段时)

如果业务需要在JSON中返回students或lessons信息,建议使用DTO(数据传输对象):

  • 创建LessonDTO、StudentDTO,仅包含需要对外暴露的字段(如ID、名称等),避免关联实体的循环引用;
  • 在Controller层将Hibernate实体转换为DTO后再返回。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 10:22:04