Сервисы редко существуют сами по себе, они активно обмениваются данными с окружающим миром и другими сервисами.
Сериализация превращает Java объект в набор байтов, который можно передать по сети или куда-нибудь записать. Также встречается под именами marshalling или encoding.
Десериализация восстанавливает Java объект из полученных байтов. Где-то этот процесс называется unmarshalling или decoding.
Пожелания:
1️⃣ Чтобы набор байтов занимал поменьше места. Чем короче сообщение, тем быстрее оно передаётся
2️⃣Минимум усилий со стороны программиста
Сериализация появилась в первой версии Java, и по сравнению с другими языками это была фантастика. JVM брала большую часть работы на себя. Байтовые сообщения получались компактными и быстро стали частью EJB, JMX, JMS и т.д
С тех пор механизм сериализации в java не менялся. Классы, экземпляры которых покидают JVM, должны реализовать интерфейс Serializable:
class UserRequest implements SerializableУ него нет обязательных методов, это интерфейс-маркер.
Интерфейс Externalizable даёт полный контроль над итоговым набором байтов. Хотите записать java объект в PDF или зашифровать данные - реализуйте методы Externalizable.
Как происходит сериализация Serializable классов:
1️⃣ Проверка полей
static и transient поля не участвуют в сериализации. Остальные поля должны быть либо Serializable, либо примитивами. Иначе разработчик получит NotSerializableException.
2️⃣ Объект превращается в байты
Для передачи данных обычно используется ObjectOutputStream, но часто он скрыт за фреймворком или библиотекой. Что туда пишет JVM:
🔸 Поля-заголовки
🔸 Информация о классе:
▪️ Имя класса
▪️ serialVersionUID
▪️ Количество полей
▪️ Информация по каждому полю:
▫️ Тип (имя класса или примитив)
▫️ Длина
▫️ Имя переменной
🔸 Информация про Serializable родительские классы в таком же формате
🔸 Значения переменных Serializable родительских классов
🔸 Значения переменных текущего класса. Если переменная - не примитив, то схема повторяется - записывается информация про класс и значения полей.
В примере перед постом класс Parent не реализует Serializable, поэтому parentValue не записывается в итоговый стрим, только childValue.
3️⃣ Набор байтов готов, можно отправлять.
Десериализация по шагам
Посмотрим на примере класса Parent и Child из примера выше.
1️⃣ Читаем из полученных байтов информацию о классе и о всех ближайших Serializable родителях.
2️⃣ Ищем ближайший НЕ Serializable родитель. В примере это класс Parent
3️⃣ Вызываем у класса Parent конструктор без параметров.
Тут проставляется parentValue = 2
4️⃣ Получаем экземпляр. Конструктор Child не используется, остальные поля проставляются внутренними механизмами JVM.
5️⃣ Чтение полей из потока байтов. В нашем примере передано только 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 сериализации.