Ремонт, сервис, услуги » Информация » Механизмы сериализации в Java и Kotlin




Механизмы сериализации в Java и Kotlin

Автор: addministr от 11-05-2022, 09:00

Категория: Информация





Илья Гершман

Ведущий разработчик UsetechВ этой статье Илья Гершман, ведущий разработчик Юзтех, рассматривает понятия сериализации и десериализации в сравнении между двумя языками программирования — Java и Kotlin.

Немного об определениях “сериализация” и “десериализация”

Существует несколько примеров использования этих механизмов:

Хранение объектов в каком-либо хранилище. В этом случае мы сериализуем объект в массив байт, записываем его в хранилище, а затем, через какое-то время, когда нам этот объект понадобится, мы десериализуем его из массива байт, полученного из хранилища.



Передача объекта между приложениями. В этом случае одно наше приложение сериализует объект, передает полученный массив байт каким-либо образом другому нашему же приложению, а десериализацией уже занимается последнее.



Получение объектного представления запроса или формирование ответа. В этом случае наше приложение находится лишь на одной стороне и нам, соответственно, нужно либо десериализовать запрос, либо сериализовать ответ.



Внутренний формат. Этот формат понимает только та реализация, которая его и сделала. Java Serializable — наглядный пример реализации такого формата.



XML. Достаточно широкий формат. На его основе существует множество “подформатов”, которые реализуются различными библиотеками.



JSON. Наиболее популярный формат, так как поддерживается различными языками программирования и имеет практически однозначный вариант преобразования объекта в него.



Avro. Двоичный формат, который поддерживается многими языками программирования.



Protobuf. Ещё один двоичный формат, который поддерживается многими языками программирования.

Важным свойством механизма является устойчивость к эволюции объекта. Это значит, что нам бывает нужно десериализовать объект, который был сериализован предыдущей версией нашего приложения (записали в файл объект, затем обновили приложение, прочитали объект из файла). Или наоборот: нужно чтобы старая версия нашего приложения могла десериализовать данные, полученные новой версией (обновили только одну часть нашего приложения, и теперь она посылает данные в новом формате, которые читает старая версия приложения).Механизмы сериализации по-разному обеспечивают это свойство. Давайте рассмотрим несколько вариантов.

Стандартный

В Java есть стандартный способ сериализации. Его минус в том, что прочитать данные можно лишь из Java, а в classpath у нас должны быть классы, которые мы сериализовали.

import java.io.Serializable;

public class Address implements Serializable {
private final int countryCode;
private final String city;
private final String street;

public Address(int countryCode, String city, String street) {
this.countryCode = countryCode;
this.city = city;
this.street = street;
}

@Override
public String toString() {
return "[Address " +
"countryCode=" + countryCode +
", city='" + city + ''' +
", street='" + street + ''' +
']';
}
}

import java.io.Serializable;

public class Person implements Serializable {
private final String firstName;
private final String lastName;
private final Address address;

public Person(String firstName, String lastName, Address address) {
this.firstName = firstName;
this.lastName = lastName;
this.address = address;
}

@Override
public String toString() {
return "[Person " +
"firstName='" + firstName + ''' +
", lastName='" + lastName + ''' +
", address=" + address +
']';
}
}

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Main {
public static void main(String[] args) throws Throwable {
Path path = Paths.get("vasya.dat");
try (ObjectOutputStream oos = new ObjectOutputStream(
Files.newOutputStream(path))) {
Person person = new Person("Вася", "Пупкин",
new Address(7, "Н", "Бассейная"));
oos.writeObject(person);
}

try (ObjectInputStream ois = new ObjectInputStream(
Files.newInputStream(path))) {
Person read = (Person) ois.readObject();
System.out.printf("Read person: %s", read);
}
}
}Заметьте, как удобно — не пришлось ничего делать дополнительно. JVM сама за нас записала все поля объектов, а затем их сама прочитала.Если мы поменяем классы, например, добавим номер дома в адрес, то при чтении старого файла произойдет ошибка java.io.InvalidClassException. Давайте попробуем этого избежать.Сделаем свои методы записи и чтения, будем записывать версию класса и при чтении определять, какие поля нужно читать, а какие нет. Таким образом, мы можем знать про все прошлые версии и уметь их вычитывать различными способами, обеспечивая обратную совместимость. Прямую совместимость мы таким образом не реализуем — в данном механизме это не совсем тривиальная задача.

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Address implements Serializable {
// сами задаём значение, чтобы JVM не генерировала его
private static final long serialVersionUID = -4554333115192365232L;
private static final int VER = 2;

private int countryCode;
private String city;
private String street;
private int houseNumber;

public Address(int countryCode, String city, String street,
int houseNumber) {
this.countryCode = countryCode;
this.city = city;
this.street = street;
this.houseNumber = houseNumber;
}

private void writeObject(ObjectOutputStream oos) throws IOException {
oos.writeInt(VER);
oos.writeInt(countryCode);
oos.writeUTF(city);
oos.writeUTF(street);
oos.writeInt(houseNumber);
}

private void readObject(ObjectInputStream ois) throws IOException {
int ver = ois.readInt();
if (ver == 1) {
countryCode = ois.readInt();
city = ois.readUTF();
street = ois.readUTF();
houseNumber = 0;
} else if (ver == 2) {
countryCode = ois.readInt();
city = ois.readUTF();
street = ois.readUTF();
houseNumber = ois.readInt();
} else {
throw new IOException("Неизвестная версия: " + ver);
}
}

@Override
public String toString() {
return "[Address " +
"countryCode=" + countryCode +
", city='" + city + ''' +
", street='" + street + ''' +
", houseNumber=" + houseNumber +
']';
}
}

Внешние библиотеки

Теперь поговорим о нескольких библиотеках, которые позволяют сериализовывать объекты более гибким способом, чем стандартный механизм.

FasterXML Jackson

Это библиотека, которая изначально делалась для сериализации в JSON формат, но затем в неё добавили возможность сериализации любого формата. В свою очередь разработчики сделали соответствующие расширения для многих популярных форматов.

Jackson JSON

Добавим конструкторы по умолчанию и getter’ы к нашим классам, как того требует библиотека.

public class Address {
private final int countryCode;
private final String city;
private final String street;

public Address(int countryCode, String city, String street) {
this.countryCode = countryCode;
this.city = city;
this.street = street;
}

public Address() {
}

public int getCountryCode() {
return countryCode;
}

public String getCity() {
return city;
}

public String getStreet() {
return street;
}

@Override
public String toString() {
return "[Address " +
"countryCode=" + countryCode +
", city='" + city + ''' +
", street='" + street + ''' +
']';
}
}

public class Person {
private final String firstName;
private final String lastName;
private final Address address;

public Person(String firstName, String lastName, Address address) {
this.firstName = firstName;
this.lastName = lastName;
this.address = address;
}

public Person() {
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}

public Address getAddress() {
return address;
}

@Override
public String toString() {
return "[Person " +
"firstName='" + firstName + ''' +
", lastName='" + lastName + ''' +
", address=" + address +
']';
}
}

import com.fasterxml.jackson.databind.ObjectMapper;

public class Main {
public static void main(String[] args) throws Throwable {
ObjectMapper om = new ObjectMapper();
Person person = new Person("Вася", "Пупкин",
new Address(7, "Н", "Бассейная"));

String json = om.writeValueAsString(person);

Person read = om.readValue(json, Person.class);
System.out.printf("Read person: %sn", read);
}
}Получим такую строку:

{"firstName":"Вася","lastName":"Пупкин","address":{"countryCode":7,"city":"Н","street":"Бассейная"}}А что будет, если мы захотим добавить номер дома? Ничего страшного не случится: поле просто останется тем, каким оно было после вызова конструктора по умолчанию.А если наоборот, добавим в JSON houseNumber, а будем читать старым кодом? Получим ошибку com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException. Чтобы её избежать, можно добавить аннотацию на класс Address:

@JsonIgnoreProperties(ignoreUnknown = true)
public class Address {На самом деле, в библиотеке есть очень много различных настроек, с помощью которых можно сделать практически всё, что вы хотите.

Jackson XML

Ничего не меня в классах Person и Address мы с минимальными изменениями (создав другой ObjectMapper) можем сериализовать наш объект в XML:

ObjectMapper om = new XmlMapper();Получим при этом вот такую строку:

ВасяПупкин7НБассейная

Jackson Avro

Avro формат создан таким образом, что он работает со схемой данных. Мы должны указать схему при сериализации объекта, а также схему при десериализации (при этом есть возможность включать схему в сериализуемые данные). У получателя будет две схемы — схема писателя и своя, и он может решить, по какой из них читать.В библиотеке есть возможность получить схему прямо из нашего POJO, но мы не будем пользоваться этой возможностью, чтобы посмотреть, что такое Avro схема.Давайте опишем схему вручную. Делается это в JSON формате:

{
"type": "record",
"name": "Person",
"fields": [
{
"name": "firstName", "type": "string"
},
{
"name": "lastName", "type": "string"
},
{
"name": "address",
"type": {
"type": "record",
"name": "Address",
"fields": [
{
"name": "countryCode", "type": "int"
},
{
"name": "city", "type": "string"
},
{
"name": "street", "type": "string"
}
]
}
}
]
}Наш main теперь будет выглядеть так:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.avro.AvroMapper;
import com.fasterxml.jackson.dataformat.avro.AvroSchema;
import org.apache.avro.Schema;

import java.io.File;

public class Main {
public static void main(String[] args) throws Throwable {
Schema raw = new Schema.Parser()
.setValidate(true)
.parse(new File("avro-schema.json"));
AvroSchema schema = new AvroSchema(raw);

ObjectMapper om = new AvroMapper();

Person person = new Person("Вася", "Пупкин",
new Address(7, "Н", "Бассейная"));
byte[] bytes = om.writer(schema).writeValueAsBytes(person);

Person read = om.readerFor(Person.class)
.with(schema)
.readValue(bytes);
System.out.printf("Read person: %sn", read);
}
}

Jackson Protobuf

Protobuf — формат, который тоже требует предварительного описания схемы данных. На этот раз мы воспользуемся генератором из POJO:

import com.fasterxml.jackson.dataformat.protobuf.ProtobufMapper;
import com.fasterxml.jackson.dataformat.protobuf.schema.ProtobufSchema;

public class Main {
public static void main(String[] args) throws Throwable {
ProtobufMapper om = new ProtobufMapper();
ProtobufSchema schema = om.generateSchemaFor(Person.class);

Person person = new Person("Вася", "Пупкин",
new Address(7, "Н", "Бассейная"));
byte[] bytes = om.writer(schema).writeValueAsBytes(person);

Person read = om.readerFor(Person.class)
.with(schema)
.readValue(bytes);
System.out.printf("Read person: %sn", read);
}
}

Jackson Smile

Smile – это просто бинарный формат представления JSON’а. Нам нужно просто создать соответствующий ObjectMapper:

ObjectMapper om = new SmileMapper();

Kryo

Kryo — это библиотека для сериализации, которая нацелена на скорость и эффективность.

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Main {
public static void main(String[] args) throws Throwable {
Kryo kryo = new Kryo();

// нужно либо зарегистрировать все используемые классы,
kryo.register(Person.class);
kryo.register(Address.class);

// либо указать, что мы доверяем источнику и можно инстанцировать
// любые классы
kryo.setRegistrationRequired(false);

Path path = Paths.get("vasya.dat");
try (Output output = new Output(Files.newOutputStream(path))) {
Person person = new Person("Вася", "Пупкин",
new Address(7, "Н", "Бассейная"));
kryo.writeObject(output, person);
}

try (Input input = new Input(Files.newInputStream(path))) {
Person read = kryo.readObject(input, Person.class);
System.out.printf("Read person: %sn", read);
}
}
}Для обеспечения прямой и обратной совместимости можно указать:

kryo.setDefaultSerializer(CompatibleFieldSerializer.class);

Kotlin

А теперь давайте посмотрим, что интересного по поводу сериализации сделали в Kotlin. Так как Kotlin — это JVM based язык, то мы можем пользоваться всеми предыдущими библиотеками для сериализации. Но у Kotlin’а есть очень полезная библиотека kotlinx.serialization, которая позволяет строить схему на этапе компиляции, а не пользоваться Reflection API во время выполнения. Это обеспечивает более быструю работу.Давайте для начала перепишем наши классы на Kotlin:

import kotlinx.serialization.Serializable

@Serializable
data class Address(
val countryCode: Int,
val city: String,
val street: String,
)

@Serializable
data class Person(
val firstName: String,
val lastName: String,
val address: Address,
)

JSON

Теперь сделаем сериализацию в JSON:

import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

fun main() {
val json = Json

val person = Person("Вася", "Пупкин",Address(7, "Н", "Бассейная"))
val str = json.encodeToString(person)

val read = json.decodeFromString(str)
println("Read person: $read")
}Добавляя поле в Address, получим kotlinx.serialization.MissingFieldException. Чтобы этого избежать можно указать значение по умолчанию для этого поля:

@Serializable
data class Address(
val countryCode: Int,
val city: String,
val street: String,
val houseNumber: Int = 0,
)

Protobuf

В Protobuf сериализация делается не сложнее:

import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.protobuf.ProtoBuf

fun main() {
val protobuf = ProtoBuf

val person = Person("Вася", "Пупкин",Address(7, "Н", "Бассейная"))
val bytes = protobuf.encodeToByteArray(person)

val read = protobuf.decodeFromByteArray(bytes)
println("Read person: $read")
}

Что можно сказать в конце?

Мы рассмотрели лишь небольшое количество вариантов для сериализации объектов в JVM. Выбор метода зависит от многих факторов. Решите что вам нужно: кроссплатформенность, поддержка обратной и/или прямой совместимости, скорость сериализации и десериализации, а также важен ли размер получаемых данных.В любом случае стандартный метод сериализации вряд ли стоит использовать. Мы его рассмотрели исключительно в познавательных целях. Он медленный, довольно объёмный, и совместимость там делается руками.Если вы можете использовать Kotlin, то я бы посоветовал использовать его библиотеку – это удобное и эффективное решение.Ну а если вы ограничены чистой Java, то, на мой взгляд, библиотека Jackson – отличный вариант. Она довольно быстрая, имеет множество настроек, а также вы легко можете поменять формат, не переписывая свой код. Формат можно выбрать под вашу конкретную задачу:

JSON – на все случаи жизни, так как он поддерживается всеми языками и фреймворками, а также из-за его наглядности;



Protobuf или Avro – если нужна скорость и минимальный размер (у них есть различия, но их обсуждение – это дело отдельной статьи);



XML – например, если вам нужно валидировать данные по XSD, или ещё по каким-то причинам;



Ещё какой-либо формат.




 

Источник: https://habr.com/ru/company/usetech/blog/665046/





Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Архив | Связь с админом | Конфиденциальность

RSS канал новостей     Яндекс.Метрика