Рубрики
Без рубрики

Джексон – Двунаправленные Отношения

Как использовать Джексона для решения проблемы бесконечной рекурсии в двунаправленных отношениях.

Автор оригинала: Eugen Paraschiv.

1. Обзор

В этом уроке мы рассмотрим лучшие способы работы с двунаправленными отношениями в Jackson .

Мы обсудим проблему бесконечной рекурсии Джексона JSON, затем – мы увидим, как сериализовать сущности с двунаправленными отношениями, и, наконец, – мы десериализуем их.

2. Бесконечная Рекурсия

Во – первых, давайте взглянем на проблему бесконечной рекурсии Джексона. В следующем примере у нас есть две сущности – ” Пользователь ” и ” Элемент “– с простым отношением “один ко многим” :

Сущность ” Пользователь “:

public class User {
    public int id;
    public String name;
    public List userItems;
}

В ” Элемент ” сущность:

public class Item {
    public int id;
    public String itemName;
    public User owner;
}

Когда мы пытаемся сериализовать экземпляр ” Item “, Джексон выдаст 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 , чтобы Джексон лучше справлялся с отношением:

Вот сущность ” User “:

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

    @JsonManagedReference
    public List 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"
}

А вот результат сериализации пользовательского объекта:

{
 "id":1,
 "name":"John",
 "userItems":[{
   "id":2,
   "itemName":"book"}]
}

Обратите внимание, что:

  • @JsonManagedReference – это прямая часть ссылки, которая обычно сериализуется.
  • @JsonBackReference является обратной частью ссылки – она будет опущена из сериализации.
  • Сериализованный Элемент объект не содержит ссылки на объект User .

Кроме того, обратите внимание, что мы не можем переключаться между аннотациями. Для сериализации будет работать следующее:

@JsonBackReference
public List userItems;

@JsonManagedReference
public User owner;

Но вызовет исключение, когда мы попытаемся десериализовать объект, так как @JsonBackReference не может использоваться в коллекции.

Если мы хотим, чтобы объект сериализованного элемента содержал ссылку на пользователя, нам нужно использовать @JsonIdentityInfo . Мы рассмотрим это в следующем разделе.

4. Используйте @JsonIdentityInfo

Теперь давайте посмотрим, как помочь с сериализацией сущностей с двунаправленными отношениями с помощью @JsonIdentityInfo .

Мы добавляем аннотацию уровня класса к нашей сущности ” User “:

@JsonIdentityInfo(
  generator = ObjectIdGenerators.PropertyGenerator.class, 
  property = "id")
public class User { ... }

И к ” Элемент ” сущность:

@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 ” от сериализации:

Вот ” Пользователь ” сущность:

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

    @JsonIgnore
    public List 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 {}
}

Мы включим все User и Item поля в Public View – за исключением User поля userItems , которые будут включены в Внутреннее представление:

Вот наша сущность ” Пользователь “:

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

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

    @JsonView(Views.Internal.class)
    public List 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;
}

Когда мы сериализуем с помощью представления 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")));
}

Но если мы сериализуем с помощью внутреннего представления, 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. Используйте пользовательский сериализатор

Далее – давайте посмотрим, как сериализовать объекты с двунаправленными отношениями с помощью пользовательского сериализатора.

В следующем примере – мы будем использовать пользовательский сериализатор для сериализации ” User “свойства” userItems “:

Вот сущность ” User “:

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

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

А вот ” Пользовательский ListSerializer “:

public class CustomListSerializer extends StdSerializer>{

   public CustomListSerializer() {
        this(null);
    }

    public CustomListSerializer(Class t) {
        super(t);
    }

    @Override
    public void serialize(
      List items, 
      JsonGenerator generator, 
      SerializerProvider provider) 
      throws IOException, JsonProcessingException {
        
        List 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 { ... }

И ” Элемент ” сущность:

@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 “:

Вот ” Пользователь ” сущность:

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

    @JsonDeserialize(using = CustomListDeserializer.class)
    public List userItems;
}

А вот наш ” Пользовательский десериализатор списка “:

public class CustomListDeserializer extends StdDeserializer>{

    public CustomListDeserializer() {
        this(null);
    }

    public CustomListDeserializer(Class vc) {
        super(vc);
    }

    @Override
    public List 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. Заключение

В этом руководстве мы проиллюстрировали, как сериализовать/десериализовать сущности с двунаправленными отношениями с помощью Джексона.

Реализацию всех этих примеров и фрагментов кода можно найти в нашем проекте GitHub – это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.