Сериализация в Java – сериализация Java

Сериализация в Java была введена в JDK 1.1 и является одной из важных функций Core Java.

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

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

  1. Serializable в Java
  2. Рефакторинг классов с сериализацией и serialVersionUID
  3. Интерфейс Java Externalizable
  4. Методы сериализации в Java
  5. Сериализация с наследованием
  6. Шаблон сериализации Proxy

Сериализация в 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+"}";
	}
	
	//методы получения и установки
	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 bean с некоторыми свойствами и методами getter-setter. Если вы не хотите, чтобы свойство объекта сериализовывалось в поток, вы можете использовать ключевое слово transient, как я сделал с переменной salary. Теперь предположим, что мы хотим записать наши объекты в файл, а затем десериализовать их из того же файла. Нам нужны вспомогательные методы, которые будут использовать 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 {

	// десериализация в объект из указанного файла
	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;
	}

	// сериализация указанного объекта и сохранение его в файл
	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);
		
		// сериализация в файл
		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);
		
	}
}

Теперь раскомментируйте переменную “password” и методы ее получения-установки из класса 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 использует переменные класса, методы, имя класса, пакет и т. д. для генерации этого уникального длинного числа. Если вы работаете с какой-либо средой разработки, вы автоматически получите предупреждение, что “Сериализуемый класс Employee не объявляет поле serialVersionUID статического финального типа long”. Мы можем использовать утилиту java “serialver” для генерации serialVersionUID класса; для класса Employee мы можем запустить ее с помощью следующей команды.

SerializationExample/bin$serialver -classpath . com.journaldev.serialization.Employee

Обратите внимание, что не обязательно, чтобы версия сериала генерировалась из этой программы самостоятельно, мы можем присвоить это значение так, как хотим. Главное, чтобы оно было там, чтобы процесс десериализации знал, что новый класс – это новая версия того же класса и его следует десериализовать, если это возможно. Например, раскомментируйте только поле serialVersionUID из класса Employee и запустите программу SerializationTest. Теперь раскомментируйте поле пароля из класса Employee и запустите программу DeserializationTest, и вы увидите, что поток объектов успешно десериализован, потому что изменение в классе Employee совместимо с процессом сериализации.

Интерфейс Java Externalizable

Если вы обратите внимание на процесс сериализации Java, он происходит автоматически. Иногда мы хотим скрыть данные объекта, чтобы сохранить их целостность. Мы можем сделать это, реализовав интерфейс java.io.Externalizable и предоставив реализацию методов writeExternal() и 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();
		// читать в том же порядке, что и записывать
		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? На самом деле, лучше использовать интерфейс Serializable, и к концу статьи вы узнаете почему.

Методы сериализации Java

Мы видели, что сериализация в Java происходит автоматически, и все, что нам нужно сделать, это реализовать интерфейс Serializable. Реализация присутствует в классах ObjectInputStream и ObjectOutputStream. Но что, если мы хотим изменить способ сохранения данных, например, если у нас есть чувствительная информация в объекте, и перед сохранением/восстановлением мы хотим её зашифровать/расшифровать? Именно поэтому существуют четыре метода, которые мы можем предоставить в классе для изменения поведения сериализации. Если эти методы присутствуют в классе, они используются в целях сериализации.

  1. readObject(ObjectInputStream ois): Если этот метод присутствует в классе, метод readObject() из ObjectInputStream будет использовать этот метод для чтения объекта из потока.
  2. writeObject(ObjectOutputStream oos): Если этот метод присутствует в классе, метод writeObject() из ObjectOutputStream будет использовать этот метод для записи объекта в поток. Одним из распространенных применений является затемнение переменных объекта для поддержания целостности данных.
  3. Object writeReplace(): Если этот метод присутствует, то после процесса сериализации вызывается этот метод, и объект, который он возвращает, сериализуется в поток.
  4. Object readResolve(): Если этот метод присутствует, то после процесса десериализации вызывается этот метод для возврата конечного объекта программе вызывающего. Одним из применений этого метода является реализация паттерна Singleton с сериализуемыми классами. Подробнее см. в Serialization and Singleton.

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

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

Иногда нам нужно расширить класс, который не реализует интерфейс Serializable. Если мы полагаемся на автоматическое поведение сериализации, и у родительского класса есть некоторое состояние, то оно не будет преобразовано в поток данных и, следовательно, не будет восстановлено позднее. Вот место, где методы 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;
	}	
}

SuperClass – это простой бин Java, но он не реализует интерфейс Serializable.

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()+"}";
	}
	
	// добавление вспомогательного метода для сериализации для сохранения/инициализации состояния суперкласса
	private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
		ois.defaultReadObject();
		
		// обратите внимание на то, что порядок чтения и записи должен быть одинаковым
		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 {
		// проверяем объект здесь
		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}

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

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

Сериализация в 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+"}";
	}
	
	//класс сериализации прокси
	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){
			//затемняем данные для безопасности
			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");
		}
		
	}
	
	//заменяем сериализованный объект объектом DataProxy
	private Object writeReplace(){
		return new DataProxy(this);
	}
	
	private void readObject(ObjectInputStream ois) throws InvalidObjectException{
		throw new InvalidObjectException("Proxy is not used, something fishy");
	}
}
  • Оба класса Data и DataProxy должны реализовывать интерфейс Serializable.
  • DataProxy должен быть способен поддерживать состояние объекта Data.
  • DataProxy – это внутренний приватный статический класс, поэтому к нему нельзя обратиться из других классов.
  • DataProxy должен иметь единственный конструктор, принимающий Data в качестве аргумента.
  • Класс Data должен предоставлять метод writeReplace(), возвращающий экземпляр DataProxy. Таким образом, когда объект Data сериализуется, возвращаемый поток представляет собой класс DataProxy. Однако класс DataProxy не виден снаружи, поэтому его нельзя использовать напрямую.
  • Класс DataProxy должен реализовывать метод readResolve(), возвращающий объект Data. Таким образом, когда класс Data десериализуется, внутренне десериализуется класс DataProxy, и при вызове его метода readResolve() мы получаем объект Data.
  • Наконец, реализуйте метод readObject() в классе Data и выбросьте исключение InvalidObjectException, чтобы предотвратить атаки хакеров, пытающихся подделать поток объектов Data и его разбор.

Давайте напишем небольшой тест, чтобы проверить, работает ли реализация или нет.

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.ser, вы увидите, что объект DataProxy сохранен в файле как поток.

Загрузите проект по сериализации Java

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

Source:
https://www.digitalocean.com/community/tutorials/serialization-in-java