В Java может появиться новая сериализация

в 16:41, , рубрики: java, serialization, Программирование

На сайте OpenJDK появился новый исследовательский документ, в котором описывается идея введения в язык новой улучшенной сериализации взамен старой.

Сериализация в Java существует с версии 1.1, то есть практически с момента её рождения. С одной стороны, сериализация является очень удобным механизмом, который позволяет быстро и просто сделать любой класс сериализуемым посредством наследования этого класса от интерфейса java.io.Serializable. Возможно даже, эта простота стала одной из ключевых причин, почему Java набрала такую огромную популярность в мире, ведь она позволила быстро и эффективно писать сетевые приложения.

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

Что не так с сериализацией в Java? Перечислим наиболее серьёзные проблемы:

  • Сериализация (и десериализация) происходит в обход языковых механизмов. Она игнорирует модификаторы доступа полей (private, protected) и создаёт объекты, не используя конструкторы, а значит игнорирует инварианты, которые могут присутствовать в этих конструкторах. Такую уязвимость может использовать злоумышленник, подменив данные на невалидные, и они успешно «проглотятся» при десериализации.
  • При написании сериализуемых классов никак не помогает компилятор и не обнаруживает ошибки. Например, вы не можете статически гарантировать, что все поля сериализуемого класса сами являются сериализуемыми. Или можете опечататься в имени методов readObject, writeObject, readResolve и т.д., и тогда эти методы просто не будут использоваться во время сериализации.
  • Сериализация не поддерживает нормального механизма версионирования, поэтому очень сложно изменять сериализуемые классы так, чтобы они оставались совместимыми с их старыми версиями.
  • Сериализация сильно завязана на потоковое кодирование/декодирование, а значит поменять формат кодирования на отличный от стандартного очень сложно. Кроме того, стандартный формат не является ни компактным, ни эффективным и ни человекочитаемым.

Фундаментальная ошибка существующей сериализации в Java заключается в том, что она пытается быть слишком «невидимой» для программиста. Он просто наследуется от java.io.Serializable и получает некую неявную магию, которая выполняется платформой.
Наоборот, программист должен явно писать конструкции, отвечающие за конструирование и деконструирование объектов. Эти конструкции должны быть на уровне языка и должны быть написаны посредством статического доступа к полям, а не рефлексии.

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

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

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

public class Range {
    int lo;
    int hi;
  
    private Range(int lo, int hi) {
        if (lo > hi)
            throw new IllegalArgumentException(String.format("(%d,%d)",
                                                             lo, hi));
        this.lo = lo;
        this.hi = hi;
    }
 
    @Serializer
    public pattern Range(int lo, int hi) { 
        lo = this.lo;
        hi = this.hi;
    }
    
    @Deserializer
    public static Range make(int lo, int hi) {
        return new Range(lo, hi);
    }
}

В этом примере объявлен класс Range, который готов к сериализации посредством двух специальных членов класса: сериализатора и десериализатора помеченных аннотациями @Serializer и @Deserializer. Сериализатор реализован через деконструктор паттерна, а десериализатор – через статический метод, в котором вызывается конструктор. Таким образом, при десериализации неминуемо проверяется инвариант hi >= lo, указанный в конструкторе.
В таком подходе нет никакой магии, и используются обычные аннотации, поэтому сериализацию может делать любой фреймворк, а не только сама платформа Java. Это значит, что формат кодирования может быть также абсолютно любым (бинарный, XML, JSON, YAML и т.д.).

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

Версионирование в таком подходе реализуется с помощью специального поля version у аннотаций @Serializer и @Deserializer:

class C {
    int a;
    int b;
    int c;
   
    @Deserializer(version = 3)
    public C(int a, int b, int c) { 
        this a = a;
        this.b = b;
        this.c = c;
    }
    
    @Deserializer(version = 2)
    public C(int a, int b) { 
        this(a, b, 0);
    }

    @Deserializer(version = 1)
    public C(int a) { 
        this(a, 0, 0);
    }
    
    @Serializer(version = 3)
    public pattern C(int a, int b, int c) {
        a = this.a;
        b = this.b;
        c = this.c;
    }
}

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

class Foo {
    private final InternalState is;

    public Foo(ExternalState es) {
        this(new InternalState(es));
    }
    
    @Deserializer
    private open Foo(InternalState is) { 
        this.is = is;
    }
    
    @Serializer
    private open pattern serialize(InternalState is) { 
        is = this.is;
    }
}

Здесь сериализаторы и десериализаторы помечены ключевым словом open, что делает их открытыми для setAccessible.

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

P.S. Друзья, если вы хотите получать подобные новости о Java более быстро и удобно, то подписывайтесь на мой канал в Telegram.

Автор: John Kozlov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js