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

返回JPA实体触发无限递归StackOverflowError问题排查与解决

问题原因分析

返回JPA实体时出现Infinite recursion - Stackoverflow error,核心原因是实体间的双向循环引用导致Jackson序列化死循环:

  • Bookings实体包含seatBookings列表(@OneToMany关联SeatBookings)
  • SeatBookings实体又通过bookings字段引用回Bookings(@ManyToOne关联)
    当Spring尝试将Bookings序列化为JSON时,Jackson会遍历seatBookings,每个SeatBookings又会触发bookings的序列化,如此往复直到栈内存耗尽。

返回ResponseEntity<String>时不会触发实体的序列化逻辑,因此不会出现循环问题。

解决方案

下面提供三种可行的解决方法,按推荐程度排序:

方法1:使用DTO(推荐的最佳实践)

定义专门的DTO类封装响应数据,只暴露前端需要的字段,避免直接返回JPA实体。

步骤:

  1. 创建BookingResponseDTO类:
package com.bookmyshow.consumer.api.dto;

import lombok.Getter;
import lombok.Setter;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

@Getter
@Setter
public class BookingResponseDTO {
    private Long bookingId;
    private Long showId;
    private Long userId;
    private List<SeatBookingDTO> seatBookings;
    private LocalDate bookingDate;
    private int amount;
    private LocalDateTime created;

    // 内部类封装座位预订信息
    @Getter
    @Setter
    public static class SeatBookingDTO {
        private Long seatBookingId;
        private String seatNumber;
        private Long showId;
    }
}
  1. 修改BookingService,将Bookings实体转换为DTO:
// 在BookingService中添加转换方法
private BookingResponseDTO convertToDTO(Bookings bookings) {
    BookingResponseDTO dto = new BookingResponseDTO();
    dto.setBookingId(bookings.getBookingId());
    dto.setShowId(bookings.getShow().getShowId());
    dto.setUserId(bookings.getUser().getUserId());
    dto.setBookingDate(bookings.getBookingDate());
    dto.setAmount(bookings.getAmount());
    dto.setCreated(bookings.getCreated());

    List<BookingResponseDTO.SeatBookingDTO> seatDTOs = bookings.getSeatBookings().stream()
            .map(seatBooking -> {
                BookingResponseDTO.SeatBookingDTO seatDTO = new BookingResponseDTO.SeatBookingDTO();
                seatDTO.setSeatBookingId(seatBooking.getSeatBookingId());
                seatDTO.setSeatNumber(seatBooking.getTheatreSeat().getSeatNo());
                seatDTO.setShowId(seatBooking.getShow().getShowId());
                return seatDTO;
            })
            .toList();
    dto.setSeatBookings(seatDTOs);
    return dto;
}

// 修改bookSeats方法的返回类型为BookingResponseDTO
public BookingResponseDTO bookSeats(BookingDetails bookingDetails) {
    // 原业务逻辑不变
    Show show = showService.findShowByShowId(bookingDetails.getShowId());
    Customer user = userService.findByUserId(bookingDetails.getUserId());
    List<TheatreSeat> theatreSeats = bookingDetails.getSeatNumbers().stream().map(theatreSeatService::findBySeatNo).toList();
    List<SeatBookings> seatBookings = theatreSeats.stream().map(seat -> new SeatBookings(seat, show)).toList();
    Bookings bookings = new Bookings(show, user, seatBookings);
    bookingRepository.save(bookings);
    for (SeatBookings seatBooking : seatBookings) {
        seatBooking.setBookings(bookings);
    }
    seatBookingsRepository.saveAll(seatBookings);
    
    return convertToDTO(bookings);
}
  1. 修改BookingController返回DTO:
@RequestMapping(path = "/book-seats", method = RequestMethod.POST)
public ResponseEntity<BookingResponseDTO> bookSeats(@RequestBody BookingDetails bookingDetails) {
    BookingResponseDTO response = bookingService.bookSeats(bookingDetails);
    return ResponseEntity.status(201).body(response);
}

方法2:使用Jackson双向引用注解

通过@JsonManagedReference和@JsonBackReference告诉Jackson如何处理循环引用:

  1. 修改Bookings实体(移除错误的@Transient注解):
// 替换原seatBookings字段的注解
@JsonManagedReference // 标记为主动序列化的一方
@OneToMany(mappedBy = "bookings", cascade = CascadeType.ALL)
private List<SeatBookings> seatBookings;
  1. 修改SeatBookings实体:
// 替换原bookings字段的注解
@JsonBackReference // 标记为被动忽略序列化的一方
@ManyToOne
@JoinColumn(name = "bookingid")
private Bookings bookings;

方法3:使用@JsonIgnore忽略循环字段

直接在循环引用的一方添加@JsonIgnore,让Jackson序列化时跳过该字段:

修改SeatBookings实体:

@JsonIgnore // 序列化时忽略该字段,避免循环
@ManyToOne
@JoinColumn(name = "bookingid")
private Bookings bookings;

这种方法最简单,但会导致SeatBookings序列化时丢失bookings信息,如果前端不需要这个字段可以使用。

额外修复点

你的Bookings实体中seatBookings字段错误地添加了@Transient注解,这个注解会让JPA忽略该字段,导致关联的SeatBookings数据无法被正确持久化,必须移除。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 20:05:57