1. 概述

本文我们将学习如何处理Jackson双向引用导致的无限递归问题。

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注解来简单地忽略关系的一方,从而打破循环。

在下面的例子中,我们通过忽略“用户”属性“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视图中包含所有“用户”和“项目”字段,但排除“用户”字段“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. 使用自定义序列化器

接下来,我们将学习如何使用自定义序列化器来序列化具有双向关系的实体。

在以下示例中,我们将使用自定义序列化器来序列化“用户”属性“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. 使用自定义反序列化器

最后,我们将使用自定义反序列化器来反序列化具有双向关系的实体。

在以下示例中,我们将使用自定义反序列化器来解析“用户”属性“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);
}