Java Language
Serialisierung
Suche…
Einführung
Java stellt einen Mechanismus bereit, der Objektserialisierung, bei dem ein Objekt als Folge von Bytes dargestellt werden kann, die die Daten des Objekts sowie Informationen über den Objekttyp und die im Objekt gespeicherten Datentypen enthält.
Nachdem ein serialisiertes Objekt in eine Datei geschrieben wurde, kann es aus der Datei gelesen und deserialisiert werden, d. H. Die Typinformationen und Bytes, die das Objekt und seine Daten darstellen, können verwendet werden, um das Objekt im Speicher wiederherzustellen.
Grundlegende Serialisierung in Java
Was ist Serialisierung?
Serialisierung ist der Prozess, bei dem der Status eines Objekts (einschließlich seiner Referenzen) in eine Folge von Bytes konvertiert wird, sowie der Prozess, bei dem diese Bytes zu einem späteren Zeitpunkt in ein Live-Objekt umgewandelt werden. Die Serialisierung wird verwendet, wenn Sie das Objekt beibehalten möchten. Es wird auch von Java RMI verwendet, um Objekte zwischen JVMs zu übergeben, entweder als Argumente in einem Methodenaufruf von einem Client an einen Server oder als Rückgabewerte von einem Methodenaufruf oder als von Remotemethoden ausgelöste Ausnahmen. Im Allgemeinen wird die Serialisierung verwendet, wenn das Objekt über die Lebensdauer der JVM hinaus existieren soll.
java.io.Serializable
ist eine Markierungsschnittstelle (hat keinen Körper). Es wird nur verwendet, um Java-Klassen als serialisierbar zu "kennzeichnen".
Die Serialisierungslaufzeit serialVersionUID
jeder serialisierbaren Klasse eine Versionsnummer zu, die als serialVersionUID
bezeichnet wird. Diese wird während der De- Serialisierung verwendet, um zu überprüfen, ob der Sender und der Empfänger eines serialisierten Objekts Klassen für dieses Objekt geladen haben, die hinsichtlich der Serialisierung kompatibel sind. Wenn der Empfänger eine Klasse für das Objekt geladen hat, die eine andere serialVersionUID
als die Klasse des entsprechenden Senders, führt die Deserialisierung zu einer InvalidClassException
. Eine serialisierbare Klasse kann ihre eigene serialVersionUID
explizit deklarieren, indem sie ein Feld mit dem Namen serialVersionUID
, das static, final,
und vom Typ long
:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 1L;
So machen Sie eine Klasse für die Serialisierung geeignet
Um ein Objekt zu persistieren, muss die entsprechende Klasse die Schnittstelle java.io.Serializable
implementieren.
import java.io.Serializable;
public class SerialClass implements Serializable {
private static final long serialVersionUID = 1L;
private Date currentTime;
public SerialClass() {
currentTime = Calendar.getInstance().getTime();
}
public Date getCurrentTime() {
return currentTime;
}
}
Wie schreibe ich ein Objekt in eine Datei?
Nun müssen wir dieses Objekt in ein Dateisystem schreiben. Wir verwenden dazu java.io.ObjectOutputStream
.
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;
public class PersistSerialClass {
public static void main(String [] args) {
String filename = "time.ser";
SerialClass time = new SerialClass(); //We will write this object to file system.
try {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename));
out.writeObject(time); //Write byte stream to file system.
out.close();
} catch(IOException ex){
ex.printStackTrace();
}
}
}
So erstellen Sie ein Objekt aus dem serialisierten Status
Das gespeicherte Objekt kann zu einem späteren Zeitpunkt mit java.io.ObjectInputStream
wie java.io.ObjectInputStream
aus dem Dateisystem gelesen werden:
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.IOException;
import java.io.java.lang.ClassNotFoundException;
public class ReadSerialClass {
public static void main(String [] args) {
String filename = "time.ser";
SerialClass time = null;
try {
ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename));
time = (SerialClass)in.readObject();
in.close();
} catch(IOException ex){
ex.printStackTrace();
} catch(ClassNotFoundException cnfe){
cnfe.printStackTrace();
}
// print out restored time
System.out.println("Restored time: " + time.getTime());
}
}
Die serialisierte Klasse ist in binärer Form. Die Deserialisierung kann problematisch sein, wenn sich die Klassendefinition ändert: Weitere Informationen finden Sie im Kapitel Versionierung von serialisierten Objekten der Java-Serialisierungsspezifikation .
Durch das Serialisieren eines Objekts wird der gesamte Objektgraph, von dem es die Wurzel ist, serialisiert und bei Vorhandensein von zyklischen Graphen korrekt ausgeführt. Eine reset()
-Methode wird bereitgestellt, um den ObjectOutputStream
zu zwingen, ObjectOutputStream
zu vergessen, die bereits serialisiert wurden.
Transientenfelder - Serialisierung
Serialisierung mit Gson
Die Serialisierung mit Gson ist einfach und gibt korrektes JSON aus.
public class Employe {
private String firstName;
private String lastName;
private int age;
private BigDecimal salary;
private List<String> skills;
//getters and setters
}
(Serialisierung)
//Skills
List<String> skills = new LinkedList<String>();
skills.add("leadership");
skills.add("Java Experience");
//Employe
Employe obj = new Employe();
obj.setFirstName("Christian");
obj.setLastName("Lusardi");
obj.setAge(25);
obj.setSalary(new BigDecimal("10000"));
obj.setSkills(skills);
//Serialization process
Gson gson = new Gson();
String json = gson.toJson(obj); //{"firstName":"Christian","lastName":"Lusardi","age":25,"salary":10000,"skills":["leadership","Java Experience"]}
Beachten Sie, dass Sie Objekte mit Zirkelverweisen nicht serialisieren können, da dies zu einer unendlichen Rekursion führt.
(Deserialisierung)
//it's very simple...
//Assuming that json is the previous String object....
Employe obj2 = gson.fromJson(json, Employe.class); // obj2 is just like obj
Serialisierung mit Jackson 2
Die folgende Implementierung zeigt, wie ein Objekt in die entsprechende JSON-Zeichenfolge serialisiert werden kann.
class Test {
private int idx;
private String name;
public int getIdx() {
return idx;
}
public void setIdx(int idx) {
this.idx = idx;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Serialisierung:
Test test = new Test();
test.setIdx(1);
test.setName("abc");
ObjectMapper mapper = new ObjectMapper();
String jsonString;
try {
jsonString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(test);
System.out.println(jsonString);
} catch (JsonProcessingException ex) {
// Handle Exception
}
Ausgabe:
{
"idx" : 1,
"name" : "abc"
}
Sie können den Standarddrucker auslassen, wenn Sie ihn nicht benötigen.
Die hier verwendete Abhängigkeit ist wie folgt:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.6.3</version>
</dependency>
Benutzerdefinierte Serialisierung
In diesem Beispiel möchten wir eine Klasse erstellen, die eine Konsole generiert und ausgibt, eine Zufallszahl zwischen zwei Ganzzahlen, die während der Initialisierung als Argumente übergeben werden.
public class SimpleRangeRandom implements Runnable {
private int min;
private int max;
private Thread thread;
public SimpleRangeRandom(int min, int max){
this.min = min;
this.max = max;
thread = new Thread(this);
thread.start();
}
@Override
private void WriteObject(ObjectOutputStreamout) throws IO Exception;
private void ReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
public void run() {
while(true) {
Random rand = new Random();
System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Wenn wir diese Klasse serialisierbar machen wollen, gibt es einige Probleme. Der Thread ist eine der bestimmten Klassen auf Systemebene, die nicht serialisierbar sind. Also müssen wir den Thread als transient erklären. Auf diese Weise können wir die Objekte dieser Klasse serialisieren, haben jedoch noch ein Problem. Wie Sie im Konstruktor sehen können, legen wir die Min- und Max-Werte unseres Randomizers fest. Danach starten wir den Thread, der für das Generieren und Drucken des Zufallswerts verantwortlich ist. Beim Wiederherstellen des persistenten Objekts durch Aufrufen von readObject () wird der Konstruktor daher nicht erneut ausgeführt, da kein neues Objekt erstellt wird. In diesem Fall müssen wir eine benutzerdefinierte Serialisierung entwickeln, indem wir zwei Methoden innerhalb der Klasse bereitstellen. Diese Methoden sind:
private void writeObject(ObjectOutputStream out) throws IOException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
Indem wir unsere Implementierung in readObject () hinzufügen, können wir unseren Thread initiieren und starten:
class RangeRandom implements Serializable, Runnable {
private int min;
private int max;
private transient Thread thread;
//transient should be any field that either cannot be serialized e.g Thread or any field you do not want serialized
public RangeRandom(int min, int max){
this.min = min;
this.max = max;
thread = new Thread(this);
thread.start();
}
@Override
public void run() {
while(true) {
Random rand = new Random();
System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
thread = new Thread(this);
thread.start();
}
}
Hier ist das Wichtigste für unser Beispiel:
public class Main {
public static void main(String[] args) {
System.out.println("Hello");
RangeRandom rangeRandom = new RangeRandom(1,10);
FileOutputStream fos = null;
ObjectOutputStream out = null;
try
{
fos = new FileOutputStream("test");
out = new ObjectOutputStream(fos);
out.writeObject(rangeRandom);
out.close();
}
catch(IOException ex)
{
ex.printStackTrace();
}
RangeRandom rangeRandom2 = null;
FileInputStream fis = null;
ObjectInputStream in = null;
try
{
fis = new FileInputStream("test");
in = new ObjectInputStream(fis);
rangeRandom2 = (RangeRandom)in.readObject();
in.close();
}
catch(IOException ex)
{
ex.printStackTrace();
}
catch(ClassNotFoundException ex)
{
ex.printStackTrace();
}
}
}
Wenn Sie main ausführen, werden Sie sehen, dass für jede RangeRandom-Instanz zwei Threads ausgeführt werden. Dies liegt daran, dass sich die Thread.start () - Methode jetzt sowohl im Konstruktor als auch in readObject () befindet .
Versionierung und serialVersionUID
Wenn Sie die Schnittstelle java.io.Serializable
implementieren, um eine Klasse serialisierbar zu machen, sucht der Compiler nach einem static final
serialVersionUID
namens serialVersionUID
vom Typ long
. Wenn für die Klasse dieses Feld nicht explizit deklariert ist, erstellt der Compiler ein solches Feld und weist ihm einen Wert zu, der aus einer implementierungsabhängigen Berechnung von serialVersionUID
. Diese Berechnung hängt von verschiedenen Aspekten der Klasse ab und folgt den von Sun angegebenen Objekt-Serialisierungsspezifikationen . Es ist jedoch nicht garantiert, dass der Wert bei allen Compiler-Implementierungen gleich ist.
Dieser Wert wird zur Überprüfung der Kompatibilität der Klassen in Bezug auf die Serialisierung verwendet. Dies erfolgt während der Deserialisierung eines gespeicherten Objekts. Die Serialisierungslaufzeit stellt serialVersionUID
dass die aus den serialVersionUID
gelesene serialVersionUID
und die in der Klasse deklarierte serialVersionUID
identisch sind. Ist dies nicht der Fall, wird eine InvalidClassException
.
Es wird dringend empfohlen, das statische, letzte Feld des Typs long und den Namen 'serialVersionUID' in allen Klassen, in denen Sie Serializable machen möchten, explizit zu deklarieren und zu initialisieren, anstatt sich auf die Standardberechnung des Werts für dieses Feld zu verlassen, auch wenn Sie dies nicht tun Versionierung verwenden. Die 'serialVersionUID'-Berechnung ist äußerst empfindlich und kann von einer Compilerimplementierung zur anderen variieren. Daher kann es InvalidClassException
, dass Sie die InvalidClassException
auch für dieselbe Klasse InvalidClassException
, nur weil Sie verschiedene Compilerimplementierungen auf der Sender- und Empfängerseite des Serialisierungsprozesses verwendet haben.
public class Example implements Serializable {
static final long serialVersionUID = 1L /*or some other value*/;
//...
}
Solange serialVersionUID
ist, kann Java Serialization verschiedene Versionen einer Klasse verarbeiten. Kompatible und inkompatible Änderungen sind:
Kompatible Änderungen
- Felder hinzufügen: Wenn die wiederherzustellende Klasse ein Feld enthält, das im Stream nicht vorkommt, wird dieses Feld im Objekt mit dem Standardwert für seinen Typ initialisiert. Wenn eine klassenspezifische Initialisierung erforderlich ist, kann die Klasse eine readObject-Methode bereitstellen, mit der das Feld auf nicht standardmäßige Werte initialisiert werden kann.
- Klassen hinzufügen: Der Stream enthält die Typhierarchie jedes Objekts im Stream. Beim Vergleich dieser Hierarchie im Stream mit der aktuellen Klasse können zusätzliche Klassen erkannt werden. Da es im Stream keine Informationen gibt, von denen aus das Objekt initialisiert werden kann, werden die Felder der Klasse mit den Standardwerten initialisiert.
- Entfernen von Klassen: Wenn Sie die Klassenhierarchie im Stream mit der der aktuellen Klasse vergleichen, kann dies feststellen, dass eine Klasse gelöscht wurde. In diesem Fall werden die dieser Klasse entsprechenden Felder und Objekte aus dem Stream gelesen. Primitive Felder werden verworfen, aber die von der gelöschten Klasse referenzierten Objekte werden erstellt, da auf sie später im Stream verwiesen werden kann. Sie werden Müll gesammelt, wenn der Stream Müll gesammelt oder zurückgesetzt wird.
- WriteObject / readObject-Methoden hinzufügen: Wenn die Version, die den Stream liest, über diese Methoden verfügt, wird erwartet, dass readObject wie üblich die erforderlichen Daten liest, die von der Standardserialisierung in den Stream geschrieben werden. Es sollte defaultReadObject zuerst aufrufen, bevor optionale Daten gelesen werden. Es wird erwartet, dass die writeObject-Methode wie üblich defaultWriteObject zum Schreiben der erforderlichen Daten aufruft und möglicherweise optionale Daten schreibt.
- Hinzufügen von java.io.Serializable: Dies entspricht dem Hinzufügen von Typen. Es gibt keine Werte im Stream für diese Klasse, daher werden die Felder mit den Standardwerten initialisiert. Die Unterstützung für Unterklassen nicht serialisierbarer Klassen erfordert, dass der Supertyp der Klasse über einen No-Arg-Konstruktor verfügt und die Klasse selbst mit Standardwerten initialisiert wird. Wenn der no-arg-Konstruktor nicht verfügbar ist, wird die InvalidClassException ausgelöst.
- Ändern des Zugriffs auf ein Feld: Die Zugriffsmodifizierer public, package, protected und private haben keinen Einfluss auf die Möglichkeit der Serialisierung, den Feldern Werte zuzuweisen.
- Ändern eines Felds von statisch in nicht statisch oder vorübergehend in nicht transient: Wenn Sie sich bei der Berechnung der serialisierbaren Felder auf die Standardserialisierung verlassen, entspricht diese Änderung dem Hinzufügen eines Felds zur Klasse. Das neue Feld wird in den Stream geschrieben, aber frühere Klassen ignorieren den Wert, da die Serialisierung statischen oder transienten Feldern keine Werte zuweist.
Inkompatible Änderungen
- Löschen von Feldern: Wenn ein Feld in einer Klasse gelöscht wird, enthält der geschriebene Datenstrom seinen Wert nicht. Wenn der Stream von einer früheren Klasse gelesen wird, wird der Wert des Felds auf den Standardwert gesetzt, da im Stream kein Wert verfügbar ist. Dieser Standardwert kann jedoch die Fähigkeit der früheren Version zur Erfüllung ihres Vertrags beeinträchtigen.
- Klassen in der Hierarchie nach oben oder unten verschieben: Dies ist nicht zulässig, da die Daten im Stream in der falschen Reihenfolge angezeigt werden.
- Ändern eines nicht statischen Felds in ein statisches Feld oder ein nicht-transientes Feld in einen vorübergehenden Zustand: Wenn Sie sich auf die Standardserialisierung verlassen, entspricht diese Änderung dem Löschen eines Felds aus der Klasse. Diese Version der Klasse schreibt diese Daten nicht in den Stream, sodass sie nicht von früheren Versionen der Klasse gelesen werden können. Wie beim Löschen eines Felds wird das Feld der früheren Version mit dem Standardwert initialisiert, was dazu führen kann, dass die Klasse auf unerwartete Weise versagt.
- Ändern des deklarierten Typs eines primitiven Felds: Jede Version der Klasse schreibt die Daten mit ihrem deklarierten Typ. Frühere Versionen der Klasse, die versuchen, das Feld zu lesen, schlagen fehl, da der Datentyp im Stream nicht mit dem Typ des Felds übereinstimmt.
- Ändern der writeObject- oder readObject-Methode, sodass die Standardfelddaten nicht mehr geschrieben oder gelesen werden, oder geändert, sodass versucht wird, sie zu schreiben oder zu lesen, wenn die vorherige Version dies nicht tat. Die Standardfelddaten müssen entweder dauerhaft im Stream angezeigt werden oder nicht.
- Das Ändern einer Klasse von Serializable in Externalizable oder umgekehrt ist eine inkompatible Änderung, da der Stream Daten enthält, die mit der Implementierung der verfügbaren Klasse nicht kompatibel sind.
- Ändern einer Klasse von einem Nicht-Enummentyp in einen Enummentyp oder umgekehrt, da der Stream Daten enthält, die mit der Implementierung der verfügbaren Klasse nicht kompatibel sind.
- Das Entfernen von Serializable oder Externalizable ist eine inkompatible Änderung, da beim Schreiben nicht mehr die Felder bereitgestellt werden, die von älteren Versionen der Klasse benötigt werden.
- Das Hinzufügen der writeReplace- oder readResolve-Methode zu einer Klasse ist nicht kompatibel, wenn das Verhalten ein Objekt erzeugen würde, das mit einer älteren Version der Klasse nicht kompatibel ist.
Benutzerdefinierte JSON-Deserialisierung mit Jackson
Wir verwenden Rest API als JSON-Format und entpacken es dann in ein POJO. Jacksons org.codehaus.jackson.map.ObjectMapper funktioniert einfach "out of the box" und wir machen in den meisten Fällen wirklich nichts. Manchmal benötigen wir jedoch einen benutzerdefinierten Deserializer, um unsere benutzerdefinierten Anforderungen zu erfüllen. In diesem Lernprogramm werden Sie durch den Prozess des Erstellens Ihres eigenen benutzerdefinierten Deserializers geführt.
Nehmen wir an, wir haben folgende Entitäten.
public class User {
private Long id;
private String name;
private String email;
//getter setter are omitted for clarity
}
Und
public class Program {
private Long id;
private String name;
private User createdBy;
private String contents;
//getter setter are omitted for clarity
}
Lassen Sie uns zuerst ein Objekt serialisieren / marshallen.
User user = new User();
user.setId(1L);
user.setEmail("[email protected]");
user.setName("Bazlur Rahman");
Program program = new Program();
program.setId(1L);
program.setName("Program @# 1");
program.setCreatedBy(user);
program.setContents("Some contents");
ObjectMapper objectMapper = new ObjectMapper();
final String json = objectMapper.writeValueAsString (Programm); System.out.println (json);
Der obige Code erzeugt folgende JSON-
{
"id": 1,
"name": "Program @# 1",
"createdBy": {
"id": 1,
"name": "Bazlur Rahman",
"email": "[email protected]"
},
"contents": "Some contents"
}
Jetzt kann man ganz leicht das Gegenteil tun. Wenn wir über diese JSON verfügen, können Sie die Objektgenerierung mithilfe von ObjectMapper wie folgt aufheben:
Nehmen wir an, dies ist nicht der Fall. Wir haben eine andere JSON als eine API, die nicht zu unserer Program
passt.
{
"id": 1,
"name": "Program @# 1",
"ownerId": 1
"contents": "Some contents"
}
Schauen Sie sich die JSON-Zeichenfolge an, wie Sie sehen, sie hat ein anderes Feld, das owenerId ist.
Wenn Sie diesen JSON wie zuvor serialisieren möchten, haben Sie Ausnahmen.
Es gibt zwei Möglichkeiten, Ausnahmen zu vermeiden und diese serialisiert zu haben:
Ignoriere die unbekannten Felder
Ignoriere die onwerId
. Fügen Sie die folgende Anmerkung in der Programmklasse hinzu
@JsonIgnoreProperties(ignoreUnknown = true)
public class Program {}
Schreiben Sie benutzerdefinierte Deserializer
Es gibt jedoch Fälle, in denen Sie dieses owerId
Feld tatsächlich benötigen. Angenommen, Sie möchten es als ID der User
Klasse angeben.
In diesem Fall müssen Sie einen benutzerdefinierten Deserialisierer schreiben.
Wie Sie sehen, müssen Sie zunächst vom JsonNode
aus auf den JonsParser
. Dann können JsonNode
mit der Methode get()
problemlos Informationen aus einem JsonNode
extrahieren. und Sie müssen sich um den Feldnamen kümmern. Es sollte der genaue Name sein. Rechtschreibfehler verursachen Ausnahmen.
Und schließlich müssen Sie Ihren ProgramDeserializer beim ObjectMapper
registrieren.
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Program.class, new ProgramDeserializer());
mapper.registerModule(module);
String newJsonString = "{\"id\":1,\"name\":\"Program @# 1\",\"ownerId\":1,\"contents\":\"Some contents\"}";
final Program program2 = mapper.readValue(newJsonString, Program.class);
Alternativ können Sie Annotation verwenden, um den Deserializer direkt zu registrieren.
@JsonDeserialize(using = ProgramDeserializer.class)
public class Program {
}