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

Spring Boot中Jackson注解处理自引用@ManyToMany关系失效,序列化出现无限递归

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)泄露。

步骤如下:

  1. 创建两个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;
    }
}
  1. 在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
        );
    }
}
  1. 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对象时只输出你需要的字段。

  1. 编写自定义序列化器
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();
    }
}
  1. 在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只显示基础字段。

  1. 定义视图接口
public class UserViews {
    // 基础视图:仅包含id、username等核心字段
    public interface Basic {}
    // 完整视图:继承Basic,包含更多字段
    public interface Full extends Basic {}
}
  1. 在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<>();
  1. 在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 17:13:07