Jackson – 双向关系
最后更新:2025 年 11 月 12 日
1. 概述
在本教程中,我们将探讨处理 Jackson 中的双向关系的最佳方法。
首先,我们将讨论 Jackson JSON 无限递归问题。然后我们将了解如何序列化具有双向关系的实体。最后,我们将反序列化它们。
2. 无限递归
让我们看一下 Jackson 无限递归问题。在下面的示例中,我们有两个实体,“User”和“Item”,具有 一个简单的多对一关系
“User”实体
public class User {
public int id;
public String name;
public List<Item> userItems;
}
“Item”实体
public class Item {
public int id;
public String itemName;
public User owner;
}
当我们尝试序列化“Item”的实例时,Jackson 会抛出一个 JsonMappingException 异常
@Test(expected = JsonMappingException.class)
public void givenBidirectionRelation_whenSerializing_thenException()
throws JsonProcessingException {
User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);
new ObjectMapper().writeValueAsString(item);
}
完整的异常是
com.fasterxml.jackson.databind.JsonMappingException:
Infinite recursion (StackOverflowError)
(through reference chain:
org.baeldung.jackson.bidirection.Item["owner"]
->org.baeldung.jackson.bidirection.User["userItems"]
->java.util.ArrayList[0]
->org.baeldung.jackson.bidirection.Item["owner"]
->…..
我们将在接下来的几个部分中了解如何解决此问题。
3. 使用 @JsonManagedReference, @JsonBackReference
首先,让我们使用 @JsonManagedReference 和 @JsonBackReference 注解关系,以便 Jackson 更好地处理关系
这是“User”实体
public class User {
public int id;
public String name;
@JsonManagedReference
public List<Item> userItems;
}
还有“Item”
public class Item {
public int id;
public String itemName;
@JsonBackReference
public User owner;
}
现在让我们测试新的实体
@Test
public void givenBidirectionRelation_whenUsingJacksonReferenceAnnotationWithSerialization_thenCorrect() throws JsonProcessingException {
final User user = new User(1, "John");
final Item item = new Item(2, "book", user);
user.addItem(item);
final String itemJson = new ObjectMapper().writeValueAsString(item);
final String userJson = new ObjectMapper().writeValueAsString(user);
assertThat(itemJson, containsString("book"));
assertThat(itemJson, not(containsString("John")));
assertThat(userJson, containsString("John"));
assertThat(userJson, containsString("userItems"));
assertThat(userJson, containsString("book"));
}
这是序列化 Item 对象后的输出
{
"id":2,
"itemName":"book"
}
这是序列化 User 对象后的输出
{
"id":1,
"name":"John",
"userItems":[{
"id":2,
"itemName":"book"}]
}
请注意
- @JsonManagedReference 是引用的前向部分,它会正常序列化。
- @JsonBackReference 是引用的后向部分;它将从序列化中省略。
- 序列化的 Item 对象不包含对 User 对象的引用。
还需要注意的是,我们不能交换注释。以下对于序列化有效
@JsonBackReference
public List<Item> userItems;
@JsonManagedReference
public User owner;
但是,当我们尝试反序列化对象时,它会抛出一个异常,因为 @JsonBackReference 不能用于集合。.
如果我们要让序列化的 Item 对象包含对 User 的引用,我们需要使用 @JsonIdentityInfo。我们将在下一节中了解它。
4. 使用 @JsonIdentityInfo
现在,让我们学习如何使用 @JsonIdentityInfo 帮助序列化具有双向关系的实体。
我们将把类级注释添加到我们的“User”实体
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class User { ... }
以及“Item”实体
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Item { ... }
现在进行测试
@Test
public void givenBidirectionRelation_whenUsingJsonIdentityInfo_thenCorrect()
throws JsonProcessingException {
User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);
String result = new ObjectMapper().writeValueAsString(item);
assertThat(result, containsString("book"));
assertThat(result, containsString("John"));
assertThat(result, containsString("userItems"));
}
这是序列化输出
{
"id":2,
"itemName":"book",
"owner":
{
"id":1,
"name":"John",
"userItems":[2]
}
}
5. 使用 @JsonIgnore
或者,我们可以使用 @JsonIgnore 注解来简单地 忽略关系的其中一方,从而打破链条。
在下面的示例中,我们将通过忽略“User”属性“userItems”从序列化中来防止无限递归
这是“User”实体
public class User {
public int id;
public String name;
@JsonIgnore
public List<Item> userItems;
}
这是我们的测试
@Test
public void givenBidirectionRelation_whenUsingJsonIgnore_thenCorrect()
throws JsonProcessingException {
User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);
String result = new ObjectMapper().writeValueAsString(item);
assertThat(result, containsString("book"));
assertThat(result, containsString("John"));
assertThat(result, not(containsString("userItems")));
}
最后,这是序列化输出
{
"id":2,
"itemName":"book",
"owner":
{
"id":1,
"name":"John"
}
}
6. 使用 @JsonView
我们还可以使用较新的 @JsonView 注解来排除关系的一方。
在下面的示例中,我们将使用 两个 JSON 视图,Public 和 Internal,其中 Internal 扩展了 Public
public class Views {
public static class Public {}
public static class Internal extends Public {}
}
我们将在 Public 视图中包含所有 User 和 Item 字段,除了 User 字段 userItems,它将包含在 Internal 视图中
这是我们的“User”实体:
public class User {
@JsonView(Views.Public.class)
public int id;
@JsonView(Views.Public.class)
public String name;
@JsonView(Views.Internal.class)
public List<Item> userItems;
}
这是我们的“Item”实体
public class Item {
@JsonView(Views.Public.class)
public int id;
@JsonView(Views.Public.class)
public String itemName;
@JsonView(Views.Public.class)
public User owner;
}
当使用 Public 视图进行序列化时,它可以正常工作,因为我们排除了 userItems 的序列化
@Test
public void givenBidirectionRelation_whenUsingPublicJsonView_thenCorrect()
throws JsonProcessingException {
User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);
String result = new ObjectMapper().writerWithView(Views.Public.class)
.writeValueAsString(item);
assertThat(result, containsString("book"));
assertThat(result, containsString("John"));
assertThat(result, not(containsString("userItems")));
}
但是,如果我们使用 Internal 视图进行序列化,JsonMappingException 会被抛出,因为所有字段都已包含。
@Test(expected = JsonMappingException.class)
public void givenBidirectionRelation_whenUsingInternalJsonView_thenException()
throws JsonProcessingException {
User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);
new ObjectMapper()
.writerWithView(Views.Internal.class)
.writeValueAsString(item);
}
7. 使用自定义 Serializer
接下来,我们将了解如何使用自定义 serializer 序列化具有双向关系的实体。
在下面的示例中,我们将使用一个自定义序列化器来序列化“User”属性“userItems”。
这是“User”实体
public class User {
public int id;
public String name;
@JsonSerialize(using = CustomListSerializer.class)
public List<Item> userItems;
}
这是“CustomListSerializer”
public class CustomListSerializer extends StdSerializer<List<Item>>{
public CustomListSerializer() {
this(null);
}
public CustomListSerializer(Class<List> t) {
super(t);
}
@Override
public void serialize(
List<Item> items,
JsonGenerator generator,
SerializerProvider provider)
throws IOException, JsonProcessingException {
List<Integer> ids = new ArrayList<>();
for (Item item : items) {
ids.add(item.id);
}
generator.writeObject(ids);
}
}
现在,让我们测试一下序列化器。正如我们所见,正在生成正确的输出。
@Test
public void givenBidirectionRelation_whenUsingCustomSerializer_thenCorrect()
throws JsonProcessingException {
User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);
String result = new ObjectMapper().writeValueAsString(item);
assertThat(result, containsString("book"));
assertThat(result, containsString("John"));
assertThat(result, containsString("userItems"));
}
这是使用自定义序列化器进行序列化的最终输出
{
"id":2,
"itemName":"book",
"owner":
{
"id":1,
"name":"John",
"userItems":[2]
}
}
8. 使用 @JsonIdentityInfo 反序列化
现在,让我们看看如何使用 @JsonIdentityInfo 反序列化具有双向关系的实体。
这是“User”实体
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class User { ... }
这是“Item”实体
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Item { ... }
我们将编写一个快速测试,从我们想要解析的一些手动 JSON 数据开始,并以正确构造的实体结束
@Test
public void givenBidirectionRelation_whenDeserializingWithIdentity_thenCorrect()
throws JsonProcessingException, IOException {
String json =
"{\"id\":2,\"itemName\":\"book\",\"owner\":{\"id\":1,\"name\":\"John\",\"userItems\":[2]}}";
ItemWithIdentity item
= new ObjectMapper().readerFor(ItemWithIdentity.class).readValue(json);
assertEquals(2, item.id);
assertEquals("book", item.itemName);
assertEquals("John", item.owner.name);
}
9. 使用自定义反序列化器
最后,让我们使用自定义反序列化器反序列化具有双向关系的实体。
在下面的示例中,我们将使用一个自定义反序列化器来解析“User”属性“userItems”。
这是“User”实体
public class User {
public int id;
public String name;
@JsonDeserialize(using = CustomListDeserializer.class)
public List<Item> userItems;
}
这是我们的“CustomListDeserializer”
public class CustomListDeserializer extends StdDeserializer<List<Item>>{
public CustomListDeserializer() {
this(null);
}
public CustomListDeserializer(Class<?> vc) {
super(vc);
}
@Override
public List<Item> deserialize(
JsonParser jsonparser,
DeserializationContext context)
throws IOException, JsonProcessingException {
return new ArrayList<>();
}
}
最后,这是一个简单的测试
@Test
public void givenBidirectionRelation_whenUsingCustomDeserializer_thenCorrect()
throws JsonProcessingException, IOException {
String json =
"{\"id\":2,\"itemName\":\"book\",\"owner\":{\"id\":1,\"name\":\"John\",\"userItems\":[2]}}";
Item item = new ObjectMapper().readerFor(Item.class).readValue(json);
assertEquals(2, item.id);
assertEquals("book", item.itemName);
assertEquals("John", item.owner.name);
}
10. 解决 Jackson 异常
当我们在同一个实体中使用多个双向关系时,我们可能在序列化期间遇到异常
com.fasterxml.jackson.databind.JsonMappingException: Multiple back-reference properties with name 'defaultReference'
这是因为 Jackson 为每个 @JsonBackReference 分配默认引用名称‘defaultReference’,除非我们显式提供一个值。如果一个实体有多个没有指定唯一名称的反向引用,Jackson 就不知道如何区分它们。
假设我们有一个 User,它有两个物品列表:wishlist 和 soldItems。两者都将 User 作为所有者进行引用
public class User {
public int id;
public String name;
@JsonManagedReference
public List wishlist = new ArrayList<>();
@JsonManagedReference
public List soldItems = new ArrayList<>();
}
public class Item {
public int id;
public String itemName;
@JsonBackReference
public User owner;
}
这会导致 Jackson 在序列化期间抛出 JsonMappingException,因为它无法区分具有相同默认名称的多个反向引用。
为了解决这个问题,我们需要通过 value 属性为每对引用提供一个唯一的名称
public class User {
public int id;
public String name;
@JsonManagedReference(value="wishlistRef")
public List wishlist = new ArrayList<>();
@JsonManagedReference(value="soldItemsRef")
public List soldItems = new ArrayList<>();
}
public class Item {
public int id;
public String itemName;
@JsonBackReference(value="wishlistRef")
public User wishlistOwner;
@JsonBackReference(value="soldItemsRef")
public User soldOwner;
}
现在,Jackson 确切地知道哪个反向引用对应哪个托管引用,并且序列化可以正确工作。
我们可以编写一个测试示例,演示如何使用命名的 @JsonManagedReference 和 @JsonBackReference 注解允许 Jackson 正确序列化具有多个反向引用的对象,而不会抛出异常
@Test
public void givenMultipleBackReferencesOnWishlist_whenNamedReference_thenNoException() throws JsonProcessingException {
User user = new User();
user.id = 1;
user.name = "Alice";
Item item1 = new Item();
item1.id = 101;
item1.itemName = "Book";
item1.wishlistOwner = user;
Item item2 = new Item();
item2.id = 102;
item2.itemName = "Pen";
item2.wishlistOwner = user;
user.wishlist = List.of(item1, item2);
Item item3 = new Item();
item3.id = 201;
item3.itemName = "Laptop";
item3.soldOwner = user;
Item item4 = new Item();
item4.id = 202;
item4.itemName = "Phone";
item4.soldOwner = user;
user.soldItems = List.of(item3, item4);
String json = new ObjectMapper().writeValueAsString(user);
assertThat(json, containsString("Alice"));
assertThat(json, containsString("Book"));
assertThat(json, containsString("Pen"));
}
11. 结论
在本文中,我们说明了如何使用 Jackson 序列化/反序列化具有双向关系的实体。
支持本文的代码可在 GitHub 上获取。 一旦你以 Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。















