Serialization in Java

Serialization in Java ermöglicht es uns, ein Objekt in einen Stream umzuwandeln, den wir über das Netzwerk senden, als Datei speichern oder in einer Datenbank für späteren Gebrauch aufbewahren können. Deserialisierung ist der Prozess der Umwandlung eines Objektstreams in ein tatsächliches Java-Objekt, das in unserem Programm verwendet wird. Serialization in Java scheint zunächst sehr einfach zu verwenden, bringt aber einige triviale Sicherheits- und Integritätsprobleme mit sich, die wir im späteren Teil dieses Artikels betrachten werden.

Serializable in Java

Wenn Sie ein Klassenobjekt serialisierbar machen möchten, müssen Sie lediglich das Interface java.io.Serializable implementieren. Serializable in Java ist ein Marker-Interface und hat keine zu implementierenden Felder oder Methoden. Es ist wie ein Opt-In-Prozess, durch den wir unsere Klassen serialisierbar machen. Serialization in Java wird durch ObjectInputStream und ObjectOutputStream implementiert, daher brauchen wir nur einen Wrapper über diese, um sie entweder als Datei zu speichern oder über das Netzwerk zu senden. Lassen Sie uns ein einfaches Beispielprogramm zur Serialization in Java sehen.

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;
//	}
	
}

Beachten Sie, dass es sich um eine einfache Java-Bean mit einigen Eigenschaften und Getter-Setter-Methoden handelt. Wenn Sie eine Objekteigenschaft nicht in den Stream serialisieren möchten, können Sie das Schlüsselwort transient verwenden, wie ich es bei der Gehaltsvariablen getan habe. Nehmen wir an, wir möchten unsere Objekte in eine Datei schreiben und dann aus derselben Datei deserialisieren. Daher benötigen wir Hilfsmethoden, die ObjectInputStream und ObjectOutputStream für Serialisierungszwecke verwenden.

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();
	}

}

Beachten Sie, dass die Methodenargumente mit Object arbeiten, das die Basisklasse jedes Java-Objekts ist. Es ist so geschrieben, um generisch zu sein. Schreiben wir jetzt ein Testprogramm, um die Java-Serialization in Aktion zu sehen.

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);
	}
}


Wenn wir das obige Testprogramm für die Serialization in Java ausführen, erhalten wir folgende Ausgabe.

emp Object::Employee{name=Pankaj,id=100,salary=5000}
empNew Object::Employee{name=Pankaj,id=100,salary=0}

Da Gehalt eine transiente Variable ist, wurde ihr Wert nicht in die Datei gespeichert und daher nicht im neuen Objekt abgerufen. Ebenso werden statische Variablenwerte nicht serialisiert, da sie zur Klasse und nicht zum Objekt gehören.

Klassenrefaktorierung mit Serialization in Java und serialVersionUID

Serialization in Java ermöglicht einige Änderungen in der Java-Klasse, wenn sie ignoriert werden können. Einige der Änderungen in der Klasse, die den Deserialisierungsprozess nicht beeinflussen, sind:

  • Hinzufügen neuer Variablen zur Klasse
  • Ändern der Variablen von transient zu nicht-transient, für die Serialisierung ist es wie das Hinzufügen eines neuen Feldes.
  • Ändern der Variablen von statisch zu nicht-statisch, für die Serialisierung ist es wie das Hinzufügen eines neuen Feldes.

Aber damit all diese Änderungen funktionieren, sollte die Java-Klasse eine serialVersionUID für die Klasse definiert haben. Schreiben wir eine Testklasse nur zur Deserialisierung der bereits serialisierten Datei aus der vorherigen Testklasse.

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);
		
	}
}

Jetzt heben Sie die Kommentierung der „password“-Variable und ihrer Getter-Setter-Methoden aus der Employee-Klasse auf und führen sie aus. Sie erhalten die folgende Ausnahme;

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

Der Grund ist klar, dass die serialVersionUID der vorherigen Klasse und der neuen Klasse unterschiedlich sind. Tatsächlich, wenn die Klasse serialVersionUID nicht definiert, wird sie automatisch berechnet und der Klasse zugewiesen. Java verwendet Klassenvariablen, Methoden, Klassennamen, Pakete usw., um diese einzigartige lange Nummer zu generieren. Wenn Sie mit einer IDE arbeiten, erhalten Sie automatisch eine Warnung, dass „Die serialisierbare Klasse Employee kein statisches finales serialVersionUID-Feld vom Typ long deklariert“. Wir können das Java-Utility „serialver“ verwenden, um die serialVersionUID der Klasse zu generieren, für die Employee-Klasse können wir es mit dem folgenden Befehl ausführen.

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

Beachten Sie, dass die serialisierte Version nicht unbedingt von diesem Programm selbst generiert werden muss, wir können diesen Wert zuweisen, wie wir möchten. Sie muss nur vorhanden sein, damit der Deserialisierungsprozess weiß, dass die neue Klasse die neue Version der gleichen Klasse ist und, wenn möglich, deserialisiert werden sollte. Entfernen Sie zum Beispiel nur das serialVersionUID-Feld aus der Employee-Klasse und führen Sie das SerializationTest-Programm aus. Entfernen Sie nun das Passwortfeld aus der Employee-Klasse und führen Sie das DeserializationTest-Programm aus und Sie werden sehen, dass der Objektstrom erfolgreich deserialisiert wird, weil die Änderung in der Employee-Klasse mit dem Serialisierungsprozess kompatibel ist.

Java Externalizable Interface

Wenn Sie den Java-Serialisierungsprozess beachten, wird er automatisch durchgeführt. Manchmal möchten wir die Objektdaten verbergen, um deren Integrität zu bewahren. Dies können wir tun, indem wir das java.io.Externalizable-Interface implementieren und Implementierungen der Methoden writeExternal() und readExternal() für den Serialisierungsprozess bereitstellen.

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;
	}

}

Beachten Sie, dass ich die Feldwerte geändert habe, bevor ich sie in einen Stream umwandelte, und dann beim Lesen die Änderungen rückgängig gemacht habe. Auf diese Weise können wir eine gewisse Art von Datenintegrität aufrechterhalten. Wir können eine Ausnahme auslösen, wenn nach dem Lesen der Stream-Daten die Integritätsprüfungen fehlschlagen. Schreiben wir ein Testprogramm, um es in Aktion zu sehen.

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();
		}
	    
	}
}


Wenn wir das obige Programm ausführen, erhalten wir die folgende Ausgabe.

Person Object Read=Person{id=1,name=Pankaj,gender=Male}

Welche ist also besser für die Serialisierung in Java zu verwenden? Tatsächlich ist es besser, das Serializable-Interface zu verwenden, und bis zum Ende des Artikels werden Sie wissen, warum.

Java Serialization Methods

Wir haben gesehen, dass die Serialisierung in Java automatisch erfolgt und alles, was wir tun müssen, ist das Serializable-Interface zu implementieren. Die Implementierung ist in den Klassen ObjectInputStream und ObjectOutputStream vorhanden. Aber was, wenn wir die Art und Weise, wie wir Daten speichern, ändern möchten, zum Beispiel wenn wir einige sensible Informationen im Objekt haben und vor dem Speichern/Abfragen verschlüsseln/entschlüsseln möchten. Deshalb gibt es vier Methoden, die wir in der Klasse bereitstellen können, um das Serialisierungsverhalten zu ändern. Wenn diese Methoden in der Klasse vorhanden sind, werden sie für Serialisierungszwecke verwendet.

  • readObject(ObjectInputStream ois): Wenn diese Methode in der Klasse vorhanden ist, wird die Methode readObject() von ObjectInputStream diese Methode verwenden, um das Objekt aus dem Stream zu lesen.
  • writeObject(ObjectOutputStream oos): Wenn diese Methode in der Klasse vorhanden ist, wird die Methode writeObject() von ObjectOutputStream diese Methode verwenden, um das Objekt in den Stream zu schreiben. Eine der häufigsten Verwendungen ist das Verschleiern der Objektvariablen, um die Datenintegrität zu wahren.
  • Object writeReplace(): Wenn diese Methode vorhanden ist, wird sie nach dem Serialisierungsprozess aufgerufen und das zurückgegebene Objekt wird in den Stream serialisiert.
  • Object readResolve(): Wenn diese Methode vorhanden ist, wird sie nach dem Deserialisierungsprozess aufgerufen, um das endgültige Objekt an das aufrufende Programm zurückzugeben. Eine der Verwendungen dieser Methode ist die Implementierung des Singleton-Musters mit serialisierten Klassen. Lesen Sie mehr unter Serialization und Singleton.

Normalerweise werden diese Methoden als privat deklariert, so dass Unterklassen sie nicht überschreiben können. Sie sind nur für Serialisierungszwecke gedacht und ihr privater Status vermeidet Sicherheitsprobleme.

Serialization in Java mit Inheritance

Manchmal müssen wir eine Klasse erweitern, die das Serializable-Interface nicht implementiert. Wenn wir uns auf das automatische Serialisierungsverhalten verlassen und die Superklasse einen Zustand hat, dann werden diese nicht in den Stream konvertiert und daher später nicht abgerufen. Hier helfen die Methoden readObject() und writeObject() wirklich. Durch ihre Implementierung können wir den Zustand der Superklasse in den Stream speichern und später wieder abrufen. Lassen Sie uns dies in Aktion sehen.

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 ist ein einfaches Java-Bean, implementiert aber nicht das Serializable-Interface.

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");
	}	
}

Beachten Sie, dass die Reihenfolge des Schreibens und Lesens der zusätzlichen Daten im Stream gleich sein sollte. Wir können einige Logik in das Lesen und Schreiben von Daten einbringen, um es sicher zu machen. Beachten Sie auch, dass die Klasse das ObjectInputValidation-Interface implementiert. Durch die Implementierung der Methode validateObject() können wir einige Geschäftsvalidierungen durchführen, um sicherzustellen, dass die Datenintegrität nicht beeinträchtigt wird. Lassen Sie uns eine Testklasse schreiben und sehen, ob wir den Zustand der Superklasse aus serialisierten Daten abrufen können oder nicht.

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();
		}
	}
}

Wenn wir die obige Klasse ausführen, erhalten wir folgende Ausgabe.

SubClass read = SubClass{id=10,value=Data,name=Pankaj}

Auf diese Weise können wir den Zustand der Superklasse serialisieren, auch wenn sie das Serializable-Interface nicht implementiert. Diese Strategie ist praktisch, wenn die Superklasse eine Drittanbieterklasse ist, die wir nicht ändern können.

Serialization Proxy Pattern

Serialization in Java kommt mit einigen ernsthaften Fallstricken, wie zum Beispiel:

  • Die Klassenstruktur kann nicht stark verändert werden, ohne den Java Serialization-Prozess zu unterbrechen. Daher müssen wir, auch wenn wir einige Variablen später nicht benötigen, diese nur aus Gründen der Rückwärtskompatibilität beibehalten.
  • Serialization in Java birgt große Sicherheitsrisiken, ein Angreifer kann die Stream-Sequenz ändern und dem System schaden. Zum Beispiel wird die Benutzerrolle serialisiert und ein Angreifer ändert den Stream-Wert, um ihn zu admin zu machen und schädlichen Code auszuführen.

Das Java Serialization Proxy-Muster ist ein Weg, um größere Sicherheit mit Serialization in Java zu erreichen. In diesem Muster wird eine innere private statische Klasse als Proxy-Klasse für Serialisierungszwecke verwendet. Diese Klasse ist so gestaltet, dass sie den Zustand der Hauptklasse beibehält. Dieses Muster wird implementiert, indem die Methoden readResolve() und writeReplace() richtig implementiert werden. Lassen Sie uns zuerst eine Klasse schreiben, die das Serialization Proxy-Muster implementiert, und dann werden wir es für ein besseres Verständnis analysieren.

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");
	}
}

  • Sowohl die Data- als auch die DataProxy-Klasse sollten das Serializable-Interface implementieren.
  • DataProxy sollte in der Lage sein, den Zustand des Data-Objekts aufrechtzuerhalten.
  • DataProxy ist eine innere private statische Klasse, sodass andere Klassen nicht darauf zugreifen können.
  • DataProxy sollte einen einzigen Konstruktor haben, der Data als Argument nimmt.
  • Die Data-Klasse sollte die Methode writeReplace() bereitstellen, die eine DataProxy-Instanz zurückgibt. Wenn also ein Data-Objekt serialisiert wird, ist der zurückgegebene Stream von der DataProxy-Klasse. Die DataProxy-Klasse ist jedoch von außen nicht sichtbar, sodass sie nicht direkt verwendet werden kann.
  • Die DataProxy-Klasse sollte die Methode readResolve() implementieren, die ein Data-Objekt zurückgibt. Wenn also die Data-Klasse deserialisiert wird, wird intern DataProxy deserialisiert und wenn dessen readResolve()-Methode aufgerufen wird, erhalten wir ein Data-Objekt.
  • Implementieren Sie schließlich die Methode readObject() in der Data-Klasse und werfen Sie InvalidObjectException, um Hackerangriffe zu vermeiden, die versuchen, den Data-Objektstream zu fälschen und zu parsen.

Lassen Sie uns einen kleinen Test schreiben, um zu überprüfen, ob die Implementierung funktioniert oder nicht.

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();
		}
	}

}

Wenn wir die oben genannte Klasse ausführen, erhalten wir den folgenden Output in der Konsole.

Das ist alles zur Serialization in Java, es sieht einfach aus, aber wir sollten sie bedacht einsetzen und es ist immer besser, sich nicht auf die Standardimplementierung zu verlassen. Laden Sie das Projekt vom oben genannten Link herunter und experimentieren Sie damit, um mehr zu lernen.

Kostenlosen Account erstellen

Registrieren Sie sich jetzt und erhalten Sie Zugang zu unseren Cloud Produkten.

Das könnte Sie auch interessieren:

centron Managed Cloud Hosting in Deutschland

Java-Array: So prüfst du Werte effizient

JavaScript
Wie prüft man, ob ein Java Array einen Wert enthält? Es gibt viele Möglichkeiten, um zu überprüfen, ob ein Java Array einen bestimmten Wert enthält. Einfache Iteration mit einer for-Schleife…