Serialisierung in Java wurde in JDK 1.1 eingeführt und ist eine der wichtigen Funktionen von Core Java.
Serialisierung in Java
Serialisierung in Java ermöglicht es uns, ein Objekt in einen Stream umzuwandeln, den wir über das Netzwerk senden oder als Datei speichern oder für spätere Verwendung in einer Datenbank speichern können. Deserialisierung ist der Prozess, einen Objektstrom in ein tatsächliches Java-Objekt umzuwandeln, das in unserem Programm verwendet werden soll. Die Serialisierung in Java scheint anfangs sehr einfach zu sein, aber sie birgt einige triviale Sicherheits- und Integritätsprobleme, die wir später in diesem Artikel betrachten werden. Wir werden in diesem Tutorial folgende Themen behandeln.
- Serializable in Java
- Klassenumbau mit Serialisierung und serialVersionUID
- Java Externalizable Interface
- Java Serialisierungsmethoden
- Serialisierung mit Vererbung
- Serialisierung Proxy Pattern
Serializable in Java
Wenn Sie möchten, dass eine Klassenobjekt serialisierbar ist, müssen Sie lediglich das Interface java.io.Serializable
implementieren. Serializable in Java ist ein Marker-Interface und verfügt über keine Felder oder Methoden, die implementiert werden müssen. Es handelt sich um einen Opt-In-Prozess, durch den wir unsere Klassen serialisierbar machen. Die Serialisierung in Java wird von ObjectInputStream
und ObjectOutputStream
implementiert. Daher benötigen wir lediglich einen Wrapper über ihnen, um sie entweder in einer Datei zu speichern oder über das Netzwerk zu senden. Sehen wir uns ein einfaches Beispiel für die Serialisierung in Java an.
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- und Setter-Methoden
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 möchten, dass eine Objekteigenschaft nicht in den Stream serialisiert wird, können Sie das Schlüsselwort transient verwenden, wie ich es mit der Gehaltsvariable getan habe. Nehmen wir nun an, wir möchten unsere Objekte in einer Datei speichern und sie dann aus derselben Datei deserialisieren. Dafür benötigen wir Hilfsmethoden, die ObjectInputStream
und ObjectOutputStream
für die 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 {
// Deserialisieren zu Objekt aus der angegebenen Datei
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;
}
// Serialisieren des gegebenen Objekts und Speichern in einer Datei
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 einem Objekt arbeiten, das die Basisklasse jedes Java-Objekts ist. Es ist so geschrieben, dass es generisch ist. Jetzt schreiben wir ein Testprogramm, um die Java-Serialisierung 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);
// Serialisieren in eine Datei
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 zur Serialisierung 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 das Gehalt eine transient Variable ist, wurde sein Wert nicht in die Datei gespeichert und daher nicht im neuen Objekt abgerufen. Ebenso werden statische Variablenwerte nicht serialisiert, da sie zur Klasse gehören und nicht zum Objekt.
Klassenumbau mit Serialisierung und serialVersionUID
Die Serialisierung in Java erlaubt einige Änderungen an 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 Variable von statisch zu nicht-statisch, für die Serialisierung ist es wie das Hinzufügen eines neuen Feldes.
Aber für all diese Änderungen, um zu funktionieren, sollte die Java-Klasse die serialVersionUID für die Klasse definiert haben. Lassen Sie uns eine Testklasse nur für die Deserialisierung der bereits serialisierten Datei aus der vorherigen Testklasse schreiben.
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);
}
}
Entkommentieren Sie jetzt die Variable „password“ und ihre Getter-Setter-Methoden aus der Klasse „Employee“ und führen Sie sie aus. Sie werden untenstehende Ausnahme erhalten;
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 serialVersionUID der vorherigen Klasse und der neuen Klasse unterschiedlich sind. Tatsächlich, wenn die Klasse serialVersionUID nicht definiert, wird es automatisch berechnet und der Klasse zugewiesen. Java verwendet Klassenvariablen, Methoden, Klassenname, Paket usw., um diese eindeutige lange Nummer zu generieren. Wenn Sie mit einer IDE arbeiten, erhalten Sie automatisch eine Warnung, dass „Die serialisierbare Klasse Employee kein statisches final serialVersionUID-Feld vom Typ long deklariert“. Wir können das Java-Hilfsprogramm „serialver“ verwenden, um die serialVersionUID der Klasse zu generieren. Für die Klasse Employee können wir es mit folgendem Befehl ausführen.
SerializationExample/bin$serialver -classpath . com.journaldev.serialization.Employee
Beachten Sie, dass es nicht erforderlich ist, dass die Seriennummer selbst aus diesem Programm generiert wird. Wir können diesen Wert so zuweisen, wie wir möchten. Es muss nur vorhanden sein, um dem Deserialisierungsprozess mitzuteilen, dass die neue Klasse die neue Version derselben Klasse ist und wenn möglich deserialisiert werden sollte. Entkommentieren Sie beispielsweise nur das serialVersionUID-Feld aus der Employee
-Klasse und führen Sie das Programm SerializationTest
aus. Entkommentieren Sie jetzt das Passwortfeld aus der Employee-Klasse und führen Sie das Programm DeserializationTest
aus, und Sie werden sehen, dass der Objekt-Stream erfolgreich deserialisiert wird, da die Änderung in der Employee-Klasse mit dem Serialisierungsprozess kompatibel ist.
Java Externalizable Interface
Wenn Sie den Java-Serialisierungsprozess bemerken, wird er automatisch durchgeführt. Manchmal möchten wir die Objektdaten verschleiern, um ihre Integrität zu erhalten. Dies können wir erreichen, indem wir das java.io.Externalizable
-Interface implementieren und die Implementierung der Methoden writeExternal() und readExternal() bereitstellen, die im Serialisierungsprozess verwendet werden sollen.
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();
// in der gleichen Reihenfolge wie geschrieben einlesen
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 konvertiert habe, und dann beim Lesen die Änderungen umgekehrt habe. Auf diese Weise können wir die Datenintegrität in gewisser Weise aufrechterhalten. Wir können eine Ausnahme werfen, wenn nach dem Lesen der Streamdaten 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-generierter 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 folgende Ausgabe.
Person Object Read=Person{id=1,name=Pankaj,gender=Male}
Also, welche Methode ist besser für die Serialisierung in Java zu verwenden? Eigentlich ist es besser, das Serializable-Interface zu verwenden, und bis zum Ende des Artikels werden Sie wissen, warum.
Java Serialisierungsmethoden
Wir haben festgestellt, dass die Serialisierung in Java automatisch erfolgt und alles, was wir benötigen, die Implementierung des Serializable-Interfaces ist. Die Umsetzung erfolgt in den Klassen ObjectInputStream und ObjectOutputStream. Aber was ist, wenn wir die Art und Weise ändern möchten, wie wir Daten speichern? Zum Beispiel haben wir einige sensitive Informationen im Objekt, und bevor wir es speichern/abrufen, möchten wir es verschlüsseln/entschlüsseln. Aus diesem Grund 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 ObjectInputStream readObject() diese Methode zum Lesen des Objekts aus dem Stream verwenden.
- writeObject(ObjectOutputStream oos): Wenn diese Methode in der Klasse vorhanden ist, wird die Methode ObjectOutputStream writeObject() diese Methode zum Schreiben des Objekts in den Stream verwenden. Eine häufige Verwendung besteht darin, die Objektvariablen zu verschleiern, um die Datenintegrität zu gewährleisten.
- 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 Verwendung dieser Methode besteht darin, das Singleton-Muster mit serialisierten Klassen zu implementieren. Weitere Informationen finden Sie unter Serialisierung und Singleton.
Normalerweise werden während der Implementierung obiger Methoden diese als privat gehalten, damit Unterklassen sie nicht überschreiben können. Sie dienen nur dem Serialisierungszweck, und indem man sie privat hält, werden mögliche Sicherheitsprobleme vermieden.
Vererbung bei der Serialisierung
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 sie nicht in einen Stream konvertiert und später nicht abgerufen. Dies ist ein Ort, an dem die Methoden readObject() und writeObject() wirklich helfen. Indem wir ihre Implementierung bereitstellen, können wir den Zustand der Superklasse in den Stream speichern und ihn später abrufen. Schauen wir uns das in Aktion an.
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;
}
}
Superklasse ist eine einfache Java-Bean, aber sie implementiert 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()+"}";
}
// Hinzufügen einer Hilfsmethode zur Serialisierung, um den Zustand der Superklasse zu speichern/initialisieren
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
ois.defaultReadObject();
// Beachten Sie, dass die Reihenfolge von Lesen und Schreiben gleich sein sollte
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 {
// Das Objekt hier validieren
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 Logiken beim Lesen und Schreiben von Daten implementieren, um sie sicher zu machen. Beachten Sie auch, dass die Klasse das Interface \texttt{ObjectInputValidation} implementiert. Durch die Implementierung der Methode \texttt{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.
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 nützlich, wenn die Superklasse eine Drittanbieterklasse ist, die wir nicht ändern können.
Serialization Proxy Pattern
Die Serialisierung in Java birgt ernsthafte Fallstricke wie;
- Die Klassenstruktur kann ohne Unterbrechung des Java-Serialisierungsprozesses nicht stark verändert werden. Daher müssen wir selbst dann, wenn wir einige Variablen später nicht benötigen, sie für die Abwärtskompatibilität beibehalten.
- Die Serialisierung birgt erhebliche Sicherheitsrisiken, da ein Angreifer die Sequenz im Stream ändern und dem System Schaden zufügen kann. Zum Beispiel wird die Benutzerrolle serialisiert, und ein Angreifer ändert den Streamwert, um sie zu Admin zu machen und bösartigen Code auszuführen.
Das Java Serialisierungsproxy-Muster ist eine Möglichkeit, eine höhere Sicherheit bei der Serialisierung zu erreichen. In diesem Muster wird eine innere private statische Klasse als Proxyklasse für den Serialisierungszweck verwendet. Diese Klasse ist so konzipiert, dass sie den Zustand der Hauptklasse beibehält. Dieses Muster wird durch die ordnungsgemäße Implementierung der readResolve() und writeReplace() Methoden umgesetzt. Lassen Sie uns zunächst eine Klasse schreiben, die das Serialisierungsproxy-Muster implementiert, und sie dann zur besseren Verständlichkeit 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+"}";
}
// Serialisierungsproxyklasse
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){
// Daten zur Sicherheit verbergen
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");
}
}
// Serialisiertes Objekt durch DataProxy-Objekt ersetzen
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 Klassen
Data
als auchDataProxy
sollten das Serializable-Interface implementieren. DataProxy
sollte in der Lage sein, den Zustand des Data-Objekts beizubehalten.DataProxy
ist eine innere private statische Klasse, damit andere Klassen nicht darauf zugreifen können.DataProxy
sollte einen einzigen Konstruktor haben, der Data als Argument akzeptiert.- Die Klasse
Data
sollte eine writeReplace() Methode bereitstellen, die eine Instanz vonDataProxy
zurückgibt. Wenn also ein Data-Objekt serialisiert wird, ist der zurückgegebene Stream von der Klasse DataProxy. Die DataProxy-Klasse ist jedoch außerhalb nicht sichtbar, sodass sie nicht direkt verwendet werden kann. - Die Klasse
DataProxy
sollte eine readResolve() Methode implementieren, die einData
-Objekt zurückgibt. Wenn also die Data-Klasse deserialisiert wird, wird intern DataProxy deserialisiert, und wenn dessen readResolve() Methode aufgerufen wird, erhalten wir das Data-Objekt. - Endlich implementieren Sie die Methode readObject() in der Data-Klasse und werfen Sie
InvalidObjectException
, um Hackerangriffe zu vermeiden, die versuchen, ein 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 obige Klasse ausführen, erhalten wir die folgende Ausgabe in der Konsole.
Data{data=Pankaj}
Wenn Sie die Datei data.ser öffnen, sehen Sie, dass das DataProxy-Objekt als Strom in der Datei gespeichert ist.
Java Serialization Projekt herunterladen
Das ist alles zur Serialisierung in Java, es sieht einfach aus, aber wir sollten sie sorgfältig verwenden und es ist immer besser, sich nicht auf die Standardimplementierung zu verlassen. Laden Sie das Projekt über den oben genannten Link herunter und spielen Sie damit, um mehr zu lernen.
Source:
https://www.digitalocean.com/community/tutorials/serialization-in-java