Статьи

Магические методы сериализации Java и их использование на примере

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

Все подтипы сериализуемого класса сами по себе сериализуемы и
Интерфейс Externalizable также расширяет Serializable. Так что даже если мы
настроить наш процесс сериализации с помощью Externalizable наш класс по-прежнему
Serializable .

Serializable интерфейс — это маркерный интерфейс, который не имеет методов или полей и работает как флаг для JVM. Процесс сериализации Java, предоставляемый классами ObjectInputStream и ObjectOutputStream , полностью контролируется JVM.

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

методы writeObject и readObject

Сериализуемые классы, которые хотят настроить или добавить некоторую дополнительную логику для улучшения нормального процесса сериализации / десериализации
writeObject и readObject с такими точными сигнатурами:

  • private void writeObject(java.io.ObjectOutputStream out) throws IOException
  • private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException

Эти методы уже подробно обсуждаются в статье « Все, что нужно знать о сериализации Java» .

метод readObjectNoData

Как описано в документации Java класса Serializable , если мы хотим инициализировать состояние объекта для его конкретного класса в случае, если поток сериализации не перечисляет данный класс как суперкласс десериализованного объекта, то мы должны предоставить writeObject и методы readObject с этими точными сигнатурами:

  • private void readObjectNoData() throws ObjectStreamException

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

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

методы writeReplace и readResolve

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

  • ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException

И Serializable классы, которые должны назначить замену, когда экземпляр этого читается из потока, должны предоставить этому специальному методу точную подпись:

  • ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException

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

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


Вы можете прочитать больше о клонировании и сериализации Java на Java Cloning и
Темы сериализации Java .

Метод readResolve вызывается после readObject (и наоборот, writeReplace вызывается перед writeObject и, возможно, для другого объекта). Объект, который возвращает метод, заменяет this объект, возвращенный пользователю ObjectInputStream.readObject и любые дальнейшие обратные ссылки на объект в потоке. Мы можем использовать метод writeReplace для замены объекта сериализации на null, чтобы ничего не было сериализовано, а затем использовать метод readResolve для замены десериализованного объекта экземпляром singleton.

метод validateObject

Если мы хотим выполнить определенные проверки для некоторых из наших полей, мы можем сделать это путем реализации интерфейса ObjectInputValidation и переопределения
Метод validateObject из него.

Метод validateObject будет автоматически вызван, когда мы зарегистрируем эту проверку, вызвав ObjectInputStream.registerValidation(this, 0) из метода readObject . Очень полезно проверить, что поток не был подделан, или что данные имеют смысл, прежде чем передать их обратно в ваше приложение.

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

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
public class SerializationMethodsExample {
 
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Employee emp = new Employee("Naresh Joshi", 25);
        System.out.println("Object before serialization: " + emp.toString());
 
        // Serialization
        serialize(emp);
 
        // Deserialization
        Employee deserialisedEmp = deserialize();
        System.out.println("Object after deserialization: " + deserialisedEmp.toString());
 
 
        System.out.println();
 
        // This will print false because both object are separate
        System.out.println(emp == deserialisedEmp);
 
        System.out.println();
 
        // This will print false because both `deserialisedEmp` and `emp` are pointing to same object,
        // Because we replaced de-serializing object in readResolve method by current instance
        System.out.println(Objects.equals(emp, deserialisedEmp));
    }
 
    // Serialization code
    static void serialize(Employee empObj) throws IOException {
        try (FileOutputStream fos = new FileOutputStream("data.obj");
             ObjectOutputStream oos = new ObjectOutputStream(fos))
        {
            oos.writeObject(empObj);
        }
    }
 
    // Deserialization code
    static Employee deserialize() throws IOException, ClassNotFoundException {
        try (FileInputStream fis = new FileInputStream("data.obj");
             ObjectInputStream ois = new ObjectInputStream(fis))
        {
            return (Employee) ois.readObject();
        }
    }
}
 
class Employee implements Serializable, ObjectInputValidation {
    private static final long serialVersionUID = 2L;
 
    private String name;
    private int age;
 
    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    // With ObjectInputValidation interface we get a validateObject method where we can do our validations.
    @Override
    public void validateObject() {
        System.out.println("Validating age.");
 
        if (age < 18 || age > 70)
        {
            throw new IllegalArgumentException("Not a valid age to create an employee");
        }
    }
 
    // Custom serialization logic,
    // This will allow us to have additional serialization logic on top of the default one e.g. encrypting object before serialization.
    private void writeObject(ObjectOutputStream oos) throws IOException {
        System.out.println("Custom serialization logic invoked.");
        oos.defaultWriteObject(); // Calling the default serialization logic
    }
 
    // Replacing de-serializing object with this,
    private Object writeReplace() throws ObjectStreamException {
        System.out.println("Replacing serialising object by this.");
        return this;
    }
 
    // Custom deserialization logic
    // This will allow us to have additional deserialization logic on top of the default one e.g. performing validations, decrypting object after deserialization.
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        System.out.println("Custom deserialization logic invoked.");
 
        ois.registerValidation(this, 0); // Registering validations, So our validateObject method can be called.
 
        ois.defaultReadObject(); // Calling the default deserialization logic.
    }
 
    // Replacing de-serializing object with this,
    // It will will not give us a full proof singleton but it will stop new object creation by deserialization.
    private Object readResolve() throws ObjectStreamException {
        System.out.println("Replacing de-serializing object by this.");
        return this;
    }
 
    @Override
    public String toString() {
        return String.format("Employee {name='%s', age='%s'}", name, age);
    }
}

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

Опубликовано на Java Code Geeks с разрешения Нареша Джоши, партнера нашей программы JCG . Смотрите оригинальную статью здесь: магические методы сериализации Java и их использование на примере

Мнения, высказанные участниками Java Code Geeks, являются их собственными.