Автор оригинала: Pankaj Kumar.
Сериализация в Java была введена в JDK 1.1, и это одна из важных функций Core Java .
Сериализация на Java
Сериализация в Java позволяет нам преобразовать объект в поток, который мы можем отправить по сети или сохранить в виде файла или сохранить в БД для последующего использования. Десериализация-это процесс преобразования потока объектов в реальный объект Java, который будет использоваться в нашей программе. Сериализация на Java поначалу кажется очень простой в использовании, но она сопряжена с некоторыми тривиальными проблемами безопасности и целостности, которые мы рассмотрим в более поздней части этой статьи. В этом уроке мы рассмотрим следующие темы.
- Сериализуемый в Java
- Рефакторинг классов с сериализацией и serialVersionUID
- Внешний интерфейс Java
- Методы сериализации Java
- Сериализация с наследованием
- Шаблон прокси-сервера Сериализации
Сериализуемый в Java
Если вы хотите, чтобы объект класса был сериализуемым, все, что вам нужно сделать, это реализовать интерфейс java.io.Serializable
. Сериализуемый в java является интерфейсом маркера и не имеет полей или методов для реализации. Это похоже на процесс регистрации, с помощью которого мы делаем наши классы сериализуемыми.
Сериализация в java реализована ObjectInputStream
и ObjectOutputStream
, поэтому все, что нам нужно, – это обернуть их, чтобы либо сохранить в файл, либо отправить по сети. Давайте рассмотрим простую сериализацию в примере программы java.
package com.journaldev.serialization; import java.io.Serializable; public class Employee implements Serializable { // private static final long serialVersionUID = -6470090944414208496L; private String name; private int id; transient private int salary; // private String password; @Override public String toString(){ return "Employee{name="+name+",id="+id+",salary="+salary+"}"; } //getter and setter methods public String getName() { return name; } public void setName(String name) { this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public int getSalary() { return salary; } public void setSalary(int salary) { this.salary = salary; } // public String getPassword() { // return password; // } // // public void setPassword(String password) { // this.password = password; // } }
Обратите внимание, что это простой java-компонент с некоторыми свойствами и методами получения и установки. Если вы хотите, чтобы свойство объекта не сериализовывалось для потоковой передачи, вы можете использовать ключевое слово transient , как я сделал с переменной зарплаты.
Теперь предположим, что мы хотим записать наши объекты в файл, а затем десериализовать их из того же файла. Поэтому нам нужны служебные методы, которые будут использовать ObjectInputStream
и ObjectOutputStream
для целей сериализации.
package com.journaldev.serialization; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; /** * A simple class with generic serialize and deserialize method implementations * * @author pankaj * */ public class SerializationUtil { // deserialize to Object from given file public static Object deserialize(String fileName) throws IOException, ClassNotFoundException { FileInputStream fis = new FileInputStream(fileName); ObjectInputStream ois = new ObjectInputStream(fis); Object obj = ois.readObject(); ois.close(); return obj; } // serialize the given object and save it to file public static void serialize(Object obj, String fileName) throws IOException { FileOutputStream fos = new FileOutputStream(fileName); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(obj); fos.close(); } }
Обратите внимание, что аргументы метода работают с объектом, который является базовым классом любого объекта java. Это написано таким образом, чтобы быть общим по своей природе.
Теперь давайте напишем тестовую программу, чтобы увидеть сериализацию Java в действии.
package com.journaldev.serialization; import java.io.IOException; public class SerializationTest { public static void main(String[] args) { String fileName="employee.ser"; Employee emp = new Employee(); emp.setId(100); emp.setName("Pankaj"); emp.setSalary(5000); //serialize to file try { SerializationUtil.serialize(emp, fileName); } catch (IOException e) { e.printStackTrace(); return; } Employee empNew = null; try { empNew = (Employee) SerializationUtil.deserialize(fileName); } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); } System.out.println("emp Object::"+emp); System.out.println("empNew Object::"+empNew); } }
Когда мы запускаем вышеуказанную тестовую программу для сериализации на java, мы получаем следующий вывод.
emp Object::Employee{name=Pankaj,id=100,salary=5000} empNew Object::Employee{name=Pankaj,id=100,salary=0}
Поскольку зарплата является переходной переменной, ее значение не было сохранено в файл и, следовательно, не было извлечено в новом объекте. Аналогично статические значения переменных также не сериализуются, поскольку они принадлежат классу, а не объекту.
Рефакторинг классов с сериализацией и serialVersionUID
Сериализация в java допускает некоторые изменения в классе java, если их можно игнорировать. Некоторые изменения в классе, которые не повлияют на процесс десериализации, включают:
- Добавление новых переменных в класс
- Изменение переменных с переходных на непереходные, для сериализации это похоже на создание нового поля.
- Изменение переменной со статической на нестатическую для сериализации похоже на создание нового поля.
Но для того, чтобы все эти изменения работали, класс java должен иметь serialVersionUID , определенный для класса. Давайте напишем тестовый класс только для десериализации уже сериализованного файла из предыдущего тестового класса.
package com.journaldev.serialization; import java.io.IOException; public class DeserializationTest { public static void main(String[] args) { String fileName="employee.ser"; Employee empNew = null; try { empNew = (Employee) SerializationUtil.deserialize(fileName); } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); } System.out.println("empNew Object::"+empNew); } }
Теперь раскомментируйте переменную “пароль” и ее методы getter-setter из класса Employee и запустите ее. Вы получите ниже исключение;
java.io.InvalidClassException: com.journaldev.serialization.Employee; local class incompatible: stream classdesc serialVersionUID = -6470090944414208496, local class serialVersionUID = -6234198221249432383 at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:604) at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1601) at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1514) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1750) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1347) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:369) at com.journaldev.serialization.SerializationUtil.deserialize(SerializationUtil.java:22) at com.journaldev.serialization.DeserializationTest.main(DeserializationTest.java:13) empNew Object::null
Причина ясна, что serialVersionUID предыдущего класса и нового класса отличаются. На самом деле, если класс не определяет serialVersionUID, он автоматически вычисляется и присваивается классу. Java использует переменные класса, методы, имя класса, пакет и т.д. Для создания этого уникального длинного числа. Если вы работаете с какой-либо средой разработки, вы автоматически получите предупреждение о том, что “Сотрудник сериализуемого класса не объявляет статическое поле final serialVersionUID типа long”.
Мы можем использовать утилиту java “serialver” для генерации идентификатора класса serialVersionUID, для класса Employee мы можем запустить его с помощью команды ниже.
SerializationExample/bin$serialver -classpath . com.journaldev.serialization.Employee
Обратите внимание, что не требуется, чтобы серийная версия создавалась из самой этой программы, мы можем присвоить это значение по своему усмотрению. Он просто должен быть там, чтобы процесс десериализации знал, что новый класс является новой версией того же класса и должен быть десериализован из возможных.
Например, раскомментируйте только поле serialVersionUID из класса Сотрудник
и запустите программу Тест сериализации
. Теперь раскомментируйте поле пароля из класса Employee и запустите программу DeserializationTest
, и вы увидите, что поток объектов успешно десериализован, поскольку изменение класса Employee совместимо с процессом сериализации.
Внешний интерфейс Java
Если вы заметили процесс сериализации java, это делается автоматически. Иногда мы хотим скрыть данные объекта, чтобы сохранить его целостность. Мы можем сделать это, реализовав интерфейс java.io.Externalizable
и обеспечив реализацию методов write External() и readExternal () , которые будут использоваться в процессе сериализации.
package com.journaldev.externalization; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; public class Person implements Externalizable{ private int id; private String name; private String gender; @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(id); out.writeObject(name+"xyz"); out.writeObject("abc"+gender); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { id=in.readInt(); //read in the same order as written name=(String) in.readObject(); if(!name.endsWith("xyz")) throw new IOException("corrupted data"); name=name.substring(0, name.length()-3); gender=(String) in.readObject(); if(!gender.startsWith("abc")) throw new IOException("corrupted data"); gender=gender.substring(3); } @Override public String toString(){ return "Person{id="+id+",name="+name+",gender="+gender+"}"; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } }
Обратите внимание, что я изменил значения полей перед преобразованием их в поток, а затем во время чтения изменил изменения. Таким образом, мы можем поддерживать некоторую целостность данных. Мы можем создать исключение, если после считывания потоковых данных проверка целостности завершится неудачей. Давайте напишем тестовую программу, чтобы увидеть ее в действии.
package com.journaldev.externalization; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class ExternalizationTest { public static void main(String[] args) { String fileName = "person.ser"; Person person = new Person(); person.setId(1); person.setName("Pankaj"); person.setGender("Male"); try { FileOutputStream fos = new FileOutputStream(fileName); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(person); oos.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } FileInputStream fis; try { fis = new FileInputStream(fileName); ObjectInputStream ois = new ObjectInputStream(fis); Person p = (Person)ois.readObject(); ois.close(); System.out.println("Person Object Read="+p); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } }
Когда мы запускаем вышеуказанную программу, мы получаем следующий вывод.
Person Object Read=Person{id=1,name=Pankaj,gender=Male}
Итак, какой из них лучше использовать для сериализации в java. На самом деле лучше использовать сериализуемый интерфейс, и к тому времени, когда мы дойдем до конца статьи, вы поймете, почему.
Методы сериализации Java
Мы видели, что сериализация в java происходит автоматически, и все, что нам нужно, – это реализация сериализуемого интерфейса. Реализация присутствует в классах ObjectInputStream и ObjectOutputStream. Но что, если мы хотим изменить способ сохранения данных, например, у нас есть некоторая конфиденциальная информация в объекте, и перед сохранением/извлечением мы хотим зашифровать/расшифровать ее. Вот почему в классе есть четыре метода, которые мы можем предоставить для изменения поведения сериализации.
Если эти методы присутствуют в классе, они используются для целей сериализации.
- readObject(ois ObjectInputStream) : Если этот метод присутствует в классе, метод ObjectInputStream readObject() будет использовать этот метод для чтения объекта из потока.
- writeObject(oos ObjectOutputStream) : Если этот метод присутствует в классе, метод ObjectOutputStream writeObject() будет использовать этот метод для записи объекта в поток. Одним из распространенных способов использования является скрытие переменных объекта для поддержания целостности данных.
- Object writeReplace() : Если этот метод присутствует, то после процесса сериализации этот метод вызывается и возвращаемый объект сериализуется в поток.
- Объект readResolve() : Если этот метод присутствует, то после процесса десериализации этот метод вызывается для возврата конечного объекта вызывающей программе. Одним из способов использования этого метода является реализация одноэлементного шаблона с сериализованными классами. Подробнее читайте в Сериализация и синглтон .
Обычно при реализации вышеуказанных методов он сохраняется как закрытый, чтобы подклассы не могли их переопределить. Они предназначены только для целей сериализации, и сохранение их в тайне позволяет избежать каких-либо проблем с безопасностью.
Сериализация с наследованием
Иногда нам нужно расширить класс, который не реализует сериализуемый интерфейс. Если мы полагаемся на поведение автоматической сериализации и суперкласс имеет некоторое состояние, то они не будут преобразованы в поток и, следовательно, не будут извлечены позже.
Это одно место, где действительно помогают методы readObject() и writeObject (). Предоставляя их реализацию, мы можем сохранить состояние суперкласса в потоке, а затем получить его позже. Давайте посмотрим на это в действии.
package com.journaldev.serialization.inheritance; public class SuperClass { private int id; private String value; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } }
СуперКласс-это простой компонент java, но он не реализует сериализуемый интерфейс.
package com.journaldev.serialization.inheritance; import java.io.IOException; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectInputValidation; import java.io.ObjectOutputStream; import java.io.Serializable; public class SubClass extends SuperClass implements Serializable, ObjectInputValidation{ private static final long serialVersionUID = -1322322139926390329L; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString(){ return "SubClass{id="+getId()+",value="+getValue()+",name="+getName()+"}"; } //adding helper method for serialization to save/initialize super class state private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{ ois.defaultReadObject(); //notice the order of read and write should be same setId(ois.readInt()); setValue((String) ois.readObject()); } private void writeObject(ObjectOutputStream oos) throws IOException{ oos.defaultWriteObject(); oos.writeInt(getId()); oos.writeObject(getValue()); } @Override public void validateObject() throws InvalidObjectException { //validate the object here if(name == null || "".equals(name)) throw new InvalidObjectException("name can't be null or empty"); if(getId() <=0) throw new InvalidObjectException("ID can't be negative or zero"); } }
Обратите внимание, что порядок записи и чтения дополнительных данных в поток должен быть одинаковым. Мы можем вложить некоторую логику в чтение и запись данных, чтобы сделать их безопасными.
Также обратите внимание, что класс реализует ObjectInputValidation
интерфейс. Реализуя метод validateObject () , мы можем провести некоторые бизнес-проверки, чтобы убедиться, что целостность данных не нарушена.
Давайте напишем тестовый класс и посмотрим, сможем ли мы извлечь состояние суперкласса из сериализованных данных или нет.
package com.journaldev.serialization.inheritance; import java.io.IOException; import com.journaldev.serialization.SerializationUtil; public class InheritanceSerializationTest { public static void main(String[] args) { String fileName = "subclass.ser"; SubClass subClass = new SubClass(); subClass.setId(10); subClass.setValue("Data"); subClass.setName("Pankaj"); try { SerializationUtil.serialize(subClass, fileName); } catch (IOException e) { e.printStackTrace(); return; } try { SubClass subNew = (SubClass) SerializationUtil.deserialize(fileName); System.out.println("SubClass read = "+subNew); } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); } } }
Когда мы запускаем выше класса, мы получаем следующий результат.
SubClass read = SubClass{id=10,value=Data,name=Pankaj}
Таким образом, таким образом, мы можем сериализовать состояние суперкласса, даже если он не реализует сериализуемый интерфейс. Эта стратегия удобна, когда суперкласс является сторонним классом, который мы не можем изменить.
Шаблон прокси-сервера Сериализации
Сериализация в java сопряжена с некоторыми серьезными подводными камнями, такими как;
- Структура классов не может быть сильно изменена без нарушения процесса сериализации java. Поэтому, хотя в дальнейшем нам не понадобятся некоторые переменные, нам нужно сохранить их только для обратной совместимости.
- Сериализация создает огромные риски для безопасности, злоумышленник может изменить последовательность потоков и нанести вред системе. Например, роль пользователя сериализуется, и злоумышленник изменяет значение потока, чтобы сделать его администратором и запустить вредоносный код.
Шаблон прокси-сервера сериализации Java-это способ повысить безопасность с помощью сериализации. В этом шаблоне внутренний закрытый статический класс используется в качестве прокси-класса для целей сериализации. Этот класс разработан таким образом, чтобы поддерживать состояние основного класса. Этот шаблон реализуется путем правильной реализации методов readResolve() и writeReplace () .
Давайте сначала напишем класс, который реализует шаблон прокси-сервера сериализации, а затем проанализируем его для лучшего понимания.
package com.journaldev.serialization.proxy; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.Serializable; public class Data implements Serializable{ private static final long serialVersionUID = 2087368867376448459L; private String data; public Data(String d){ this.data=d; } public String getData() { return data; } public void setData(String data) { this.data = data; } @Override public String toString(){ return "Data{data="+data+"}"; } //serialization proxy class private static class DataProxy implements Serializable{ private static final long serialVersionUID = 8333905273185436744L; private String dataProxy; private static final String PREFIX = "ABC"; private static final String SUFFIX = "DEFG"; public DataProxy(Data d){ //obscuring data for security this.dataProxy = PREFIX + d.data + SUFFIX; } private Object readResolve() throws InvalidObjectException { if(dataProxy.startsWith(PREFIX) && dataProxy.endsWith(SUFFIX)){ return new Data(dataProxy.substring(3, dataProxy.length() -4)); }else throw new InvalidObjectException("data corrupted"); } } //replacing serialized object to DataProxy object private Object writeReplace(){ return new DataProxy(this); } private void readObject(ObjectInputStream ois) throws InvalidObjectException{ throw new InvalidObjectException("Proxy is not used, something fishy"); } }
- Как
Данные
, так иПрокси-сервер данных
класс должны реализовывать сериализуемый интерфейс. Прокси-сервер данных
должен поддерживать состояние объекта данных.Прокси-сервер данных
является внутренним частным статическим классом, поэтому другие классы не могут получить к нему доступ.Прокси-сервер данных
должен иметь один конструктор, который принимает данные в качестве аргумента.Данные
класс должен предоставить writeReplace() метод, возвращающийПрокси-сервер данных
экземпляр. Поэтому, когда объект данных сериализуется, возвращаемый поток относится к классу DataProxy. Однако класс DataProxy не виден снаружи, поэтому его нельзя использовать напрямую.Прокси-сервер данных
класс должен реализовать readResolve() метод, возвращающийДанные
объект. Поэтому, когда класс данных десериализуется, внутренне десериализуется DataProxy, и когда вызывается метод readResolve (), мы получаем объект данных.- Наконец, реализуйте readObject() метод в классе данных и бросьте
Исключение InvalidObjectException
чтобы избежать атаки хакеров, пытающихся создать поток объектов данных и проанализировать его.
Давайте напишем небольшой тест, чтобы проверить, работает ли реализация или нет.
package com.journaldev.serialization.proxy; import java.io.IOException; import com.journaldev.serialization.SerializationUtil; public class SerializationProxyTest { public static void main(String[] args) { String fileName = "data.ser"; Data data = new Data("Pankaj"); try { SerializationUtil.serialize(data, fileName); } catch (IOException e) { e.printStackTrace(); } try { Data newData = (Data) SerializationUtil.deserialize(fileName); System.out.println(newData); } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); } } }
Когда мы запускаем выше класса, мы получаем результат ниже в консоли.
Data{data=Pankaj}
Если вы откроете файл data.set, вы увидите, что объект прокси-сервера данных сохранен в файле в виде потока.
Это все для сериализации в Java, это выглядит просто, но мы должны использовать это разумно, и всегда лучше не полагаться на реализацию по умолчанию. Скачайте проект по ссылке выше и поиграйте с ним, чтобы узнать больше.