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

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

1. обзор

В этом руководстве мы рассмотрим лучшие способы борьбы сbidirectional relationships in Jackson.

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

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

Во-первых, давайте взглянем на проблему бесконечной рекурсии Джексона. В следующем примере у нас есть две сущности - «User» и «Item» - сa simple one-to-many relationship:

Сущность «User»:

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

Сущность «Item»:

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

full exception:

com.fasterxml.jackson.databind.JsonMappingException:
Infinite recursion (StackOverflowError)
(through reference chain:
org.example.jackson.bidirection.Item["owner"]
->org.example.jackson.bidirection.User["userItems"]
->java.util.ArrayList[0]
->org.example.jackson.bidirection.Item["owner"]
->…..

Давайте посмотрим, в течение следующих нескольких разделов - как решить эту проблему.

3. Используйте@JsonManagedReference,@JsonBackReference

Во-первых, давайте аннотируем отношения с помощью@JsonManagedReference,@JsonBackReference, чтобы Джексон мог лучше обрабатывать отношения:

Вот сущность «User»:

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

    @JsonBackReference
    public List userItems;
}

И «Item»:

public class Item {
    public int id;
    public String itemName;

    @JsonManagedReference
    public User owner;
}

Теперь давайте проверим новые сущности:

@Test
public void
  givenBidirectionRelation_whenUsingJacksonReferenceAnnotation_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"
    }
}

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

  • @JsonManagedReference - это прямая часть ссылки - та, которая сериализуется нормально.

  • @JsonBackReference - это задняя часть ссылки - она ​​будет опущена при сериализации.

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 для простогоignore one of the sides of the relationship, тем самым разорвав цепочку.

В следующем примере - мы предотвратим бесконечную рекурсию, игнорируя свойство «User» «userItems» из сериализации:

Вот сущность «User»:

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, чтобы исключить одну сторону связи.

В следующем примере - мы используемtwo JSON Views – Public and Internal, гдеInternal расширяетPublic:

public class Views {
    public static class Public {}

    public static class Internal extends Public {}
}

Мы включим все поляUser иItem в представлениеPublic -except the User field 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 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, оно работает правильно -because we excluded 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. Используйте настраиваемый сериализатор

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

В следующем примере мы будем использовать настраиваемый сериализатор для сериализации свойства «User» «userItems»:

Вот сущность «User»:

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

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

А вот «CustomListSerializer»:

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

Иthe final output сериализации с помощью настраиваемого сериализатора:

{
 "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 userItems;
}

А вот наш «CustomListDeserializer»:

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

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

Реализация всех этих примеров и фрагментов кодаcan be found in our GitHub project - это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.