Jackson – 雙向關係

Data,Jackson
Remote
0
11:47 PM · Nov 30 ,2025

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. 使用 ,

首先,讓我們用 註解關係,以便 Jackson 更好地處理關係:

以下是“”實體:

public class User {
    public int id;
    public String name;

    @JsonManagedReference
    public List<Item> userItems;
}

以及“”:

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"}]
}

請注意:

    是引用的一部分(forward part),它會被正常序列化。
  • 是引用的一部分(back part),它不會包含在序列化中。
  • 序列化的 對象不包含對 對象的引用。

請注意,我們不能交換註解。以下代碼對於序列化有效:

@JsonBackReference
public List<Item> userItems;

@JsonManagedReference
public User owner;

但是,當我們嘗試反序列化對象時,它會拋出異常,因為 不能用於集合。

如果我們想讓序列化的 對象包含對 對象的引用,則需要使用 。我們將會在下一部分中討論它。

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"
    }
}

我們還可以使用較新的註解來排除關係的其中一側。

在下面的示例中,我們將使用兩個JSON視圖,,其中繼承:

public class Views {
    public static class Public {}

    public static class Internal extends Public {}
}

我們將包含所有字段在視圖中,除了字段,該字段將在視圖中包含:

以下是我們的實體:

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;
}

以下是我們的“”實體:

public class Item {
    @JsonView(Views.Public.class)
    public int id;

    @JsonView(Views.Public.class)
    public String itemName;

    @JsonView(Views.Public.class)
    public User owner;
}

當我們使用視圖進行序列化時,它會正常工作,因為我們排除了不被序列化:

@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")));
}

但是,如果我們使用視圖進行序列化,則會拋出,因為所有字段都包含在內:

@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. 使用自定義序列化器

接下來,我們將看到如何使用自定義序列化器來序列化具有雙向關係的實體。

User” property “userItems“.”">在下面的示例中,我們將使用自定義序列化器來序列化“User”屬性“userItems”。

User” entity:">以下是“User”實體:

public class User {
    public int id;
    public String name;

    @JsonSerialize(using = CustomListSerializer.class)
    public List<Item> userItems;
}

CustomListSerializer“:">以下是“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 將默認引用名稱 ‘defaultReference‘ 賦給每個 @JsonBackReference,除非我們明確提供值。 如果一個實體具有多個引用且未指定唯一名稱,Jackson 不知道如何區分它們。

假設我們有一個 User 包含兩個項目列表:wishlistsoldItems。兩者都引用 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 通過雙向關係序列化/反序列化實體。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.