Sérialisation en Java – Sérialisation Java

La sérialisation en Java a été introduite dans JDK 1.1 et c’est l’une des fonctionnalités importantes de Core Java.

La sérialisation en Java

permet de convertir un objet en un flux que nous pouvons envoyer via le réseau, enregistrer dans un fichier ou stocker dans une base de données pour une utilisation ultérieure. La désérialisation est le processus de conversion du flux d’objet en un véritable objet Java à utiliser dans notre programme. La sérialisation en Java semble très facile à utiliser au début, mais elle présente quelques problèmes de sécurité et d’intégrité mineurs que nous examinerons dans la partie ultérieure de cet article. Nous aborderons les sujets suivants dans ce tutoriel.

  1. Sérialisable en Java
  2. Refactoring de classe avec sérialisation et serialVersionUID
  3. Interface Externalizable Java
  4. Méthodes de sérialisation Java
  5. Sérialisation avec héritage
  6. Patron de proxy de sérialisation

Sérialisable en Java

Si vous souhaitez qu’un objet de classe soit sérialisable, tout ce que vous devez faire est de mettre en œuvre l’interface java.io.Serializable. Serializable en java est une interface marqueur et n’a aucun champ ni méthode à implémenter. C’est comme un processus d’adhésion grâce auquel nous rendons nos classes sérialisables. La sérialisation en java est implémentée par ObjectInputStream et ObjectOutputStream, donc tout ce dont nous avons besoin est un wrapper autour d’eux pour soit le sauvegarder dans un fichier, soit l’envoyer sur le réseau. Voyons un exemple simple de programme de sérialisation en 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+"}";
	}
	
	// méthodes getter et 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;
//	}
	
}

Remarquez que c’est un simple bean java avec quelques propriétés et des méthodes getter-setter. Si vous souhaitez qu’une propriété de l’objet ne soit pas sérialisée dans le flux, vous pouvez utiliser le mot-clé transient comme je l’ai fait avec la variable de salaire. Maintenant, supposons que nous voulions écrire nos objets dans un fichier puis les désérialiser à partir du même fichier. Nous avons donc besoin de méthodes utilitaires qui utiliseront ObjectInputStream et ObjectOutputStream à des fins de sérialisation.

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 {

	// désérialiser en objet à partir du fichier donné
	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;
	}

	// sérialiser l'objet donné et le sauvegarder dans le fichier
	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();
	}

}

Remarquez que les arguments de la méthode fonctionnent avec un objet qui est la classe de base de n’importe quel objet Java. C’est écrit de cette manière pour être générique. Maintenant, écrivons un programme de test pour voir la sérialisation Java en action.

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);
		
		// sérialiser dans le fichier
		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);
	}
}

Lorsque nous exécutons le programme de test ci-dessus pour la sérialisation en Java, nous obtenons la sortie suivante.

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

Comme le salaire est une variable transitoire, sa valeur n’a pas été sauvegardée dans le fichier et donc pas récupérée dans le nouvel objet. De même, les valeurs des variables statiques ne sont pas sérialisées car elles appartiennent à la classe et non à l’objet.

Réorganisation de classe avec sérialisation et serialVersionUID

La sérialisation en Java permet certaines modifications dans la classe Java si elles peuvent être ignorées. Certaines des modifications de classe qui n’affecteront pas le processus de désérialisation sont les suivantes :

  • Ajout de nouvelles variables à la classe
  • Changer les variables de transitoire à non-transitoire, pour la sérialisation c’est comme avoir un nouveau champ.
  • Changer la variable de statique à non statique, pour la sérialisation c’est comme avoir un nouveau champ.

Mais pour que toutes ces modifications fonctionnent, la classe Java devrait avoir serialVersionUID défini pour la classe. Écrivons une classe de test spécifique à la désérialisation du fichier déjà sérialisé à partir de la classe de test précédente.

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

Maintenant, décommentez la variable « mot de passe » et ses méthodes getter-setter de la classe Employee et exécutez-la. Vous obtiendrez l’exception suivante;

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

La raison est claire : les serialVersionUID des classes précédente et nouvelle sont différentes. En réalité, si la classe ne définit pas serialVersionUID, il est calculé automatiquement et attribué à la classe. Java utilise des variables de classe, des méthodes, le nom de la classe, le package, etc., pour générer ce nombre long unique. Si vous travaillez avec un IDE, vous obtiendrez automatiquement un avertissement indiquant que « La classe sérialisable Employee ne déclare pas de champ serialVersionUID statique final de type long ». Nous pouvons utiliser l’utilitaire Java « serialver » pour générer le serialVersionUID de la classe Employee en exécutant la commande suivante.

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

Notez qu’il n’est pas nécessaire que la version sérielle soit générée à partir de ce programme lui-même, nous pouvons attribuer cette valeur comme nous le souhaitons. Elle doit simplement être là pour permettre au processus de désérialisation de savoir que la nouvelle classe est la nouvelle version de la même classe et doit être désérialisée si possible. Par exemple, décommentez uniquement le champ serialVersionUID de la classe Employee et exécutez le programme SerializationTest. Décommentez maintenant le champ de mot de passe de la classe Employee et exécutez le programme DeserializationTest, vous verrez que le flux d’objets est désérialisé avec succès car le changement dans la classe Employee est compatible avec le processus de sérialisation.

Interface Java Externalizable

Si vous remarquez le processus de sérialisation Java, il se fait automatiquement. Parfois, nous voulons obscurcir les données de l’objet pour en maintenir l’intégrité. Nous pouvons le faire en implémentant l’interface java.io.Externalizable et en fournissant une implémentation des méthodes writeExternal() et readExternal() à utiliser dans le processus de sérialisation.

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();
		// lire dans le même ordre que écrit
		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;
	}

}

Remarquez que j’ai changé les valeurs des champs avant de les convertir en flux, puis en lisant inversé les changements. De cette manière, nous pouvons maintenir l’intégrité des données dans une certaine mesure. Nous pouvons lever une exception si, après la lecture des données du flux, les contrôles d’intégrité échouent. Écrivons un programme de test pour le voir en action.

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 Bloc catch auto-généré
			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();
		}
	    
	}
}

Lorsque nous exécutons le programme ci-dessus, nous obtenons la sortie suivante.

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

Alors, lequel est-il préférable d’utiliser pour la sérialisation en Java ? En fait, il est préférable d’utiliser l’interface Serializable et d’ici la fin de l’article, vous saurez pourquoi.

Méthodes de sérialisation Java

Nous avons vu que la sérialisation en Java est automatique et tout ce dont nous avons besoin est de mettre en œuvre l’interface Serializable. L’implémentation est présente dans les classes ObjectInputStream et ObjectOutputStream. Mais que se passe-t-il si nous voulons changer la façon dont nous sauvegardons les données, par exemple si nous avons des informations sensibles dans l’objet et avant de sauvegarder/récupérer nous voulons les crypter/décrypter. C’est pourquoi il existe quatre méthodes que nous pouvons fournir dans la classe pour modifier le comportement de sérialisation. Si ces méthodes sont présentes dans la classe, elles sont utilisées à des fins de sérialisation.

  1. readObject(ObjectInputStream ois): Si cette méthode est présente dans la classe, la méthode readObject() de ObjectInputStream utilisera cette méthode pour lire l’objet à partir du flux.
  2. writeObject(ObjectOutputStream oos): Si cette méthode est présente dans la classe, la méthode writeObject() de ObjectOutputStream utilisera cette méthode pour écrire l’objet dans le flux. L’une des utilisations courantes est d’obscurcir les variables de l’objet pour maintenir l’intégrité des données.
  3. Object writeReplace(): Si cette méthode est présente, alors après le processus de sérialisation, cette méthode est appelée et l’objet retourné est sérialisé dans le flux.
  4. Object readResolve(): Si cette méthode est présente, alors après le processus de désérialisation, cette méthode est appelée pour renvoyer l’objet final au programme appelant. L’une des utilisations de cette méthode est d’implémenter le modèle Singleton avec des classes sérialisées. En savoir plus sur la sérialisation et le Singleton.

Généralement, lors de la mise en œuvre des méthodes ci-dessus, elles sont conservées en privé afin que les sous-classes ne puissent pas les remplacer. Elles sont destinées uniquement à des fins de sérialisation et les maintenir privées évite tout problème de sécurité.

Sérialisation avec Héritage

Parfois, nous avons besoin d’étendre une classe qui n’implémente pas l’interface Serializable. Si nous nous fions au comportement de sérialisation automatique et que la superclasse a un certain état, alors ils ne seront pas convertis en flux et donc pas récupérés ultérieurement. C’est un endroit où les méthodes readObject() et writeObject() aident vraiment. En fournissant leur implémentation, nous pouvons sauvegarder l’état de la superclasse dans le flux et le récupérer ultérieurement. Voyons cela en action.

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 est un simple bean Java mais il n’implémente pas l’interface 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()+"}";
	}
	
	//ajout de la méthode d'aide à la sérialisation pour sauvegarder/initialiser l'état de la superclasse
	private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
		ois.defaultReadObject();
		
		//notez que l'ordre de lecture et d'écriture doit être le même
		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 {
		//valider l'objet ici
		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");
	}	
}

Remarquez que l’ordre d’écriture et de lecture des données supplémentaires dans le flux doit être le même. Nous pouvons ajouter de la logique dans la lecture et l’écriture des données pour les sécuriser. Remarquez également que la classe implémente l’interface ObjectInputValidation. En implémentant la méthode validateObject(), nous pouvons ajouter des validations métier pour nous assurer que l’intégrité des données n’est pas compromis. Écrivons une classe de test et voyons si nous pouvons récupérer l’état de la super classe à partir des données sérialisées ou non.

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

Lorsque nous exécutons la classe ci-dessus, nous obtenons la sortie suivante.

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

De cette manière, nous pouvons sérialiser l’état de la super classe même si elle n’implémente pas l’interface Serializable. Cette stratégie est utile lorsque la super classe est une classe tierce que nous ne pouvons pas modifier.

Modèle de proxy de sérialisation

La sérialisation en Java présente des inconvénients sérieux tels que;

  • La structure de la classe ne peut pas être modifiée beaucoup sans casser le processus de sérialisation Java. Donc, même si nous n’avons pas besoin de certaines variables plus tard, nous devons les conserver uniquement pour des raisons de compatibilité ascendante.
  • La sérialisation entraîne de graves risques de sécurité, un attaquant peut modifier la séquence du flux et causer des dommages au système. Par exemple, le rôle de l’utilisateur est sérialisé et un attaquant change la valeur du flux pour le rendre administrateur et exécuter un code malveillant.

Le modèle de proxy de sérialisation Java est une façon d’obtenir une meilleure sécurité avec la sérialisation. Dans ce modèle, une classe interne privée et statique est utilisée comme classe proxy à des fins de sérialisation. Cette classe est conçue de manière à maintenir l’état de la classe principale. Ce modèle est mis en œuvre en implémentant correctement les méthodes readResolve() et writeReplace(). Commençons par écrire une classe qui met en œuvre le modèle de proxy de sérialisation, puis nous l’analyserons pour une meilleure compréhension.

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 proxy de sérialisation
	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){
			//obscurcissement des données pour des raisons de sécurité
			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");
		}
		
	}
	
	//remplacement de l'objet sérialisé par un objet DataProxy
	private Object writeReplace(){
		return new DataProxy(this);
	}
	
	private void readObject(ObjectInputStream ois) throws InvalidObjectException{
		throw new InvalidObjectException("Proxy is not used, something fishy");
	}
}
  • Les classes Data et DataProxy doivent toutes deux implémenter l’interface Serializable.
  • DataProxy doit être capable de maintenir l’état de l’objet Data.
  • DataProxy est une classe interne privée et statique, donc les autres classes ne peuvent pas y accéder.
  • DataProxy doit avoir un seul constructeur prenant Data en argument.
  • La classe Data doit fournir une méthode writeReplace() qui renvoie une instance de DataProxy. Ainsi, lorsque l’objet Data est sérialisé, le flux renvoyé est de la classe DataProxy. Cependant, la classe DataProxy n’est pas visible à l’extérieur, elle ne peut donc pas être utilisée directement.
  • La classe DataProxy doit implémenter la méthode readResolve() qui renvoie un objet Data. Ainsi, lorsque la classe Data est désérialisée, DataProxy est désérialisé en interne et lorsque sa méthode readResolve() est appelée, nous obtenons l’objet Data.
  • Enfin, implémentez la méthode readObject() dans la classe Data et lancez une InvalidObjectException pour éviter les attaques des hackers tentant de fabriquer un flux d’objets Data et de l’analyser.

Écrivons un petit test pour vérifier si l’implémentation fonctionne ou non.

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

}

Lorsque nous exécutons la classe ci-dessus, nous obtenons la sortie suivante dans la console.

Data{data=Pankaj}

Si vous ouvrez le fichier data.ser, vous verrez que l’objet DataProxy est enregistré sous forme de flux dans le fichier.

Téléchargez le projet de sérialisation Java

C’est tout pour la sérialisation en Java, cela semble simple, mais nous devrions l’utiliser judicieusement et il est toujours préférable de ne pas dépendre de l’implémentation par défaut. Téléchargez le projet depuis le lien ci-dessus et explorez-le pour en apprendre davantage.

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