Статьи

Пользовательские сериализаторы Spring с @JsonIdentityInfo

вступление

Сериализация / десериализация из / в JSON в Spring широко используется в современных Spring-приложениях. Это основано на Джексоне. Джексон может легко сериализовать любой POJO в JSON и наоборот. Этот код хорошо написан. Я никогда не сталкивался с какими-либо проблемами. Это становится сложнее, когда участвуют пользовательские сериализаторы. В этом посте показано, как использовать настраиваемые сериализаторы в Spring с автоматически связанными полями.

Определение пользовательского сериализатора

Обычно пользовательский сериализатор для класса наследуется от
com.fasterxml.jackson.databind.ser.std.StdSerializer. Этот класс определяет некоторые конструкторы, но каркасу нужен только конструктор без аргументов, который должен вызывать суперкласс, что-то вроде этого:

1
2
3
4
5
6
7
public CustomSerializer() {
    this(null);
}
 
public CustomSerializer(Class<ObjectToSerialize> t) {
    super(t);
}

Тогда есть основной метод, который должен быть реализован для написания JSON:

1
2
3
4
5
6
7
8
@Override
public void serialize(ObjectToSerialize value, JsonGenerator gen, SerializerProvider provider) throws IOException {
    gen.writeStartObject();
    ...
    provider.defaultSerializeField("some field", value.getField(), gen);
    ...
    gen.writeEndObject();
}

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

1
2
@JsonSerialize(using = CustomSerializer.class)
public class ObjectToSerialize {

Теперь Джексон будет использовать этот собственный сериализатор для всех экземпляров этого класса. При необходимости пользовательский десериализатор может быть написан с помощью подклассов
com.fasterxml.jackson.databind.deser.std.StdDeserializer <Т>

Циркулярные ссылки и @JsonIdentityInfo

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

У нас есть 2 класса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class Building {
 
    @Id
    @GeneratedValue(<parameters>)
    private Long id;
 
    private Set<Apartment> apartments;
}
 
public class Apartment {
 
    @Id
    @GeneratedValue(<parameters>)
    private Long id;
 
    private Building building;
}

Если мы попытаемся сериализовать одно здание, в котором есть хотя бы одна квартира, мы получим исключение StackOverflowException.

У Джексона есть решение этой проблемы — @JsonIdentityInfo.

Если аннотация @JsonIdentityInfo добавляется в класс следующим образом:

1
2
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class ObjectToSerialize {

тогда любой ObjectMapper прервет цикл, заменив каждое вхождение объекта, кроме первого, его идентификатором. Как это:

01
02
03
04
05
06
07
08
09
10
11
12
13
{
    "id": 1,
    "apartments": [
        {
            "id": 2,
            "building": 1 - the object is replaced with its ID
        },
        {
            "id": 3,
            "building": 1 - the object is replaced with its ID
        }
    ]
}

Это инструменты, которые Джексон предоставляет для настройки сериализации и работы с циклическими ссылками.

JSON Структурная проблема

проблема

@JsonIdentityInfo хорошо работает для простых приложений. Но поскольку приложение растет в форме по умолчанию, это может повлиять на структуру JSON. Например, если какой-то метод возвращает здания и районы в одном ответе, вот что может произойти:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
    "buildings": [
        {
            "id": 1,
            "apartments": [
                {
                    "id": 2,
                    "building": 1 - the object is replaced with its ID
                },
                {
                    "id": 3,
                    "building": 1 - the object is replaced with its ID
                }
            ]
        }
    ],
    "districts": [
         {
             "buildings": [
                 {
                     "id": 5,
                     ...
                 },
                 1, - the object is replaced with its ID
                 {
                     "id": 6,
                     ...
                 }
             ]
         }
    ]
}

Эта замена может быть довольно непредсказуемой с точки зрения парсера. Внутри массива он может встретить объекты и идентификаторы. И это может произойти для любого поля и любого объекта. Любой объект, в котором класс помечен @JsonIdentityInfo, заменяется его идентификатором, если поставщик сериализации находит его более одного раза. Каждый второй, третий, четвертый и т. Д. Экземпляр с тем же идентификатором, найденным поставщиком сериализации, заменяется его идентификатором.

Решение

Решением здесь является использование отдельного ObjectMapper для написания частей JSON. Списки уже увиденных идентификаторов хранятся в поставщике сериализации, который создается ObjectMapper. При создании отдельного ObjectMapper (возможно, с другой конфигурацией) списки сбрасываются.

Для «составного» результата JSON, который возвращает разные типы объектов, можно написать собственный сериализатор. В этом настраиваемом сериализаторе «заголовок» пишется вручную с помощью методов JsonGenerator, и когда достигается правильный уровень в JSON, мы создаем новый ObjectMapper и пишем гораздо лучше выглядящий JSON.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
    "buildings": [ - create a new ObjectMapper
        {
            "id": 1,
            "apartments": [
                {
                    "id": 2,
                    "building": 1 - the object is replaced with its ID
                },
                {
                    "id": 3,
                    "building": 1 - the object is replaced with its ID
                }
            ]
        }
    ],
    "districts": [ - create a new ObjectMapper
         {
             "buildings": [
                 {
                     "id": 5,
                     ...
                 },
                 { - the object is written as a JSON Object not an ID
                     "id": 1,
                     ...
                 },
                 {
                     "id": 6,
                     ...
                 }
             ]
         }
    ]
}

Для записи JSON в исходный генератор мы можем использовать
ObjectMapper.writeValueAsString и
JsonGenerator.writeRawValue (String).

PS также возможно создать нового поставщика сериализации с помощью
DefaultSerializerProvider.createInstance (SerializationConfig, SerializerFactory), но это потенциально более сложно.

Проблема с автопроводами для нестандартного сериализатора

проблема

Мы хотели бы иметь возможность использовать @Autowire в наших пользовательских сериализаторах. Это одна из лучших функций Spring! На самом деле это работает, если используется ObjectMapper по умолчанию. Но если мы используем решение проблемы структуры JSON, оно не будет работать для пользовательских сериализаторов, созданных нашими собственными объектными сопоставителями.

Решение

Наши собственные объектные сопоставители должны быть настроены со специальным HandlerInstantiator:

1
2
3
4
5
// try to use the default configuration as much as possible
ObjectMapper om = Jackson2ObjectMapperBuilder.json().build();
// This instantiator will handle autowiring properties into the custom serializers
om.setHandlerInstantiator(
new SpringHandlerInstantiator(this.applicationContext.getAutowireCapableBeanFactory()));

Если настраиваемые сопоставители объектов создаются в другом настраиваемом сериализаторе, который создается с помощью ObjectMapper по умолчанию, тогда он может автоматически связать ApplicationContext.

Опубликовано на Java Code Geeks с разрешения Вадима Коркина, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Spring Custom Serializer с @JsonIdentityInfo

Мнения, высказанные участниками Java Code Geeks, являются их собственными.