Spring Boot中Jackson注解处理自引用@ManyToMany关系失效,序列化出现无限递归
兄弟,我看了你遇到的问题——自引用@ManyToMany的User实体在Jackson序列化时陷入无限递归,试了好几个注解都没达到预期效果。我来给你拆解下问题根源,再分享几个能完美实现你需求的解决方案。
先说说你当前注解为啥没生效
你用的这几个Jackson注解,在自关联双向ManyToMany的场景下确实不太适配:
- @JsonManagedReference + @JsonBackReference:这组注解本来是为一对多的父子关系设计的——父对象用
@JsonManagedReference(会被序列化),子对象用@JsonBackReference(会被完全忽略)。但你的场景是自关联,following加了@JsonManagedReference、followers加了@JsonBackReference后,followers字段会被直接隐藏,不符合你要显示followers列表的需求;而且following里的User对象,它的following又会回引原对象,还是会触发递归。 - @JsonIdentityInfo:这个注解是通过重复对象只输出id来避免递归,但它的效果是第二次出现的对象只显示
id值,而不是你想要的“显示简化版User对象(id+username)”,所以也不符合你的预期输出格式。
推荐的解决方案(按优先级排序)
方案1:用DTO(数据传输对象)分离实体与响应结构(最推荐)
这是Spring开发的最佳实践之一——数据库实体只负责和数据库交互,专门写DTO来控制前端返回的字段,完全避免递归问题,还能防止敏感字段(比如你的password)泄露。
步骤如下:
- 创建两个DTO类:一个完整的用户DTO,一个仅包含核心字段的极简用户DTO
import lombok.AllArgsConstructor; import lombok.Data; import java.util.List; import java.util.UUID; import com.pubfinder.pubfinder.models.enums.Role; // 完整用户DTO,对应你要返回的主用户信息 @Data @AllArgsConstructor public class UserDTO { private UUID id; private String username; private String firstname; private String lastname; private String email; private Role role; // 如果不需要返回角色可以删掉 private List<UserMinimalDTO> following; private List<UserMinimalDTO> followers; // 极简用户DTO,仅包含following/followers里需要显示的字段 @Data @AllArgsConstructor public static class UserMinimalDTO { private UUID id; private String username; } }
- 在Service层把User实体转换为DTO
import org.springframework.stereotype.Service; import java.util.stream.Collectors; import java.util.UUID; @Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public UserDTO getUserWithFollowInfo(UUID userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new RuntimeException("用户不存在")); // 初始化懒加载集合,避免Jackson序列化时触发懒加载异常 user.getFollowing().size(); user.getFollowers().size(); // 转换following列表为极简DTO List<UserDTO.UserMinimalDTO> followingDTOs = user.getFollowing().stream() .map(u -> new UserDTO.UserMinimalDTO(u.getId(), u.getUsername())) .collect(Collectors.toList()); // 转换followers列表为极简DTO List<UserDTO.UserMinimalDTO> followersDTOs = user.getFollowers().stream() .map(u -> new UserDTO.UserMinimalDTO(u.getId(), u.getUsername())) .collect(Collectors.toList()); // 构建并返回完整DTO return new UserDTO( user.getId(), user.getUsername(), user.getFirstname(), user.getLastname(), user.getEmail(), user.getRole(), followingDTOs, followersDTOs ); } }
- Controller层返回DTO
import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; @RestController @RequestMapping("/users") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping("/{id}") public ResponseEntity<UserDTO> getUser(@PathVariable UUID id) { UserDTO userDTO = userService.getUserWithFollowInfo(id); return ResponseEntity.ok(userDTO); } }
这个方案的优势:完全解耦数据库实体和前端响应,你可以自由控制返回的字段,不会有递归问题,也能避免敏感数据泄露,是最规范的做法。
方案2:自定义Jackson序列化器
如果你不想写DTO,可以给following和followers集合指定自定义序列化器,让Jackson序列化集合中的User对象时只输出你需要的字段。
- 编写自定义序列化器
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.pubfinder.pubfinder.models.User; import java.io.IOException; public class UserMinimalSerializer extends StdSerializer<User> { // 无参构造是必须的 public UserMinimalSerializer() { this(null); } public UserMinimalSerializer(Class<User> t) { super(t); } @Override public void serialize(User user, JsonGenerator gen, SerializerProvider provider) throws IOException { gen.writeStartObject(); gen.writeStringField("id", user.getId().toString()); gen.writeStringField("username", user.getUsername()); // 如果需要加其他字段比如firstname,直接在这里添加 gen.writeEndObject(); } }
- 在User实体的集合字段上添加
@JsonSerialize注解
// ... 其他字段保持不变 @ManyToMany( fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH} ) @JoinTable( name = "user_following", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "following_id") ) @Builder.Default @JsonSerialize(contentUsing = UserMinimalSerializer.class) // 指定集合元素的序列化器 Set<User> following = new HashSet<>(); @ManyToMany( mappedBy = "following", fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH} ) @Builder.Default @JsonSerialize(contentUsing = UserMinimalSerializer.class) // 同样给followers加 Set<User> followers = new HashSet<>();
这样序列化时,following和followers里的每个User都会被序列化器处理成仅包含id和username的对象,不会递归嵌套。
方案3:使用@JsonView控制序列化视图
这个方案是通过定义不同的序列化视图,让主User对象显示完整字段,而关联集合中的User只显示基础字段。
- 定义视图接口
public class UserViews { // 基础视图:仅包含id、username等核心字段 public interface Basic {} // 完整视图:继承Basic,包含更多字段 public interface Full extends Basic {} }
- 在User实体的字段上标注对应的视图
@Id @GeneratedValue @Column(unique = true, nullable = false) @JsonView(UserViews.Basic.class) private UUID id; @Column(unique = true, nullable = false) @JsonView(UserViews.Basic.class) private String username; @JsonView(UserViews.Full.class) private String firstname; @JsonView(UserViews.Full.class) private String lastname; @Column(unique = true, nullable = false) @JsonView(UserViews.Full.class) private String email; // password字段不要加任何视图,避免返回给前端 private String password; @Column(nullable = false) @Enumerated(EnumType.ORDINAL) @JsonView(UserViews.Full.class) private Role role; @ManyToMany(...) @Builder.Default @JsonView(UserViews.Full.class) // 指定集合元素用Basic视图序列化 @JsonSerialize(contentView = UserViews.Basic.class) Set<User> following = new HashSet<>(); @ManyToMany(...) @Builder.Default @JsonView(UserViews.Full.class) @JsonSerialize(contentView = UserViews.Basic.class) Set<User> followers = new HashSet<>();
- 在Controller方法上指定返回的视图
@GetMapping("/{id}") @JsonView(UserViews.Full.class) public ResponseEntity<User> getUser(@PathVariable UUID id) { User user = userRepository.findById(id).orElseThrow(() -> new RuntimeException("用户不存在")); // 初始化懒加载集合 user.getFollowing().size(); user.getFollowers().size(); return ResponseEntity.ok(user); }
这样主User对象会序列化Full视图的所有字段,而following和followers里的每个User只会序列化Basic视图的字段,避免递归。
总结
如果追求规范和灵活性,优先选DTO方案;如果不想改动太多代码,可以用自定义序列化器或者@JsonView。另外要注意,懒加载的集合在序列化前需要初始化,否则会抛出懒加载异常(可以在Repository里用fetch join查询,或者在Service层调用集合的size()方法触发初始化)。
备注:内容来源于stack exchange,提问作者Limpan

