6 как создать собственный протокол сериализации

Обновлено: 02.07.2024

В соответствии с определением, данным Microsoft, сериализация — это процесс преобразования объекта в поток байтов для сохранения или передачи в память, базу данных или файл. Сериализация используется для того, чтобы сохранить состояния объекта для последующего воссоздания при необходимости. Обратный процесс называется десериализацией.

Пример сериализации и десериализации JSON

Допустим, у нас имеется вот такой класс для хранения информации о человеке:

Создадим объект этого класса и сохраним состояние объекта в файл в формате JSON:

Здесь мы выполнили следующие действия:

  1. Создали сам объект для сериализации ( person )
  2. Сериализовали объект в строку ( string ) используя сериализатор JsonSerializer
  3. Записали полученную строку в текстовый файл person.json .

Теперь рядом с exe-файлом приложения появился файл со следующим содержимым:

Теперь мы можем при следующем запуске программы восстановить (десериализовать) этот объект в нашей программе, например, так:

Настройки сериализации JSON

Настройка сериализации с помощью JsonSerializerOptions

Для настройки сериализации мы можем передать в метод Serialize второй параметр — объект класса JsonSerializerOptions , который содержит настройки сериализации/десериализации объектов. Основные свойства этого класса следующие:

  • bool AllowTrailingCommas — устанавливает, надо ли добавлять после последнего элемента в json запятую. Если равно true , запятая добавляется
  • JsonNamingPolicy DictionaryKeyPolicy — возвращает или задает политику, используемую для преобразования имени ключа IDictionary в другой формат, например CamelCase.
  • JavaScriptEncoder Encoder — Возвращает или устанавливает кодировщик, используемый при экранировании строк. Укажите значение null для использования кодировщика по умолчанию
  • bool IgnoreNullValues — устанавливает, будут ли сериализоваться/десериализоваться в json объекты и их свойства со значением null
  • bool IgnoreReadOnlyProperties — аналогично устанавливает, будут ли сериализоваться свойства, предназначенные только для чтения
  • int MaxDepth — возвращает или задает максимальную глубину, разрешенную при сериализации или десериализации JSON, при этом значение по умолчанию 0 указывает максимальную глубину 64 .
  • bool PropertyNameCaseInsensitive — возвращает или задает значение, которое определяет, использует ли имя свойства сравнение без учета регистра во время десериализации. Значение по умолчанию — false .
  • JsonNamingPolicy PropertyNamingPolicy — возвращает или задает значение, указывающее политику, используемую для преобразования имени свойства объекта в другой формат, например CamelCase стиль, или null , чтобы оставить имена свойств без изменений.
  • JsonCommentHandling ReadCommentHandling — Возвращает или задает значение, определяющее, как комментарии обрабатываются во время десериализации
  • bool WriteIndented — устанавливает, будут ли добавляться в json пробелы (условно говоря, для красоты). Если равно true устанавливаются дополнительные пробелы

Рассмотрим несколько примеров использования этих настроек сериализации Json.

Как получить форматированную строку (с лидирующими пробелами) Json?

Для этого необходимо в настройках сериализации указать значение WriteIndented = true :

<
"Name": "\u0412\u0430\u0441\u044F",
"Surname": "\u041F\u0443\u043F\u043A\u0438\u043D",
"Age": 38,
"Birthday": "1983-01-16T00:00:00"
>

Как запретить экранировать строки при сериализации JSON?

В примере выше у нас все строки (имя, фамилия) экранированы. С точки зрения безопасности — это полностью правильное решение, но не всегда удобно для чтения. Чтобы позволить сериализатору не экранировать символы в строках мы можем воспользоваться свойством Encoder у JsonSerializerOptions следующим образом:

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

Как сериализовать имена свойств в CamelCase?

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

Настройка сериализации с помощью атрибутов

Исключим свойство Age из сериализуемых, используя атрибут JsonIgnore :

Также, часто используемым атрибутом для настройки сериализации/десериализации JSON является атрибут JsonPropertyName , который позволяет задать произвольное имя сериализуемому свойству. Например,

Теперь свойство Surname будет сериализоваться в JSON с именем PersonFamily .

Особенности сериализации и десериализации JSON

При сериализации/десериализации JSON с использованием штатного сериализатора JsonSerializer необходимо обратить внимание на следующие моменты:

Десериализуемый объект должен иметь конструктор без параметров

То при попытке десериализовать такой объект мы получим исключение:

Сериализации подлежат только публичные свойства

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

Итого

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

Мы будем использовать ниже Employee объект класса в качестве примера для объяснения

Что такое сериализация и десериализация?

В Java мы создаем несколько объектов, которые живут и умирают соответственно, и каждый объект обязательно умрет, когда JVM умрет. Но иногда нам может потребоваться повторно использовать объект между несколькими JVM или мы можем перенести объект на другой компьютер по сети.

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


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

Для сериализации и десериализации, наш объект в файл , мы должны вызов ObjectOutputStream.writeObject() и , ObjectInputStream.readObject() как это сделано в следующем коде:

Только классы, которые реализуют Serializable, могут быть сериализированы

Подобно интерфейсу Cloneable для клонирования Java в сериализации, у нас есть один интерфейс маркера, Serializable, который работает как флаг для JVM. Любой класс, который реализует Serializable интерфейс напрямую или через своего родителя, может быть сериализован, а классы, которые не реализуют, Serializable не могут быть сериализованы.

Процесс сериализации Java по умолчанию полностью рекурсивен, поэтому всякий раз , когда мы пытаемся сериализовать один объект, то попытка процесса сериализации сериализовать все поля (примитивные и ссылочные) с нашим классом ( за исключением static и transient полей).


Когда класс реализует
Serializable интерфейс, все его подклассы также сериализуются. Но когда объект имеет ссылку на другой объект, эти объекты должны реализовывать
Serializable интерфейс отдельно. Если у нашего класса есть хотя бы одна ссылка на не
Serializable класс, JVM сгенерирует
NotSerializableException .

Почему сериализуемый объект не реализуется объектом?

Теперь возникает вопрос: если Serialization является очень базовой функциональностью, и любой класс, который не реализует, Serializable не может быть сериализован, то почему Serializable не реализуется сам по Object себе? Таким образом, все наши объекты могут быть сериализованы по умолчанию.

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

Переходные и статические поля не сериализуются

Если мы хотим сериализовать один объект, но не хотим сериализовать определенные поля, мы можем пометить эти поля как временные .

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

  1. Сериализация не заботится о модификаторах доступа области, таких как private . Все непереходные и нестатические поля считаются частью постоянного состояния объекта и могут быть сериализованы.
  2. Мы можем присваивать значения конечным полям только в обработчиках, и процесс сериализации не вызывает никакого конструктора, но все же может присваивать значения конечным полям.

Что такое serialVersionUID? И почему мы должны это объявлять?

Предположим, у нас есть класс, и мы сериализовали его объект в файл на диске, и из-за некоторых новых требований мы добавили / удалили одно поле из нашего класса. Теперь, если мы попытаемся десериализовать уже сериализованный объект, мы получим InvalidClassException ; Почему?

Мы получаем его, потому что по умолчанию JVM связывает номер версии с каждым сериализуемым классом для управления версиями класса. Он используется для проверки того, что сериализованные и десериализованные объекты имеют одинаковые атрибуты и, следовательно, совместимы с десериализацией. Номер версии сохраняется в поле с именем serialVersionUID . Если сериализуемый класс не объявляет a serialVersionUID , JVM сгенерирует его автоматически во время выполнения.

Если мы изменим структуру нашего класса, например поля удаления / добавления, этот номер версии также изменится, и в соответствии с JVM наш класс не будет совместим с версией класса сериализованного объекта. Вот почему мы получаем исключение, но если вы действительно думаете об этом, почему оно должно быть выброшено только потому, что я добавил поле? Не может ли поле просто установить его значение по умолчанию, а затем записать в следующий раз?

Да, это можно сделать, предоставив serialVersionUID поле вручную и убедившись, что оно всегда одинаково. Настоятельно рекомендуется, чтобы каждый сериализуемый класс объявлял его serialVersionUID как сгенерированный класс зависимым от компилятора и, следовательно, может привести к непредвиденным последствиям . InvalidClassExceptions


Вы можете использовать утилиту, поставляемую с дистрибутивом JDK,
serialver для просмотра того, каким будет этот код по умолчанию (это просто хэш-код объекта по умолчанию).

Настройка сериализации и десериализации с помощью методов writeObject и readObject

JVM полностью контролирует сериализацию объекта в процессе сериализации по умолчанию, но есть много недостатков в использовании процесса сериализации по умолчанию, некоторые из которых:

  1. Он не может обработать сериализацию полей, которые не сериализуются.
  2. Процесс десериализации не вызывает конструкторов при создании объекта, поэтому он не может вызвать логику инициализации, предоставленную конструктором.

Но мы можем переопределить это поведение сериализации по умолчанию внутри нашего Java-класса и предоставить некоторую дополнительную логику для улучшения нормального процесса. Это можно сделать, предоставив два метода writeObject и readObject внутри класса, который мы хотим сериализовать:

Объявление обоих методов как закрытых необходимо (общедоступные методы не будут работать), поэтому, кроме JVM, ничто другое не сможет их увидеть. Это также доказывает, что ни один метод не наследуется, не переопределяется и не перегружается. JVM автоматически проверяет эти методы и вызывает их в процессе сериализации-десериализации. JVM может вызывать эти закрытые методы, но другие объекты не могут. Таким образом, целостность класса сохраняется, и протокол сериализации может продолжать работать в обычном режиме.

Несмотря на то, что эти специализированные частные методы предоставляются, сериализация объектов работает так же, вызывая ObjectOutputStream.writeObject() или ObjectInputStream.readObject() .

Вызов ObjectOutputStream.writeObject() или ObjectInputStream.readObject() запускает протокол сериализации. Сначала проверяется объект на предмет его реализации Serializable , а затем проверяется, предоставляется ли какой-либо из этих закрытых методов. Если они предоставляются, класс потока передается в качестве параметра этим методам, предоставляя коду контроль над его использованием.

Мы можем вызывать ObjectOutputStream.defaultWriteObject() и ObjectInputStream.defaultReadObject() из этих методов получить логику сериализации по умолчанию. Эти вызовы делают то, на что они похожи — они выполняют запись и чтение сериализованного объекта по умолчанию, что важно, потому что мы не заменяем обычный процесс; мы только добавляем к этому.

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

Остановка сериализации и десериализации

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

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

Чтобы остановить сериализацию для нашего класса, мы можем, опять же, использовать вышеупомянутые приватные методы, чтобы просто бросить NotSerializableException . Любая попытка сериализации или десериализации нашего объекта теперь всегда приводит к возникновению исключения. И поскольку эти методы объявлены как private , никто не может переопределить ваши методы и изменить их.

Однако это является нарушением принципа подстановки Лискова. И, writeReplace и readResolve методы могут быть использованы для достижения синглтоноподобного поведения. Эти методы используются, чтобы позволить объекту предоставить альтернативное представление для себя в ObjectStream . Проще говоря, readResolve может использоваться для изменения данных, десериализованных посредством readObject метода, и writeReplace может использоваться для изменения данных, которые сериализуются через writeObject .

Сериализация Java также может использоваться для глубокого клонирования объекта . Клонирование Java — самая дискуссионная тема в сообществе Java, и оно, безусловно, имеет свои недостатки, но это все еще самый популярный и простой способ создания копии объекта до тех пор, пока этот объект не заполнит обязательные условия клонирования Java. Я покрыл клонирование в деталях в 3 статье длиной Java Клонирование серии , которая включает в себя статью , такой как Java Клонирование и типы Cloning (поверхностная и глубокая) подробно на примере , Java Клонирование — Конструктор копирования Versus Cloning , Java Клонирование — Даже копировать конструктор не Достаточно , прочитайте их, если хотите узнать больше о клонировании.

Заключение

  1. Сериализация — это процесс сохранения состояния объекта в последовательность байтов, которая затем может быть сохранена в файле или отправлена ​​по сети, а десериализация — это процесс восстановления объекта из этих байтов.
  2. Только подклассы Serializable интерфейса могут быть сериализованы.
  3. Если наш класс не реализует Serializable интерфейс или если у него есть ссылка на не- Serializable класс, JVM сгенерирует NotSerializableException .
  4. Все transient и static поля не сериализуются.
  5. serialVersionUID Используются для проверки того, что сериализации и десериализации объекты имеют те же атрибуты , и , таким образом, совместимы с десериализацией.
  6. Мы должны создать serialVersionUID поле в нашем классе, поэтому, если мы изменим структуру нашего класса (добавление / удаление полей), JVM не будет проходить InvalidClassException . Если мы не предоставляем его, JVM предоставляет тот, который может измениться при изменении структуры нашего класса.
  7. Мы можем переопределить поведение сериализации по умолчанию внутри нашего Java-класса, предоставив реализацию writeObject и readObject методы.
  8. И мы можем назвать ObjectOutputStream.defaultWriteObject() и ObjectInputStream.defaultReadObject от writeObject и readObject методы получения сериализации по умолчанию и десериализации логике.
  9. Мы можем выбросить NotSerializableException исключение из writeObject и readObject , если мы не хотим, чтобы наш класс был сериализован или десериализован.

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

Вы можете найти полный исходный код для этой статьи в этом репозитории GitHub , и, пожалуйста, не стесняйтесь оставить свой ценный отзыв.

Интерфейс java.io.Serializable

При использовании Serializable применяется стандартный алгоритм сериализации, который с помощью рефлексии (Reflection API) выполняет

  • запись в поток метаданных о классе, ассоциированном с объектом (имя класса, идентификатор SerialVersionUID, идентификаторы полей класса),
  • рекурсивную запись в поток описания суперклассов до класса java.lang.Object (не включительно),
  • запись примитивных значений полей сериализуемого экземпляра, начиная с полей самого верхнего суперкласса,
  • рекурсивную запись объектов, которые являются полями сериализуемого объекта.

При этом ранее сериализованные объекты повторно не сериализуются, что позволяет алгоритму корректно работать с циклическими ссылками.

Для выполнения десериализации под объект выделяется память, после чего его поля заполняются значениями из потока. Конструктор объекта при этом не вызывается. Однако при десериализации будет вызван конструктор без параметров родительского несериализуемого класса, а его отсутствие повлечёт ошибку десериализации.

Интерфейс java.io.Externalizable

При реализации интерфейса Externalizable вызывается пользовательская логика сериализации. Способ сериализации и десериализации описывается в методах writeExternal и readExternal. Во время десериализации вызывается конструктор без параметров, а потом уже на созданном объекте вызывается метод readExternal.

Где используется сериализация Serializable?

Сериализация была введена в JDK 1.1 и позволяет преобразовать отдельный объект или группу объектов в поток битов для передачи по сети или для сохранения в файл. И как было сказано выше, данный массив байтов или поток битов, можно обратно преобразовать в объекты Java. Главным образом это происходит автоматически благодаря классам ObjectInputStream и ObjectOutputStream.

Класс сериализации Person

В следующем листинге показан класс Person, реализующий интерфейс Serializable.

Далее класс Person будет использоваться в примерах, чтобы показать дополнительные возможности, связанные с сериализацией Java-объектов.

Модификатор поля transient

Использование при описании поля класса модификатора transient позволяет исключить указанное поле из сериализации. Это бывает полезно для секретных (пароль) или не особо важных данных. Если, например, при описании объекта Person включить следующее поле address

то в результате сериализации и десериализации адрес объекта принимает значение по умолчанию или будет null.

Модификатор transient действует только на стандартный механизм сериализации Serializable. При использовании Externalizable никто не мешает сериализовать это поле, равно как и использовать его для определения других полей.

Модификатор поля static

При стандартной сериализации поля, имеющие модификатор static, не сериализуются. Соответственно, после десериализации это поле значения не меняет. При использовании реализации Externalizable сериализовать и десериализовать статическое поле можно, но не рекомендуется этого делать, т.к. это может сопровождаться трудноуловимыми ошибками.

Модификатор поля final

Поля с модификатором final сериализуются как и обычные. За одним исключением – их невозможно десериализовать при использовании Externalizable, поскольку final-поля должны быть инициализированы в конструкторе, а после этого в readExternal изменить значение этого поля будет невозможно. Соответственно, если необходимо сериализовать объект с final-полем неоходимо использовать только стандартную сериализацию.

Пример тестирования сериализации и десериализации

Для тестирования сериализации и десериализации объекта Person будем использовать юнит-тест JUnit, в котором создадим 2 объекта, запишем объекты в файл, после чего восстановим их. Более подробно об использовании JUnit сказано на странице Тестирование программы.

В примере ничего нового или удивительного не представлено – это основы сериализации, которые желательно знать, особенно при разработке WEB-приложений.

Переопределение сериализации в интерфейсе Serializable

Если у сериализуемого объекта реализован один из следующих методов, то механизм сериализации будет использовать его, а не метод по умолчанию :

  • writeObject - запись объекта в поток;
  • readObject - чтение объекта из потока;
  • writeReplace - позволяет заменить себя экземпляром другого класса перед записью;
  • readResolve - позволяет заменить на себя другой объект после чтения.

Ниже приводятся примеры использования данных методов.

Сериализация не безопасна

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

К счастью разработчиков Java Serialization API позволяет "вклиниться" в процесс сериализации, и изменить (или запутать) поля данных как перед сериализацией, так и после десериализации. Это можно сделать, определив методы writeObject, readObject объекта Serializable.

Изменение сериализованных данных, writeObject, readObject

Чтобы изменить процесс сериализации в классе Person реализуем метод writeObject. Для модифицирования процесса десериализации определим в том же классе метод readObject. При реализации этих методов необходимо корректно восстановить данные.

В данных методах значение возраста age просто умножается на 4 при записи объекта, а при восстановлении - делится на четыре. Алгоритм, конечно же можно усложнить; в примере он представлен только для демонстрации возможностей сериализации.

Сериализация и рефакторинг кода

Сериализация позволяет вносить небольшие изменения в структуру класса, так что даже после рефакторинга класс ObjectInputStream по-прежнему будет с ним прекрасно работать. К наиболее важным изменениям, с которыми спецификация Java Object Serialization может справляться автоматически:

  • добавление новых полей в класс;
  • изменение полей из статических в нестатические;
  • изменение полей из транзитных в нетранзитные.

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

Рефакторинг сериализованного класса

Для тестирования сериализации с измененной структурой класса Person на основе предыдущего примера необходимо проделать следующие предварительные шаги :

  1. Создать файл данных с использованием метода setUpBeforeClass класса JUnitPerson и блокировкой удаления файла в методе tearDownAfterClass; можно закоментировать строку удаления файла.
  2. Блокировать создание файла данных в методе setUpBeforeClass класса JUnitPerson при следующем выполнении; также можно закоментировать строки создание файла.
  3. Изменить код класса Person, как это представлено в следующем листинге.

Листинг изменений класса Person

Теперь можно выполнить тест testSerialization класса JUnitPerson и увидеть, что тест прошел успешно, т.е. класс ObjectInputStream прочитал данные и объект был восстановлен корректно. При желании можно в конце кода testSerialization вставить строку

чтобы убедиться, что значения объекта восстановлены правильно, и что параметр gender равен null.

Проверка десериализованного объекта, ObjectInputValidation и validateObject

Если есть необходимость выполнения контроля за значениями десериализованного/восстановленного объекта, то можно использовать интерфейс ObjectInputValidation с переопределением метода validateObject. В следующем листинге представлены изменения, которые следует внести в описание класса Person, чтобы контролировать возраст.

Если вызвать метод validateObject после десериализации объекта, то будет вызвано исключение InvalidObjectException при значении возраста за пределами 39. 60.

Подписывание сериализованных данных

Если требуется зашифровать и подписать объект, то проще всего поместить его в оберточный класс javax.crypto.SealedObject и/или java.security.SignedObject. Данные классы являются сериализуемыми, поэтому при оборачивании объекта в SealedObject создается подобие "подарочной упаковки" вокруг исходного объекта. Для шифрования необходимо создать симметричный ключ, управление которым должно осуществляться отдельно. Аналогично, для проверки данных можно использовать класс SignedObject, для работы с которым также нужен симметричный ключ, управляемый отдельно. Эти два объекта позволяют упаковывать и подписывать сериализованные данные, не отвлекаясь на детали проверки и шифрования цифровых подписей.

Листинг теста подписи объекта

Прокси-класс для сериализации

Листинг прокси-класса

В прокси-классе определим метод readResolve, которой будет вызываться во время десериализации объекта, чтобы вернуть объект-замену. Конструктор прокси-класса будет упаковывать объект PersonY во внутреннее поле data.

Класс PersonY создадим на основе базового класса Person с добавлением метода writeReplace следующего вида :

Вместе методы writeReplace и readResolve позволяют классу PersonY упаковывать все данные (или их наиболее важную часть) в объект класса PersonProxy, помещать его в поток и затем распаковать его при десериализации.

Пример тестирования прокси-класса JUnitPersonProxy

Особенности сериализации Date

Если объект сериализации включает поля типа Date, то здесь следует, при необходимости, учитывать временной сдвиг между различными регионами. В противном случае может возникнуть ситуация, при которой на сервере будет создан объект Date с одним временем в определенной TimeZone, а на клиенте он будет десериализован с другой TimeZone. В зависимости от времени создания игнорирование TimeZone может привести к изменению даты. Решение данной проблемы представлено на странице описания TimeZone.

Использование сериализации

Технология RMI (Java Remote Method Invocation), построенная на сериализации, позволяет java-приложению, запущенному на одной виртуальной машине, вызвать методы объекта, работающего на другой виртуальной машине JVM (Java Virtual Machine).

Исходный код рассмотренного примера Serialization в виде проекта Eclipse можно скачать здесь (15.0 Kб).

Однако в некоторых случаях может понадобиться вмешаться в процесс конструирования дерева и процесс сериализации. Например, может существовать бизнес-правило, которое гласит, что все поля данных должны сохраняться в определенном формате, или же необходимо добавить дополнительные данные в поток, которые напрямую не отображаются на поля сохраняемого объекта (временные метки, уникальные идентификаторы и т.п.).

ISerializable Этот интерфейс может быть реализован на типе [Serializable] для управления его сериализацией и десериализацией
ObjectIDGenerator Этот тип генерирует идентификаторы для членов графа объектов
[OnDeserialized] Этот атрибут позволяет указать метод, который будет вызван немедленно после десериализации объекта
[OnDeserializing] Этот атрибут позволяет указать метод, который будет вызван перед процессом десериализации
[OnSerialized] Этот атрибут позволяет указать метод, который будет вызван немедленно после того, как объект сериализован
[OnSerializing] Этот атрибут позволяет указать метод, который будет вызван перед процессом сериализации
[OptionalField] Этот атрибут позволяет определить поле типа, которое может быть пропущено в указанном потоке
SerializationInfo По существу это "мешок свойств", который поддерживает пары "имя/значение", представляющие состояние объекта во время процесса сериализации

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

полностью квалифицированное имя объекта в графе (например, МуАрр.JamesBondCar);

имя сборки, определяющей граф объектов (например, МуАрр.ехе);

экземпляр класса SerializationInfo, содержащего все данные состояния, которые поддерживаются членами графа объектов.

Во время процесса десериализации BinaryFormatter использует ту же информацию для построения идентичной копии объекта с применением информации, извлеченной из потока-источника. Процесс, выполняемый SoapFormatter, очень похож.

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

Проверка пометки объекта атрибутом [Serializable]. Если объект не помечен, генерируется исключение SerializationException.

Если объект помечен атрибутом [Serializable], выполняется проверка, реализует ли объект интерфейс ISerializable. Если да, то вызывается метод GetObjectData() на этом объекте.

Если объект не реализует интерфейс ISerializable, используется процесс сериализации по умолчанию, который обрабатывает все поля, не помеченные [NonSerialized].

В дополнение к определению того, поддерживает ли тип ISerializable, форматеры также отвечают за исследование типов на предмет поддержки членов, которые оснащены атрибутами [OnSerializing], [OnSerialized], [OnDeserializing] или [OnDeserialized]. Мы рассмотрим назначение этих атрибутов чуть позже, а сначала давайте посмотрим на предназначение ISerializable.

Настройка сериализации с использованием интерфейса ISerializable

Объекты, которые помечены атрибутом [Serializable], имеют опцию реализации интерфейса ISerializable. Реализация этого интерфейса позволяет вмешаться в процесс сериализации и выполнить необходимое форматирование данных до и после сериализации.

Метод GetObjectData() вызывается автоматически заданным форматером во время процесса сериализации. Реализация этого метода заполняет входной параметр SerializationInfo последовательностью пар "имя/значение", которые (обычно) отображают данные полей сохраняемого объекта. В SerializationInfo определены многочисленные вариации перегруженного метода AddValue(), а также небольшой набор свойств, которые позволяют устанавливать и получать имя типа, определять сборку и счетчик членов.

Типы, реализующие интерфейс ISerializable, также должны определять специальный конструктор со следующей сигнатурой:

Обратите внимание, что видимость конструктора указана как protected. Это приемлемо, учитывая, что форматер будет иметь доступ к этому члену независимо от его видимости. Такие специальные конструкторы обычно делают protected (или private), тем самым гарантируя, что небрежный пользователь объекта никогда не создаст объект подобным образом. Как видите, первым параметром конструктора является экземпляр типа SerializationInfo.

Второй параметр этого специального конструктора имеет тип StreamingContext и содержит информацию относительно источника или места назначения передаваемых данных. Наиболее информативным членом StreamingContext является свойство State, представляющее значение из перечисления StreamingContextStates.

Настройка сериализации с использованием атрибутов

В случае применения этих атрибутов метод должен быть определен так, чтобы принимать параметр StreamingContext и не возвращать ничего (иначе будет сгенерировано исключение времени выполнения). Обратите внимание, что применять каждый из атрибутов сериализации не обязательно, а можно просто вмешаться в те стадии процесса сериализации, которые интересуют. Для иллюстрации ниже приведен новый тип [Serializable]:

Выполнив сериализацию этого нового типа, вы обнаружите, что данные сохраняются в верхнем регистре, а десериализуются — в нижнем.

Читайте также: