Spring Boot+Hibernate多对多关联返回JSON时触发StackOverflowException
解决Spring Boot+Hibernate多对多关联JSON序列化循环引用及完整双向数据返回问题
方案1:使用@JsonIdentityInfo识别对象身份
这个注解让Jackson在序列化时识别对象的唯一ID,遇到循环引用时会用ID代替重复的对象实例,双向关联的字段都能被正常序列化。
在GiftCertificate和Tag实体类上都添加该注解:
import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.ObjectIdGenerators; @Entity @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class GiftCertificate { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @ManyToMany(mappedBy = "giftCertificates") private Set<Tag> tags = new HashSet<>(); // getter、setter、构造方法 }
@Entity @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class Tag { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @ManyToMany @JoinTable( name = "gift_certificate_tag", joinColumns = @JoinColumn(name = "tag_id"), inverseJoinColumns = @JoinColumn(name = "gift_certificate_id") ) private Set<GiftCertificate> giftCertificates = new HashSet<>(); // getter、setter、构造方法 }
特点:实现简单,无需额外类;但序列化后的JSON中,重复出现的对象会被替换为ID,适合能接受这种格式的场景。
方案2:使用DTO(数据传输对象)解耦实体与返回结构
这是更规范的做法,通过定义专门的DTO类控制返回的JSON结构,彻底避免循环引用,同时灵活定制返回字段。
首先创建DTO类:
public class GiftCertificateDTO { private Long id; private String name; private Set<TagDTO> tags; // 构造方法、getter、setter } public class TagDTO { private Long id; private String name; private Set<GiftCertificateDTO> giftCertificates; // 构造方法、getter、setter }
然后在业务层或控制器中完成实体到DTO的转换:
// 示例转换方法 public GiftCertificateDTO convertToDTO(GiftCertificate certificate) { GiftCertificateDTO dto = new GiftCertificateDTO(); dto.setId(certificate.getId()); dto.setName(certificate.getName()); dto.setTags(certificate.getTags().stream() .map(tag -> { TagDTO tagDTO = new TagDTO(); tagDTO.setId(tag.getId()); tagDTO.setName(tag.getName()); // 按需处理关联字段,避免无限循环 tagDTO.setGiftCertificates(tag.getGiftCertificates().stream() .map(cert -> { GiftCertificateDTO certDTO = new GiftCertificateDTO(); certDTO.setId(cert.getId()); certDTO.setName(cert.getName()); return certDTO; }).collect(Collectors.toSet())); return tagDTO; }).collect(Collectors.toSet())); return dto; }
在控制器中返回DTO:
@GetMapping("/certificates/{id}") public ResponseEntity<GiftCertificateDTO> getCertificate(@PathVariable Long id) { GiftCertificate certificate = certificateRepository.findById(id).orElseThrow(); return ResponseEntity.ok(convertToDTO(certificate)); }
特点:完全解耦数据库实体与API返回结构,灵活度高;需要额外编写DTO和转换代码,适合复杂API场景。
方案3:使用@JsonIgnoreProperties忽略对方的关联字段
在双向关联的字段上指定忽略对方的关联属性,Jackson序列化时不会递归处理对方的关联集合,既避免循环引用,又保留自身的关联字段。
修改实体类:
@Entity public class GiftCertificate { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @ManyToMany(mappedBy = "giftCertificates") @JsonIgnoreProperties("giftCertificates") // 忽略Tag中的giftCertificates字段 private Set<Tag> tags = new HashSet<>(); // getter、setter、构造方法 }
@Entity public class Tag { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @ManyToMany @JoinTable( name = "gift_certificate_tag", joinColumns = @JoinColumn(name = "tag_id"), inverseJoinColumns = @JoinColumn(name = "gift_certificate_id") ) @JsonIgnoreProperties("tags") // 忽略GiftCertificate中的tags字段 private Set<GiftCertificate> giftCertificates = new HashSet<>(); // getter、setter、构造方法 }
特点:实现简单,无需额外代码;序列化后的JSON会包含双方的关联字段,但不会递归深入,适合需要完整双向关联字段但不需要无限递归的场景。
内容的提问来源于stack exchange,提问作者Redoge
相关产品推荐
相关产品推荐

