Перейти к основному содержимому

Различные подходы к сериализации для Java

· 13 мин. чтения

1. Обзор

Сериализация — это процесс преобразования объекта в поток байтов. Затем этот объект можно сохранить в базе данных или передать по сети. Противоположной операцией по извлечению объекта из последовательности байтов является десериализация. Их основная цель — сохранить состояние объекта, чтобы мы могли воссоздать его при необходимости.

В этом руководстве мы рассмотрим различные подходы к сериализации объектов Java .

Во-первых, мы обсудим собственные API-интерфейсы Java для сериализации. Далее мы рассмотрим библиотеки, поддерживающие форматы JSON и YAML, чтобы сделать то же самое. Наконец, мы рассмотрим некоторые межъязыковые протоколы.

2. Пример класса сущности

Давайте начнем с представления простой сущности, которую мы собираемся использовать в этом руководстве:

public class User {
private int id;
private String name;

//getters and setters
}

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

3. Собственная сериализация Java

Сериализация в Java помогает обеспечить эффективную и быструю связь между несколькими системами. Java определяет способ сериализации объектов по умолчанию. Класс Java может переопределить эту сериализацию по умолчанию и определить свой собственный способ сериализации объектов.

Преимущества собственной сериализации Java:

  • Это простой, но расширяемый механизм
  • Он поддерживает тип объекта и свойства безопасности в сериализованной форме.
  • Расширяемость для поддержки маршалинга и демаршалинга по мере необходимости для удаленных объектов
  • Это нативное решение для Java, поэтому оно не требует никаких внешних библиотек.

3.1. Механизм по умолчанию

В соответствии со спецификацией сериализации объектов Java мы можем использовать метод writeObject() из класса ObjectOutputStream для сериализации объекта. С другой стороны, мы можем использовать метод readObject() , принадлежащий классу ObjectInputStream , для выполнения десериализации.

Мы проиллюстрируем основной процесс с помощью нашего класса User .

Во- первых, нашему классу нужно реализовать интерфейс Serializable :

public class User implements Serializable {
//fields and methods
}

Далее нам нужно добавить атрибут идентификатора serialVersionU `` :

private static final long serialVersionUID = 1L;

Теперь давайте создадим объект User :

User user = new User();
user.setId(1);
user.setName("Mark");

Нам нужно указать путь к файлу для сохранения наших данных:

String filePath = "src/test/resources/protocols/user.txt";

Теперь пришло время сериализовать наш объект User в файл:

FileOutputStream fileOutputStream = new FileOutputStream(filePath);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(user);

Здесь мы использовали ObjectOutputStream для сохранения состояния объекта User в файл «user.txt» .

С другой стороны, мы можем прочитать объект User из того же файла и десериализовать его:

FileInputStream fileInputStream = new FileInputStream(filePath);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
User deserializedUser = (User) objectInputStream.readObject();

Наконец, мы можем проверить состояние загруженного объекта:

assertEquals(1, deserializedUser.getId());
assertEquals("Mark", deserializedUser.getName());

Это стандартный способ сериализации объектов Java. В следующем разделе мы увидим пользовательский способ сделать то же самое.

3.2. Пользовательская сериализация с использованием внешнего интерфейса

Пользовательская сериализация может быть особенно полезна при попытке сериализовать объект с некоторыми несериализуемыми атрибутами. Это можно сделать, реализуя интерфейс Externalizable , который имеет два метода:

public void writeExternal(ObjectOutput out) throws IOException;

public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

Мы можем реализовать эти два метода внутри класса, который мы хотим сериализовать. Подробный пример можно найти в нашей статье о Externalizable Interface .

3.3. Предостережения о сериализации Java

Есть несколько предостережений, касающихся собственной сериализации в Java:

  • Только объекты с пометкой Serializable могут быть сохранены. Класс Object не реализует Serializable, поэтому не все объекты в Java могут сохраняться автоматически.
  • Когда класс реализует интерфейс Serializable , все его подклассы также являются сериализуемыми. Однако, когда объект имеет ссылку на другой объект, эти объекты должны реализовывать интерфейс Serializable отдельно, иначе будет выдано исключение NotSerializableException.
  • Если мы хотим управлять версиями, нам нужно предоставить атрибут serialVersionUID . Этот атрибут используется для проверки совместимости сохраненных и загруженных объектов. Поэтому нам нужно убедиться, что он всегда один и тот же, иначе будет выброшено исключение InvalidClassException.
  • Сериализация Java активно использует потоки ввода-вывода. Нам нужно закрыть поток сразу после операции чтения или записи, потому что , если мы забудем закрыть поток, мы получим утечку ресурсов . Чтобы предотвратить такие утечки ресурсов, мы можем использовать идиому try-with-resources . ``

4. Библиотека Гсона

Google Gson — это библиотека Java, которая используется для сериализации и десериализации объектов Java в представление JSON и из него.

Gson — это проект с открытым исходным кодом, размещенный на GitHub . Как правило, он предоставляет методы toJson() и fromJson() для преобразования объектов Java в JSON и наоборот.

4.1. Зависимость от Maven

Давайте добавим зависимость для библиотеки Gson :

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.7</version>
</dependency>

4.2. Сериализация Gson

Во-первых, давайте создадим объект User :

User user = new User();
user.setId(1);
user.setName("Mark");

Далее нам нужно указать путь к файлу для сохранения наших данных JSON:

String filePath = "src/test/resources/protocols/gson_user.json";

Теперь давайте воспользуемся методом toJson() из класса Gson для сериализации объекта User в файл gson_user.json :

Writer writer = new FileWriter(filePath);
Gson gson = new GsonBuilder().setPrettyPrinting().create();
gson.toJson(user, writer);

4.3. Десериализация Gson

Мы можем использовать метод fromJson() из класса Gson для десериализации данных JSON.

Давайте прочитаем файл JSON и десериализуем данные в объект User :

Gson gson = new GsonBuilder().setPrettyPrinting().create();
User deserializedUser = gson.fromJson(new FileReader(filePath), User.class);

Наконец, мы можем протестировать десериализованные данные:

assertEquals(1, deserializedUser.getId());
assertEquals("Mark", deserializedUser.getName());

4.4. Особенности Гсона

Gson имеет много важных функций, в том числе:

  • Он может обрабатывать коллекции, универсальные типы и вложенные классы.
  • С помощью Gson мы также можем написать собственный сериализатор и/или десериализатор, чтобы мы могли контролировать весь процесс.
  • Самое главное, он позволяет десериализовать экземпляры классов, исходный код которых недоступен.
  • Кроме того, мы можем использовать функцию управления версиями, если наш файл класса был изменен в разных версиях. Мы можем использовать аннотацию @Since для новых добавленных полей, а затем мы можем использовать метод setVersion() из GsonBuilder.

Дополнительные примеры см. в наших кулинарных книгах для сериализации Gson и десериализации Gson .

В этом разделе мы сериализовали данные в формате JSON с помощью Gson API. В следующем разделе мы будем использовать API Джексона, чтобы сделать то же самое.

5. API Джексона

Джексон также известен как «библиотека Java JSON» или «лучший синтаксический анализатор JSON для Java». Он предоставляет несколько подходов к работе с данными JSON.

Чтобы понять библиотеку Джексона в целом, лучше всего начать с нашего учебника по Джексону .

5.1. Зависимость от Maven

Добавим зависимость для Библиотеки Джексона :

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.12.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.12.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.4</version>
</dependency>

5.2. Объект Java в JSON

Мы можем использовать метод writeValue() , принадлежащий классу ObjectMapper , для сериализации любого объекта Java в виде вывода JSON.

Начнем с создания объекта User :

User user = new User();
user.setId(1);
user.setName("Mark Jonson");

После этого давайте укажем путь к файлу для хранения наших данных JSON:

String filePath = "src/test/resources/protocols/jackson_user.json";

Теперь мы можем сохранить объект User в файле JSON, используя класс ObjectMapper :

File file = new File(filePath);
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(file, user);

Этот код запишет наши данные в файл «jackson_user.json» .

5.3. JSON в объект Java

Простой метод readValue() класса ObjectMapper является хорошей отправной точкой. Мы можем использовать его для десериализации содержимого JSON в объект Java.

Давайте прочитаем объект User из файла JSON:

User deserializedUser = mapper.readValue(new File(filePath), User.class);

Мы всегда можем протестировать загруженные данные:

assertEquals(1, deserializedUser.getId());
assertEquals("Mark Jonson", deserializedUser.getName());

5.4. Особенности Джексона

  • Jackson — надежная и зрелая библиотека сериализации JSON для Java.
  • Класс ObjectMapper является точкой входа в процесс сериализации и предоставляет простой способ анализа и создания объектов JSON с большой гибкостью.
  • Одной из самых сильных сторон библиотеки Джексона является настраиваемый процесс сериализации и десериализации .

До сих пор мы видели сериализацию данных в формате JSON. В следующем разделе мы рассмотрим сериализацию с использованием YAML.

6. ЯМЛ

YAML расшифровывается как «YAML не является языком разметки». Это человекочитаемый язык сериализации данных. Мы можем использовать YAML для файлов конфигурации, а также в приложениях, где мы хотим хранить или передавать данные.

В предыдущем разделе мы видели, как API Джексона обрабатывает файлы JSON. Мы также можем использовать API-интерфейсы Jackson для обработки файлов YAML. Подробный пример можно найти в нашей статье о парсинге YAML с помощью Jackson .

Теперь давайте посмотрим на другие библиотеки.

6.1. YAML-бобы

YAML Beans упрощает сериализацию и десериализацию графов объектов Java в YAML и обратно.

Класс YamlWriter используется для сериализации объектов Java в YAML. Метод write() автоматически обрабатывает это, распознавая общедоступные поля и методы получения бина.

И наоборот, мы можем использовать класс YamlReader для десериализации объектов YAML в Java. Метод read() читает документ YAML и десериализует его в требуемый объект.

Прежде всего, давайте добавим зависимость для YAML Beans :

<dependency>
<groupId>com.esotericsoftware.yamlbeans</groupId>
<artifactId>yamlbeans</artifactId>
<version>1.15</version>
</dependency>

В настоящее время. давайте создадим карту объектов User :

private Map<String, User> populateUserMap() {
User user1 = new User();
user1.setId(1);
user1.setName("Mark Jonson");
//.. more user objects

Map<String, User> users = new LinkedHashMap<>();
users.put("User1", user1);
// add more user objects to map

return users;
}

После этого нам нужно указать путь к файлу для хранения наших данных:

String filePath = "src/test/resources/protocols/yamlbeans_users.yaml";

Теперь мы можем использовать класс YamlWriter для сериализации карты в файл YAML:

YamlWriter writer = new YamlWriter(new FileWriter(filePath));
writer.write(populateUserMap());
writer.close();

С другой стороны, мы можем использовать класс YamlReader для десериализации карты:

YamlReader reader = new YamlReader(new FileReader(filePath));
Object object = reader.read();
assertTrue(object instanceof Map);

Наконец, мы можем протестировать загруженную карту:

Map<String, User> deserializedUsers = (Map<String, User>) object;
assertEquals(4, deserializedUsers.size());
assertEquals("Mark Jonson", (deserializedUsers.get("User1").getName()));
assertEquals(1, (deserializedUsers.get("User1").getId()));

6.2. ЗмеяYAML

SnakeYAML предоставляет высокоуровневый API для сериализации объектов Java в документы YAML и наоборот. Последнюю версию 1.2 можно использовать с JDK 1.8 или более поздними версиями Java. Он может анализировать структуры Java, такие как String , List и Map .

Точкой входа для SnakeYAML является класс Yaml , который содержит несколько методов, помогающих в сериализации и десериализации.

Чтобы десериализовать ввод YAML в объекты Java, мы можем загрузить один документ с помощью метода load() и несколько документов с помощью метода loadAll() . Эти методы принимают объекты InputStream и String .

Идя в другом направлении, мы можем использовать метод dump() для сериализации объектов Java в документы YAML.

Подробный пример можно найти в нашей статье о парсинге YAML с помощью SnakeYAML .

Естественно, SnakeYAML хорошо работает с Java Maps , однако он может работать и с пользовательскими объектами Java.

В этом разделе мы видели разные библиотеки для сериализации данных в формат YAML. В следующих разделах мы обсудим межплатформенные протоколы.

7. Бережливость апачей

Apache Thrift изначально был разработан Facebook и в настоящее время поддерживается Apache.

Лучшее преимущество использования Thrift заключается в том, что он поддерживает межъязыковую сериализацию с меньшими накладными расходами . Кроме того, многие фреймворки сериализации поддерживают только один формат сериализации, однако Apache Thrift позволяет нам выбирать из нескольких.

7.1. Особенности экономии

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

  • TBinaryProtocol использует двоичный формат и, следовательно, обрабатывается быстрее, чем текстовый протокол.
  • TCompactProtocol является более компактным двоичным форматом и, следовательно, более эффективным для обработки .
  • TJSONProtocol использует JSON для кодирования данных

Thrift также поддерживает сериализацию типов контейнеров — списков, наборов и карт.

7.2. Зависимость от Maven

Чтобы использовать фреймворк Apache Thrift в нашем приложении, добавим библиотеки Thrift :

<dependency>
<groupId>org.apache.thrift</groupId>
<artifactId>libthrift</artifactId>
<version>0.14.2</version>
</dependency>

7.3. Экономичная сериализация данных

Протоколы и транспорты Apache Thrift предназначены для совместной работы в виде многоуровневого стека. Протоколы сериализуют данные в поток байтов, а транспорты считывают и записывают байты.

Как указывалось ранее, Thrift предоставляет ряд протоколов. Мы проиллюстрируем бережливую сериализацию с использованием двоичного протокола.

Прежде всего, нам нужен объект User :

User user = new User();
user.setId(2);
user.setName("Greg");

Следующим шагом будет создание бинарного протокола:

TMemoryBuffer trans = new TMemoryBuffer(4096);
TProtocol proto = new TBinaryProtocol(trans);

Теперь давайте сериализуем наши данные . Мы можем сделать это с помощью API записи :

proto.writeI32(user.getId());
proto.writeString(user.getName());

7.4. Экономичная десериализация данных

Давайте используем API чтения для десериализации данных:

int userId = proto.readI32();
String userName = proto.readString();

Наконец, мы можем протестировать загруженные данные:

assertEquals(2, userId);
assertEquals("Greg", userName);

Больше примеров можно найти в нашей статье об Apache Thrift .

8. Буферы протокола Google

Последний подход, который мы рассмотрим в этом руководстве, — буферы протокола Google (protobuf). Это хорошо известный двоичный формат данных.

8.1. Преимущества протокольных буферов

Буферы протокола обеспечивают несколько преимуществ, в том числе:

  • Не зависит от языка и платформы
  • Это двоичный формат передачи, то есть данные передаются как двоичные. Это повышает скорость передачи, поскольку занимает меньше места и полосы пропускания.
  • Поддерживает как обратную, так и прямую совместимость, поэтому новые версии могут считывать старые данные и наоборот.

8.2. Зависимость от Maven

Начнем с добавления зависимости для библиотек буферов протокола Google :

<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.17.3</version>
</dependency>

8.3. Определение протокола

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

syntax = "proto3";
package protobuf;
option java_package = "com.foreach.serialization.protocols";
option java_outer_classname = "UserProtos";
message User {
int32 id = 1;
string name = 2;
}

Это протокол простого сообщения типа User , который имеет два поля — id и name , типа integer и string соответственно. Обратите внимание, что мы сохраняем его как файл «user.proto» .

8.4. Генерация кода Java из файла Protobuf

Получив файл protobuf, мы можем использовать компилятор protoc для генерации из него кода:

protoc -I=. --java_out=. user.proto

В результате эта команда создаст файл UserProtos.java .

После этого мы можем создать экземпляр класса UserProtos :

UserProtos.User user = UserProtos.User.newBuilder().setId(1234).setName("John Doe").build();

8.5. Сериализация и десериализация Protobuf

Во-первых, нам нужно указать путь к файлу для хранения наших данных:

String filePath = "src/test/resources/protocols/usersproto";

Теперь давайте сохраним данные в файл. Мы можем использовать метод writeTo() из класса UserProtos — класса, который мы сгенерировали из файла protobuf:

FileOutputStream fos = new FileOutputStream(filePath);
user.writeTo(fos);

После выполнения этого кода наш объект будет сериализован в двоичный формат и сохранен в файле « usersproto ».

Наоборот , мы можем использовать метод mergeFrom() для загрузки этих данных из файла и их десериализации обратно в объект User :

UserProtos.User deserializedUser = UserProtos.User.newBuilder().mergeFrom(new FileInputStream(filePath)).build();

Наконец, мы можем протестировать загруженные данные:

assertEquals(1234, deserializedUser.getId());
assertEquals("John Doe", deserializedUser.getName());

9. Резюме

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

Java поддерживает встроенную сериализацию, которую легко использовать.

JSON предпочтительнее из-за удобочитаемости и отсутствия схемы. Следовательно, и Gson, и Jackson — хорошие варианты для сериализации данных JSON. Они просты в использовании и хорошо документированы. Для редактирования данных хорошо подходит YAML .

С другой стороны, двоичные форматы быстрее, чем текстовые форматы. Когда скорость важна для нашего приложения, Apache Thrift и Google Protocol Buffers являются отличными кандидатами для сериализации данных. Оба они компактнее и быстрее, чем форматы XML или JSON.

Подводя итог, можно сказать, что часто приходится идти на компромисс между удобством и производительностью, и сериализация ничем не отличается. Конечно, для сериализации данных доступно множество других форматов .

Как всегда, полный код примера закончился на GitHub .