返回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实体。
步骤:
- 创建
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; } }
- 修改
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); }
- 修改
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如何处理循环引用:
- 修改
Bookings实体(移除错误的@Transient注解):
// 替换原seatBookings字段的注解 @JsonManagedReference // 标记为主动序列化的一方 @OneToMany(mappedBy = "bookings", cascade = CascadeType.ALL) private List<SeatBookings> seatBookings;
- 修改
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
相关产品推荐
相关产品推荐

