Поиск по сайту:

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


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

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

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

Сериализуемый в Java

Если вы хотите, чтобы объект класса был сериализуемым, все, что вам нужно сделать, это реализовать интерфейс java.io.Serializable. Serializable в java представляет собой интерфейс маркера и не имеет полей или методов для реализации. Это похоже на процесс Opt-In, с помощью которого мы делаем наши классы сериализуемыми. Сериализация в 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();
	}

}

Обратите внимание, что аргументы метода работают с Object, который является базовым классом любого объекта 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);
		
	}
}

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

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

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

Внешний интерфейс Java

Если вы заметили процесс сериализации 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();
		//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. На самом деле лучше использовать интерфейс Serializable, и к тому времени, как мы дойдем до конца статьи, вы поймете, почему.

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

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

  1. readObject(ObjectInputStream ois): если этот метод присутствует в классе, метод ObjectInputStream readObject() будет использовать этот метод для чтения объекта из потока.
  2. writeObject(ObjectOutputStream oos): если этот метод присутствует в классе, метод ObjectOutputStream writeObject() будет использовать этот метод для записи объекта в поток. Одним из распространенных способов использования является скрытие объектных переменных для сохранения целостности данных.
  3. Объект writeReplace(): если этот метод присутствует, то после процесса сериализации вызывается этот метод, и возвращаемый объект сериализуется в поток.
  4. Объект readResolve(): если этот метод присутствует, то после процесса десериализации этот метод вызывается для возврата конечного объекта вызывающей программе. Одним из применений этого метода является реализация шаблона 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()+"}";
	}
	
	//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}

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

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

Сериализация в java сопряжена с некоторыми серьезными ловушками, такими как;

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

Шаблон Java Serialization Proxy — это способ добиться большей безопасности с помощью сериализации. В этом шаблоне внутренний закрытый статический класс используется в качестве прокси-класса для целей сериализации. Этот класс разработан таким образом, чтобы поддерживать состояние основного класса. Этот шаблон реализуется путем правильной реализации методов 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");
	}
}

  • Оба класса Data и DataProxy должны реализовывать сериализуемый интерфейс.
  • DataProxy должен поддерживать состояние объекта данных.
  • DataProxy — это внутренний закрытый статический класс, поэтому другие классы не могут получить к нему доступ.
  • DataProxy должен иметь единственный конструктор, который принимает данные в качестве аргумента.
  • Класс
  • Data должен предоставлять метод writeReplace(), возвращающий экземпляр DataProxy. Поэтому, когда объект данных сериализуется, возвращаемый поток относится к классу DataProxy. Однако класс DataProxy не виден снаружи, поэтому его нельзя использовать напрямую.
  • Класс
  • DataProxy должен реализовать метод readResolve(), возвращающий объект Data. Таким образом, когда класс Data десериализуется, внутренне десериализуется DataProxy, и когда вызывается его метод readResolve(), мы получаем объект Data.
  • Наконец, внедрите метод readObject() в класс Data и выдайте 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.ser, вы увидите, что объект DataProxy сохраняется как поток в файле.

Скачать проект сериализации Java

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