2021-03-22 09:00:28
Сериализация, часть 1: обзор
Сервисы редко существуют сами по себе, они активно обмениваются данными с окружающим миром и другими сервисами.
Сериализация превращает Java объект в набор байтов, который можно передать по сети или куда-нибудь записать. Также встречается под именами marshalling или encoding.
Десериализация восстанавливает Java объект из полученных байтов. Где-то этот процесс называется unmarshalling или decoding.
Пожелания:
Чтобы набор байтов занимал поменьше места. Чем короче сообщение, тем быстрее оно передаётся
Минимум усилий со стороны программиста
Сериализация появилась в первой версии Java, и по сравнению с другими языками это была фантастика. JVM брала большую часть работы на себя. Байтовые сообщения получались компактными и быстро стали частью EJB, JMX, JMS и т.д
С тех пор механизм сериализации в java не менялся. Классы, экземпляры которых покидают JVM, должны реализовать интерфейс
Serializable:
class UserRequest implements Serializable
У него нет обязательных методов, это интерфейс-маркер.
Интерфейс
Externalizable даёт полный контроль над итоговым набором байтов. Хотите записать java объект в PDF или зашифровать данные - реализуйте методы
Externalizable.
Как происходит сериализация Serializable классов:
Проверка полей
static и
transient поля не участвуют в сериализации. Остальные поля должны быть либо
Serializable, либо примитивами. Иначе разработчик получит
NotSerializableException.
Объект превращается в байты
Для передачи данных обычно используется
ObjectOutputStream, но часто он скрыт за фреймворком или библиотекой. Что туда пишет JVM:
Поля-заголовки
Информация о классе:
Имя класса
serialVersionUID
Количество полей
Информация по каждому полю:
Тип (имя класса или примитив)
Длина
Имя переменной
Информация про
Serializable родительские классы в таком же формате
Значения переменных
Serializable родительских классов
Значения переменных текущего класса. Если переменная - не примитив, то схема повторяется - записывается информация про класс и значения полей.
В примере перед постом класс
Parent не реализует
Serializable, поэтому
parentValue не записывается в итоговый стрим, только
childValue.
Набор байтов готов, можно отправлять.
Десериализация по шагам
Посмотрим на примере класса
Parent и
Child из примера выше.
Читаем из полученных байтов информацию о классе и о всех ближайших
Serializable родителях.
Ищем ближайший
НЕ Serializable родитель. В примере это класс
Parent
Вызываем у класса
Parent конструктор без параметров.
Тут проставляется
parentValue = 2
Получаем экземпляр. Конструктор
Child не используется, остальные поля проставляются внутренними механизмами JVM.
Чтение полей из потока байтов. В нашем примере передано только
childValue. Записываем:
childValue = 50;
Итого: в консоль выведется 2 и 50. Хотя изначально мы создавали объект с
parentValue = 4, это поле не передаётся при сериализации, поэтому используется значение из конструктора
Parent().
Как исправить ситуацию? Есть два варианта:
Добавить классу
Parent интерфейс
Serializable
Переопределить в классе
Child методы
writeObject и
readObject. Они не определены в Serializable, но JVM найдёт их в процессе сериализации.
В
writeObject задаётся, какие поля и в каком порядке запишутся в итоговый объект:
private void writeObject(…out) {
out.writeInt(parentValue);
out.writeInt(childValue);
}
В
readObject указывается, какие поля и в каком порядке читать из байтового стрима:
private void readObject(…in){
int parentValue=in.readInt();
setParentValue(parentValue);
this.childValue=in.readInt();
}
В любом из вариантов десериализованный объект напечатает 4 и 50.
В следующем посте поговорим, зачем в сообщении нужен
serialVersionUID, когда его задавать напрямую и менять, а также про недостатки Java сериализации.
1.4K views06:00