Spring Boot中Jackson序列化循环引用问题求助(@JsonIgnoreProperties无效)
问题描述
使用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会将其作为普通字段序列化,进而触发双向关联的无限循环。
具体修复步骤
忽略衍生属性的序列化
给两个自定义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()); }清理冗余注解
移除实体中冲突的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

