Статьи

Шаблон прокси-сервер сериализации

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

обзор

В посте основное внимание уделяется представлению подробного определения шаблона, а затем приведению двух коротких примеров и, наконец, рассмотрению всех за и против.

Насколько я знаю, модель впервые была определена в превосходной книге Джошуа Блоха « Эффективная Ява» (1-е издание: пункт 57; 2-е издание: пункт 78 ). Этот пост в основном повторяет то, что там сказано.

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

Шаблон сериализации прокси

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

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

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

сериализация прокси-модель

Цель состоит в том, чтобы спроектировать прокси таким образом, чтобы это было наилучшее логическое представление исходного класса.

Реализация

SerializationProxy — это статический вложенный класс исходного класса. Все его поля являются окончательными, и его единственный конструктор имеет оригинальный экземпляр в качестве единственного аргумента. Он извлекает логическое представление состояния этого экземпляра и присваивает его своим собственным полям. Поскольку исходный экземпляр считается «безопасным», нет необходимости в проверках согласованности или в защитном копировании.

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

Сериализация

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

Замена исходного экземпляра прокси

1
2
3
private Object writeReplace() {
    return new SerializationProxy(this);
}

десериализации

При десериализации этот перевод из оригинала в экземпляр прокси должен быть инвертирован. Это реализовано в следующем методе в SerializationProxy , который вызывается после успешной десериализации экземпляра прокси:

Перевод прокси обратно в исходный экземпляр

1
2
3
4
private Object readResolve() {
    // create an instance of the original class
    // in the state defined by the proxy's fields
}

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

Искусственный поток байтов

Из-за writeReplace обычные байтовые потоки будут содержать только кодировки прокси. Но то же самое не относится к искусственным потокам ! Они могут содержать кодировки оригинальных экземпляров и, поскольку их десериализация не охватывается шаблоном, она не обеспечивает никаких гарантий для этого случая.

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

Предотвратить прямую десериализацию оригинальных экземпляров

1
2
3
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
    throw new InvalidObjectException("Proxy required.");
}

Примеры

Следующие примеры являются выдержками из полного демонстрационного проекта . Они показывают только сочные части и writeReplace некоторые детали (например, writeReplace и readObject ).

Комплексное число

Простой случай — это тип неизменяемого для комплексных чисел , называемый ComplexNumber (сюрприз!). В этом примере он хранит координаты, а также полярную форму в своих полях (предположительно по соображениям производительности):

ComplexNumber — поля

1
2
3
4
private final double real;
private final double imaginary;
private final double magnitude;
private final double angle;

Прокси-сервер сериализации выглядит так:

ComplexNumber.SerializationProxy

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
private static class SerializationProxy implements Serializable {
 
    private final double real;
    private final double imaginary;
 
    public SerializationProxy(ComplexNumber complexNumber) {
        this.real = complexNumber.real;
        this.imaginary = complexNumber.imaginary;
    }
 
    /**
     * After the proxy is deserialized, it invokes a static factory method
     * to create a 'ComplexNumber' "the regular way".
     */
    private Object readResolve() {
        return ComplexNumber.fromCoordinates(real, imaginary);
    }
}

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

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

InstanceCache

InstanceCache — это гетерогенный безопасный для типов контейнер, который использует карту классов и их экземпляров в качестве вспомогательной структуры данных:

InstanceCache — Поля

1
private final ConcurrentMap<Class<?>, Object> cacheMap;

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

InstanceCache.SerializationProxy

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static class SerializationProxy implements Serializable {
 
    // array lists are serializable
    private final ArrayList<Serializable> serializableInstances;
 
    public SerializationProxy(InstanceCache cache) {
        serializableInstances = extractSerializableValues(cache);
    }
 
    private static ArrayList<Serializable> extractSerializableValues(
            InstanceCache cache) {
 
        return cache.cacheMap.values().stream()
                .filter(instance -> instance instanceof Serializable)
                .map(instance -> (Serializable) instance)
                .collect(Collectors.toCollection(ArrayList::new));
    }
 
    /**
     * After the proxy is deserialized, it invokes a constructor to create
     * an 'InstanceCache' "the regular way".
     */
    private Object readResolve() {
        return new InstanceCache(serializableInstances);
    }
 
}

Плюсы и минусы

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

Pros

Вот эти преимущества:

Уменьшенный экстралингвистический характер

Центральное преимущество шаблона состоит в том, что он уменьшает экстралингвистический характер сериализации. Это в основном достигается за счет использования открытого API класса для создания экземпляров (см. SerializationProxy.readResolve выше). Следовательно, каждое создание экземпляра проходит через конструктор (ы), и всегда выполняется весь код, необходимый для правильной инициализации экземпляра.

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

Нет ограничений на финальные поля

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

Гибкая реализация

На самом деле для readResolve прокси не readResolve возвращать экземпляр того же типа, что был сериализован. Он также может вернуть любой подкласс.

Блох приводит следующий пример:

Рассмотрим случай EnumSet . Этот класс не имеет открытых конструкторов, только статические фабрики. С точки зрения клиента они возвращают экземпляры EnumSet , фактически они возвращают один из двух подклассов, в зависимости от размера базового типа перечисления. Если базовый тип перечисления имеет шестьдесят четыре или менее элементов, статические фабрики возвращают RegularEnumSet ; в противном случае они возвращают JumboEnumSet .

Теперь рассмотрим, что произойдет, если вы сериализуете набор перечислений, тип перечисления которых имеет шестьдесят элементов, затем добавите еще пять элементов к типу перечисления и затем десериализуете набор перечислений. Это был экземпляр RegularEnumSet когда он был сериализован, но он должен быть экземпляром JumboEnumSet после его десериализации.

Эффективная Java, 2-е издание: с. 314

Прокси-шаблон делает это тривиальным: readResolve просто возвращает экземпляр соответствующего типа. (Это работает только тогда, когда типы соответствуют принципу подстановки Лискова .)

Более высокая безопасность

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

Соответствует принципу единой ответственности

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

Как обычно для SRP , это значительно улучшает читабельность. Все поведение в отношении сериализации можно найти в одном месте. И сериализованную форму также намного легче обнаружить, так как в большинстве случаев достаточно просто посмотреть на поля прокси.

Cons

Джошуа Блох описывает некоторые ограничения модели.

Не подходит для наследования

Он не совместим с классами, которые расширяются их клиентами.

Эффективная Java, 2-е издание: с. 315

Да, вот и все. Никаких дальнейших комментариев. Я не совсем понимаю этот момент, но я узнаю больше …

Возможные проблемы с круговыми графами объектов

Он несовместим с некоторыми классами, графы объектов которых содержат округлости: если вы попытаетесь вызвать метод объекта из метода readResolve прокси-сервера сериализации, вы получите ClassCastException , поскольку у вас еще нет этого объекта, только его Сериализация прокси.

Эффективная Java, 2-е издание: с. 315

Производительность

Прокси добавляет выполнение конструктора к сериализации и десериализации. Блох приводит пример, где это было на 14 процентов дороже на его машине. Это, конечно, не точное измерение, но подтверждает теорию, что эти вызовы конструктора не бесплатны.

отражение

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

Последнее слово от Джошуа Блоха:

Итак, рассмотрите шаблон прокси сериализации всякий раз, когда вам приходится писать readObject или writeObjet [для настраиваемой сериализованной формы] в классе, который не расширяется его клиентами. Этот шаблон, возможно, является самым простым способом надежной сериализации объектов с нетривиальными инвариантами.

Эффективная Java, 2-е издание: с. 315

Ссылка: Шаблон прокси- сервера сериализации от нашего партнера JCG Николая Парлога в блоге CodeFx .