Serializzazione in Java – Serializzazione in Java

Serializzazione in Java è stata introdotta in JDK 1.1 ed è una delle importanti caratteristiche di Core Java.

La serializzazione in Java

consente di convertire un oggetto in un flusso che può essere inviato attraverso la rete o salvato come file o memorizzato nel database per un utilizzo successivo. La deserializzazione è il processo di conversione del flusso di oggetti nell’effettivo oggetto Java da utilizzare nel nostro programma. La serializzazione in Java sembra molto facile da usare inizialmente, ma presenta alcuni problemi di sicurezza e integrità banali che esamineremo nella parte successiva di questo articolo. Esamineremo i seguenti argomenti in questo tutorial.

  1. Serializable in Java
  2. Rifattorizzazione della classe con Serializzazione e serialVersionUID
  3. Interfaccia Externalizable di Java
  4. Metodi di serializzazione di Java
  5. Serializzazione con ereditarietà
  6. Modello di proxy di serializzazione

Serializable in Java

Se vuoi che un oggetto di classe sia serializzabile, tutto ciò che devi fare è implementare l’interfaccia java.io.Serializable. Serializable in java è un’interfaccia di marcatura e non ha campi o metodi da implementare. È come un processo di autorizzazione attraverso il quale rendiamo le nostre classi serializzabili. La serializzazione in java è implementata da ObjectInputStream e ObjectOutputStream, quindi tutto ciò di cui abbiamo bisogno è un wrapper su di essi per salvarlo su file o inviarlo attraverso la rete. Vediamo un semplice esempio di programma di serializzazione 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+"}";
	}
	
	//metodi getter e setter
	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;
//	}
	
}

Nota che si tratta di un semplice java bean con alcune proprietà e metodi getter-setter. Se vuoi che una proprietà dell’oggetto non sia serializzata sullo stream, puoi utilizzare la parola chiave transient come ho fatto con la variabile salario. Ora supponiamo che vogliamo scrivere i nostri oggetti su file e quindi deserializzarli dallo stesso file. Abbiamo quindi bisogno di metodi di utilità che utilizzeranno ObjectInputStream e ObjectOutputStream per scopi di serializzazione.

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 {

	// deserializza in un oggetto dal file dato
	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;
	}

	// serializza l'oggetto dato e salvatelo su 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();
	}

}

Si noti che gli argomenti del metodo funzionano con Object che è la classe base di qualsiasi oggetto Java. È scritto in questo modo per essere generico per natura. Ora scriviamo un programma di test per vedere la serializzazione Java in azione.

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);
		
		// serializza su 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);
	}
}

Quando eseguiamo il programma di test sopra per la serializzazione in Java, otteniamo l’output seguente.

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

Dato che lo stipendio è una variabile transiente, il suo valore non è stato salvato su file e quindi non è stato recuperato nel nuovo oggetto. Allo stesso modo, i valori delle variabili statiche non vengono serializzati in quanto appartengono alla classe e non all’oggetto.

Rifattorizzazione della classe con Serializzazione e serialVersionUID

La serializzazione in Java consente alcune modifiche nella classe Java se possono essere ignorate. Alcune delle modifiche nella classe che non influenzeranno il processo di deserializzazione sono:

  • Aggiunta di nuove variabili alla classe
  • Modifica delle variabili da transiente a non transiente, per la serializzazione è come avere un nuovo campo.
  • Modifica della variabile da statica a non statica, per la serializzazione è come avere un nuovo campo.

Ma affinché tutte queste modifiche funzionino, la classe Java dovrebbe avere definito il serialVersionUID per la classe. Scriviamo una classe di test solo per la deserializzazione del file già serializzato dalla classe di test precedente.

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

Ora rimuovi il commento dalla variabile “password” e dai metodi getter-setter della classe Employee e esegui il programma. Otterrai l’eccezione seguente;

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

Il motivo è chiaro: serialVersionUID della classe precedente e della nuova classe sono diversi. In realtà, se la classe non definisce serialVersionUID, viene calcolato automaticamente e assegnato alla classe. Java utilizza variabili di classe, metodi, nome della classe, pacchetto, ecc. per generare questo numero lungo unico. Se stai lavorando con un IDE, riceverai automaticamente un avviso che dice “La classe serializzabile Employee non dichiara un campo serialVersionUID statico finale di tipo long”. Possiamo utilizzare l’utilità Java “serialver” per generare il serialVersionUID della classe; per la classe Employee possiamo eseguirlo con il comando seguente.

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

Nota che non è obbligatorio che il numero di versione seriale sia generato da questo programma stesso; possiamo assegnare questo valore come vogliamo. Deve solo essere presente per far sapere al processo di deserializzazione che la nuova classe è la nuova versione della stessa classe e dovrebbe essere deserializzata se possibile. Ad esempio, rimuovi solo il campo serialVersionUID dalla classe Employee e esegui il programma SerializationTest. Ora rimuovi il commento dal campo password della classe Employee ed esegui il programma DeserializationTest; vedrai che lo stream dell’oggetto viene deserializzato correttamente perché la modifica nella classe Employee è compatibile con il processo di serializzazione.

Interfaccia Externalizable di Java

Se noti il processo di serializzazione di Java, avviene automaticamente. A volte vogliamo oscurare i dati dell’oggetto per mantenere la sua integrità. Possiamo farlo implementando l’interfaccia java.io.Externalizable e fornendo l’implementazione dei metodi writeExternal() e readExternal() da utilizzare nel processo di serializzazione.

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();
		//leggi nello stesso ordine in cui è stato scritto
		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;
	}

}

Nota che ho cambiato i valori dei campi prima di convertirli in Stream e poi durante la lettura ho invertito le modifiche. In questo modo possiamo mantenere l’integrità dei dati in qualche modo. Possiamo generare un’eccezione se dopo la lettura dei dati dello stream, i controlli di integrità falliscono. Scriviamo un programma di test per vederlo in azione.

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 blocco catch generato automaticamente
			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();
		}
	    
	}
}

Quando eseguiamo il programma sopra, otteniamo il seguente output.

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

Quale è meglio usare per la serializzazione in Java. In realtà è meglio usare l’interfaccia Serializable e quando arriveremo alla fine dell’articolo, capirai perché.

Metodi di Serializzazione di Java

Abbiamo visto che la serializzazione in Java è automatica e tutto ciò di cui abbiamo bisogno è implementare l’interfaccia Serializable. L’implementazione è presente nelle classi ObjectInputStream e ObjectOutputStream. Ma cosa succede se vogliamo cambiare il modo in cui salviamo i dati, ad esempio se abbiamo delle informazioni sensibili nell’oggetto e vogliamo criptarle/criptarle prima di salvarle/recuperarle? Ecco perché ci sono quattro metodi che possiamo fornire nella classe per modificare il comportamento della serializzazione. Se questi metodi sono presenti nella classe, vengono utilizzati per scopi di serializzazione.

  1. readObject(ObjectInputStream ois): Se questo metodo è presente nella classe, il metodo ObjectInputStream readObject() utilizzerà questo metodo per leggere l’oggetto dallo stream.
  2. writeObject(ObjectOutputStream oos): Se questo metodo è presente nella classe, il metodo ObjectOutputStream writeObject() utilizzerà questo metodo per scrivere l’oggetto nello stream. Uno degli utilizzi comuni è oscurare le variabili dell’oggetto per mantenere l’integrità dei dati.
  3. Object writeReplace(): Se questo metodo è presente, dopo il processo di serializzazione viene chiamato questo metodo e l’oggetto restituito viene serializzato nello stream.
  4. Object readResolve(): Se questo metodo è presente, dopo il processo di deserializzazione viene chiamato questo metodo per restituire l’oggetto finale al programma chiamante. Uno degli utilizzi di questo metodo è implementare il pattern Singleton con classi serializzate. Per ulteriori informazioni, leggi su Serializzazione e Singleton.

Di solito, durante l’implementazione dei metodi sopra indicati, vengono mantenuti privati in modo che le sottoclassi non possano sovrascriverli. Sono destinati solo a scopo di serializzazione e mantenerli privati evita eventuali problemi di sicurezza.

Serializzazione con Ereditarietà

A volte è necessario estendere una classe che non implementa l’interfaccia Serializable. Se ci affidiamo al comportamento automatico di serializzazione e la superclasse ha qualche stato, allora essi non verranno convertiti in stream e quindi non saranno recuperati successivamente. Questo è un caso in cui i metodi readObject() e writeObject() sono davvero utili. Fornendo la loro implementazione, possiamo salvare lo stato della superclasse nello stream e poi recuperarlo successivamente. Vediamo questo in azione.

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 è un semplice java bean ma non implementa l’interfaccia 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()+"}";
	}
	
	//aggiunta di un metodo di supporto per la serializzazione per salvare/inizializzare lo stato della superclasse
	private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
		ois.defaultReadObject();
		
		//notare che l'ordine di lettura e scrittura dovrebbe essere lo stesso
		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 {
		//valida l'oggetto qui
		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");
	}	
}

Si noti che l’ordine di scrittura e lettura dei dati aggiuntivi nello stream dovrebbe essere lo stesso. Possiamo inserire della logica nella lettura e scrittura dei dati per renderli sicuri. Notate anche che la classe sta implementando l’interfaccia ObjectInputValidation. Implementando il metodo validateObject(), possiamo inserire delle validazioni di business per assicurarci che l’integrità dei dati non venga compromessa. Scriviamo una classe di test e vediamo se possiamo recuperare lo stato della superclasse dai dati serializzati o no.

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

Quando eseguiamo la classe sopra, otteniamo il seguente output.

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

In questo modo, possiamo serializzare lo stato della superclasse anche se non sta implementando l’interfaccia Serializable. Questa strategia è utile quando la superclasse è una classe di terze parti che non possiamo modificare.

Pattern del Proxy di Serializzazione

La serializzazione in Java presenta alcuni seri problemi come;

  • La struttura della classe non può essere cambiata molto senza compromettere il processo di serializzazione di Java. Quindi, anche se non abbiamo bisogno di alcune variabili in seguito, dobbiamo mantenerle solo per la compatibilità all’indietro.
  • La serializzazione comporta enormi rischi per la sicurezza, un attaccante può modificare la sequenza dello stream e causare danni al sistema. Ad esempio, il ruolo dell’utente viene serializzato e un attaccante cambia il valore dello stream per renderlo amministratore ed eseguire codice dannoso.

Il pattern Java Serialization Proxy è un modo per ottenere una maggiore sicurezza con la serializzazione. In questo pattern, viene utilizzata una classe interna privata e statica come classe proxy per scopi di serializzazione. Questa classe è progettata in modo da mantenere lo stato della classe principale. Questo pattern viene implementato mediante l’implementazione corretta dei metodi readResolve() e writeReplace(). Per una migliore comprensione, scriviamo prima una classe che implementa il pattern di proxy di serializzazione e poi la analizzeremo.

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+"}";
	}
	
	//classe di proxy di serializzazione
	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){
			//oscuramento dei dati per motivi di sicurezza
			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");
		}
		
	}
	
	//sostituzione dell'oggetto serializzato con l'oggetto DataProxy
	private Object writeReplace(){
		return new DataProxy(this);
	}
	
	private void readObject(ObjectInputStream ois) throws InvalidObjectException{
		throw new InvalidObjectException("Proxy is not used, something fishy");
	}
}
  • Sia la classe Data che la classe DataProxy devono implementare l’interfaccia Serializable.
  • DataProxy deve essere in grado di mantenere lo stato dell’oggetto Data.
  • DataProxy è una classe interna privata e statica, quindi altre classi non possono accedervi direttamente.
  • DataProxy deve avere un unico costruttore che prende Data come argomento.
  • La classe Data deve fornire il metodo writeReplace() che restituisce un’istanza di DataProxy. Quindi, quando l’oggetto Data viene serializzato, il flusso restituito è della classe DataProxy. Tuttavia, la classe DataProxy non è visibile all’esterno, quindi non può essere utilizzata direttamente.
  • La classe DataProxy deve implementare il metodo readResolve() che restituisce l’oggetto Data. Quindi, quando la classe Data viene deserializzata, internamente viene deserializzata anche la classe DataProxy e quando viene chiamato il suo metodo readResolve(), otteniamo l’oggetto Data.
  • Infine implementa il metodo readObject() nella classe Data e lancia l’eccezione InvalidObjectException per evitare attacchi di hacker che cercano di fabbricare un flusso di oggetti Data e analizzarlo.

Scriviamo un piccolo test per verificare se l’implementazione funziona o meno.

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

}

Quando eseguiamo la classe sopra, otteniamo l’output seguente nella console.

Data{data=Pankaj}

Se apri il file data.ser, puoi vedere che l’oggetto DataProxy è salvato come flusso nel file.

Scarica il progetto di serializzazione Java

Questo è tutto per la serializzazione in Java, sembra semplice ma dovremmo usarla in modo ponderato ed è sempre meglio non fare affidamento sull’implementazione predefinita. Scarica il progetto dal link sopra e sperimentaci per imparare di più.

Source:
https://www.digitalocean.com/community/tutorials/serialization-in-java