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