解决实体递归关联引发的StackOverflow异常问题
解决Jersey/Yasson中双向关联的递归序列化问题
你遇到的这个双向关联导致StackOverflow的问题很常见,其实不用非得拆分请求,Yasson本身就有几种实用方案,完美匹配你的需求:既能在展示Tweet时让关联的User不携带tweets列表(避免递归),又能在单独展示User时包含其tweets列表。
方案一:用@JsonbManagedReference和@JsonbBackReference自动处理递归
这是处理双向关联序列化的标准方式,专门用来解决循环引用问题,完全贴合你的场景:
修改你的User类,在tweets属性上添加@JsonbManagedReference:
public class User { ... @JsonbManagedReference @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinColumn(name = "user_id") private List<Tweet> tweets; ... }
然后修改Tweet类,在user属性上添加@JsonbBackReference:
public class Tweet { ... @JsonbBackReference @ManyToOne private User user; ... }
原理说明:
@JsonbManagedReference标记的是双向关联中的父端(这里User是Tweet的父),序列化User时会正常包含关联的tweets列表;@JsonbBackReference标记的是子端,序列化Tweet时,会自动忽略关联User中的tweets属性,彻底避免循环递归。
这样就刚好满足你的两个需求:
- 当你获取所有Tweet时,每个Tweet里的User不会携带tweets列表,不会触发StackOverflow;
- 当你单独获取User时,会完整包含其tweets列表,方便你统计数量或展示其他相关信息。
方案二:用JsonbView实现更灵活的序列化控制
如果之后你需要更细粒度的控制(比如某些场景下想让Tweet里的User携带部分属性,或者某些User需要展示tweets而另一些不需要),可以用Yasson的视图功能:
第一步:定义视图类
先创建几个空类作为视图标记:
public class Views { // 基础视图:只展示User的核心属性 public static class Public {} // 扩展视图:在基础视图上加上tweets列表 public static class WithTweets extends Public {} }
第二步:给实体类的属性标记视图
修改User类,给tweets属性指定WithTweets视图,其他属性默认属于Public视图:
public class User { // 核心属性默认属于Public视图 private Long id; private String username; private String email; @JsonbView(Views.WithTweets.class) @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinColumn(name = "user_id") private List<Tweet> tweets; ... }
修改Tweet类,给user属性指定Public视图:
public class Tweet { private Long id; private String content; @JsonbView(Views.Public.class) @ManyToOne private User user; ... }
第三步:在Jersey资源方法中指定序列化视图
在不同的接口里,你可以指定使用哪个视图来序列化:
- 获取带tweets列表的User:
@GET @Path("/users/{id}") @Produces(MediaType.APPLICATION_JSON) public Response getUserWithTweets(@PathParam("id") Long id) { User user = userRepository.findById(id); // 创建Jsonb实例时指定使用WithTweets视图 Jsonb jsonb = JsonbBuilder.create(new JsonbConfig().withView(Views.WithTweets.class)); String json = jsonb.toJson(user); return Response.ok(json).build(); }
- 获取所有Tweet(关联的User不带tweets):
@GET @Path("/tweets") @Produces(MediaType.APPLICATION_JSON) public Response getAllTweets() { List<Tweet> tweets = tweetRepository.findAll(); // 使用Public视图,User的tweets不会被序列化 Jsonb jsonb = JsonbBuilder.create(new JsonbConfig().withView(Views.Public.class)); String json = jsonb.toJson(tweets); return Response.ok(json).build(); }
这种方式的优势是灵活性极高,你可以根据业务需求定义更多视图,完全控制每个场景下要序列化的属性。
总结
如果只是解决当前的双向关联递归问题,方案一完全够用,代码改动最小;如果之后有更复杂的序列化需求,方案二能给你足够的扩展空间。不用拆分请求就能实现你想要的效果~
内容的提问来源于stack exchange,提问作者Maikkeyy
相关产品推荐
相关产品推荐

