Serialisatie in Java werd geïntroduceerd in JDK 1.1 en het is een van de belangrijke functies van Core Java.
Serialisatie in Java
Serialisatie in Java stelt ons in staat om een Object om te zetten naar een stream die we over het netwerk kunnen verzenden, of opslaan als bestand of in een database kunnen bewaren voor later gebruik. Deserialisatie is het proces van het omzetten van de Object stream naar het daadwerkelijke Java Object dat in ons programma kan worden gebruikt. Serialisatie in Java lijkt in eerste instantie heel eenvoudig te gebruiken, maar het brengt enkele triviale beveiligings- en integriteitsproblemen met zich mee, die we later in dit artikel zullen bespreken. We zullen de volgende onderwerpen behandelen in deze tutorial.
- Serializable in Java
- Klasse Refactoring met Serialisatie en serialVersionUID
- Java Externalizable Interface
- Java Serialisatiemethoden
- Serialisatie met Overerving
- Serialisatie Proxy Patroon
Serializable in Java
Als je wilt dat een klasse-object serialiseerbaar is, hoef je alleen maar de java.io.Serializable
-interface te implementeren. Serializable in Java is een markeringsinterface en heeft geen velden of methoden om te implementeren. Het is als een opt-in proces waarmee we onze klassen serialiseerbaar maken. Serialisatie in Java wordt geïmplementeerd door ObjectInputStream
en ObjectOutputStream
, dus het enige wat we nodig hebben is een wrapper eromheen om het ofwel naar een bestand te schrijven of het over het netwerk te verzenden. Laten we een eenvoudig voorbeeld bekijken van serialisatie in 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 en 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;
// }
}
Merk op dat het een eenvoudige Java Bean is met enkele eigenschappen en getter-setter methoden. Als je wilt dat een objecteigenschap niet wordt geserialiseerd naar de stream, kun je het sleutelwoord transient gebruiken, zoals ik heb gedaan met de salarisvariabele. Laten we nu veronderstellen dat we onze objecten naar een bestand willen schrijven en vervolgens willen deserialiseren vanuit hetzelfde bestand. We hebben dus hulpprogramma-methoden nodig die ObjectInputStream
en ObjectOutputStream
gebruiken voor serialisatiedoeleinden.
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 {
// deserializeer naar Object van het gegeven bestand
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;
}
// serializeer het gegeven object en sla het op naar het bestand
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();
}
}
Let op dat de methoden werken met Object dat de basisklasse is van elk Java-object. Het is op deze manier geschreven om generiek van aard te zijn. Laten we nu een testprogramma schrijven om Java-serialisatie in actie te zien.
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);
// serializeer naar bestand
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);
}
}
Wanneer we het bovenstaande testprogramma voor serialisatie in Java uitvoeren, krijgen we de volgende uitvoer.
emp Object::Employee{name=Pankaj,id=100,salary=5000}
empNew Object::Employee{name=Pankaj,id=100,salary=0}
Omdat het salaris een tijdelijke variabele is, werd de waarde niet opgeslagen in het bestand en dus niet opgehaald in het nieuwe object. Op dezelfde manier worden de waarden van statische variabelen ook niet geserialiseerd omdat ze toebehoren aan de klasse en niet aan het object.
Klasse Refactoring met Serialisatie en serialVersionUID
Serialisatie in Java staat enkele wijzigingen in de Java-klasse toe als ze kunnen worden genegeerd. Enkele van de wijzigingen in de klasse die geen invloed zullen hebben op het deserialisatieproces zijn:
- Het toevoegen van nieuwe variabelen aan de klasse
- Het wijzigen van de variabelen van tijdelijk naar niet-tijdelijk, voor serialisatie is het alsof je een nieuw veld hebt.
- Het wijzigen van de variabele van statisch naar niet-statisch, voor serialisatie is het alsof je een nieuw veld hebt.
Maar om al deze wijzigingen te laten werken, moet de java-klasse serialVersionUID gedefinieerd hebben voor de klasse. Laten we een testklasse schrijven alleen voor deserialisatie van het al eerder geserialiseerde bestand van de vorige 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);
}
}
Haal nu het commentaar weg bij de “wachtwoord” variabele en zijn getter-settermethoden van de Employee-klasse en voer het uit. Je krijgt de onderstaande uitzondering;
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
De reden is duidelijk dat serialVersionUID van de vorige klasse en de nieuwe klasse verschillend zijn. Eigenlijk, als de klasse serialVersionUID niet definieert, wordt het automatisch berekend en toegewezen aan de klasse. Java gebruikt klassevariabelen, methoden, klassenaam, pakket, etc. om dit unieke lange nummer te genereren. Als je met een IDE werkt, krijg je automatisch een waarschuwing dat “De serializable klasse Employee geen statische finale serialVersionUID-veld van het type long declareert”. We kunnen de java-hulpprogramma “serialver” gebruiken om de klasse serialVersionUID te genereren, voor de Employee-klasse kunnen we het uitvoeren met onderstaand commando.
SerializationExample/bin$serialver -classpath . com.journaldev.serialization.Employee
Merk op dat het niet vereist is dat de serialVersionUID van dit programma zelf wordt gegenereerd, we kunnen deze waarde toewijzen zoals we willen. Het moet er gewoon zijn om het deserialisatieproces te laten weten dat de nieuwe klasse de nieuwe versie van dezelfde klasse is en desgewenst moet worden gedeserialiseerd. Bijvoorbeeld, haal alleen het serialVersionUID-veld uit de Employee
-klasse en voer het SerializationTest
-programma uit. Haal nu het wachtwoordveld uit de Employee-klasse en voer het DeserializationTest
-programma uit en je zult zien dat de objectstroom met succes wordt gedeserialiseerd omdat de wijziging in de Employee-klasse compatibel is met het serialisatieproces.
Java Externalizable Interface
Als je het java serialisatieproces opmerkt, gebeurt dit automatisch. Soms willen we de objectgegevens verhullen om de integriteit ervan te behouden. Dit kunnen we doen door de java.io.Externalizable
-interface te implementeren en de implementatie te bieden van de writeExternal()– en readExternal()-methoden die worden gebruikt in het serialisatieproces.
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();
// lees in dezelfde volgorde als geschreven
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;
}
}
Merk op dat ik de veldwaarden heb gewijzigd voordat ik ze naar Stream heb geconverteerd en vervolgens bij het lezen de wijzigingen heb teruggedraaid. Op deze manier kunnen we op de een of andere manier de gegevensintegriteit behouden. We kunnen een uitzondering gooien als na het lezen van de streamgegevens de integriteitscontroles mislukken. Laten we een testprogramma schrijven om het in actie te zien.
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();
}
}
}
Als we het bovenstaande programma uitvoeren, krijgen we de volgende uitvoer.
Person Object Read=Person{id=1,name=Pankaj,gender=Male}
Dus welke is beter om te worden gebruikt voor serialisatie in java. Eigenlijk is het beter om de Serializable-interface te gebruiken en tegen de tijd dat we aan het einde van het artikel komen, zul je weten waarom.
Java Serialisatiemethoden
We hebben gezien dat serialisatie in Java automatisch is en alles wat we nodig hebben, is het implementeren van de Serializable-interface. De implementatie is aanwezig in de klassen ObjectInputStream en ObjectOutputStream. Maar wat als we de manier waarop we gegevens opslaan willen wijzigen? Bijvoorbeeld, als we gevoelige informatie in het object hebben en deze voor het opslaan/herstellen willen versleutelen/decrypteren. Daarom zijn er vier methoden die we kunnen aanbieden in de klasse om het serialisatiegedrag te wijzigen. Als deze methoden aanwezig zijn in de klasse, worden ze gebruikt voor serialisatiedoeleinden.
- readObject(ObjectInputStream ois): Als deze methode aanwezig is in de klasse, zal de ObjectInputStream-methode readObject() deze methode gebruiken om het object uit de stream te lezen.
- writeObject(ObjectOutputStream oos): Als deze methode aanwezig is in de klasse, zal de ObjectOutputStream-methode writeObject() deze methode gebruiken om het object naar de stream te schrijven. Een veelvoorkomend gebruik is het obscuur maken van de objectvariabelen om gegevensintegriteit te behouden.
- Object writeReplace(): Als deze methode aanwezig is, wordt deze methode na het serialisatieproces opgeroepen en het geretourneerde object wordt naar de stream geserialiseerd.
- Object readResolve(): Als deze methode aanwezig is, wordt deze methode na het deserialisatieproces opgeroepen om het uiteindelijke object aan het oproepende programma terug te geven. Een van de toepassingen van deze methode is het implementeren van het Singleton-patroon met geserialiseerde klassen. Lees meer op Serialisatie en Singleton.
Gewoonlijk worden bij het implementeren van bovengenoemde methoden deze privé gehouden zodat subklassen ze niet kunnen overschrijven. Ze zijn alleen bedoeld voor serialisatiedoeleinden en door ze privé te houden, worden mogelijke beveiligingsproblemen vermeden.
Serialisatie met Overerving
Soms moeten we een klasse uitbreiden die de Serializable-interface niet implementeert. Als we vertrouwen op het automatische serialisatiegedrag en de superklasse heeft een bepaalde toestand, dan worden ze niet omgezet naar een stroom en dus later niet opgehaald. Dit is een situatie waarin de methoden readObject() en writeObject() echt helpen. Door hun implementatie te bieden, kunnen we de toestand van de superklasse opslaan naar de stroom en deze later ophalen. Laten we dit in actie zien.
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 is een eenvoudige Java bean maar implementeert de Serializable-interface niet.
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()+"}";
}
// hulpmethode toevoegen voor serialisatie om de toestand van de superklasse op te slaan/initialiseren
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
ois.defaultReadObject();
// let op de volgorde van lezen en schrijven moet hetzelfde zijn
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 {
// valideer het object hier
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");
}
}
Let op dat de volgorde van het schrijven en lezen van de extra gegevens naar de stream hetzelfde moet zijn. We kunnen wat logica toevoegen bij het lezen en schrijven van gegevens om het veilig te maken. Merk ook op dat de klasse de interface ObjectInputValidation
implementeert. Door de methode validateObject() te implementeren, kunnen we enkele zakelijke validaties uitvoeren om ervoor te zorgen dat de gegevensintegriteit niet wordt geschaad. Laten we een testklasse schrijven en kijken of we de toestand van de superklasse kunnen ophalen uit geserialiseerde gegevens of niet.
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();
}
}
}
Als we de bovenstaande klasse uitvoeren, krijgen we de volgende uitvoer.
SubClass read = SubClass{id=10,value=Data,name=Pankaj}
Op deze manier kunnen we dus de toestand van de superklasse serialiseren, zelfs als deze de Serializable-interface niet implementeert. Deze strategie is handig wanneer de superklasse een klasse van derden is die we niet kunnen wijzigen.
Serialization Proxy Pattern
Serialisatie in Java gaat gepaard met een aantal ernstige valkuilen zoals;
- De klassenstructuur kan niet veel worden gewijzigd zonder het serialisatieproces in Java te verbreken. Dus zelfs als we later sommige variabelen niet nodig hebben, moeten we ze behouden voor achterwaartse compatibiliteit.
- Serialisatie veroorzaakt enorme beveiligingsrisico’s, een aanvaller kan de stroomsequentie wijzigen en schade aanrichten aan het systeem. Bijvoorbeeld, de gebruikersrol wordt geserialiseerd en een aanvaller verandert de stroomwaarde om het admin te maken en kwaadwillende code uit te voeren.
Het Java Serialization Proxy-patroon is een manier om een hogere beveiliging te bereiken met Serialization. In dit patroon wordt een innerlijke private statische klasse gebruikt als een proxyklasse voor serialisatiedoeleinden. Deze klasse is ontworpen op zo’n manier dat de toestand van de hoofdklasse wordt gehandhaafd. Dit patroon wordt geïmplementeerd door readResolve() en writeReplace() methoden op de juiste manier te implementeren. Laten we eerst een klasse schrijven die het serialisatieproxy-patroon implementeert en het vervolgens analyseren voor een beter begrip.
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+"}";
}
//serialisatieproxyklasse
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){
//gegevens verhullen voor beveiliging
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");
}
}
//vervangen van geserialiseerd object door 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");
}
}
- Zowel de klasse
Data
als de klasseDataProxy
moeten de interface Serializable implementeren. - De klasse
DataProxy
moet de toestand van het Data-object kunnen behouden. DataProxy
is een innerlijke private statische klasse, zodat andere klassen er geen toegang toe hebben.DataProxy
moet een enkele constructor hebben die Data als argument neemt.- De klasse
Data
moet een writeReplace() methode bieden die een instantie vanDataProxy
retourneert. Dus wanneer een Data-object wordt geserialiseerd, is de geretourneerde stream van de klasse DataProxy. DataProxy-klasse is echter niet zichtbaar buiten, dus het kan niet rechtstreeks worden gebruikt. - De klasse
DataProxy
moet een readResolve() methode implementeren die eenData
-object retourneert. Dus wanneer de Data-klasse wordt gedeserialiseerd, wordt intern DataProxy gedeserialiseerd en wanneer de readResolve() methode wordt aangeroepen, krijgen we het Data-object. - Eindelijk implementeer de readObject()-methode in de Data-klasse en gooi een
InvalidObjectException
om aanvallen van hackers te voorkomen die proberen een Data-objectstroom te fabriceren en deze te parseren.
Laten we een kleine test schrijven om te controleren of de implementatie werkt of niet.
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();
}
}
}
Wanneer we de bovenstaande klasse uitvoeren, krijgen we de onderstaande uitvoer op de console.
Data{data=Pankaj}
Als je het data.ser-bestand opent, zie je dat het DataProxy-object als een stroom in het bestand is opgeslagen.
Download Java Serialization Project
Dat is alles voor Serialisatie in Java, het lijkt eenvoudig maar we moeten er verstandig mee omgaan en het is altijd beter om niet te vertrouwen op de standaardimplementatie. Download het project via de bovenstaande link en speel ermee om meer te leren.
Source:
https://www.digitalocean.com/community/tutorials/serialization-in-java