Java Language
La sérialisation
Recherche…
Introduction
Java fournit un mécanisme, appelé sérialisation d'objet, dans lequel un objet peut être représenté sous la forme d'une séquence d'octets incluant les données de l'objet, ainsi que des informations sur le type de l'objet et les types de données stockés dans l'objet.
Une fois qu'un objet sérialisé a été écrit dans un fichier, il peut être lu à partir du fichier et désérialisé, ce qui signifie que les informations de type et les octets qui représentent l'objet et ses données peuvent être utilisés pour recréer l'objet en mémoire.
Sérialisation de base en Java
Qu'est-ce que la sérialisation
La sérialisation est le processus consistant à convertir l'état d'un objet (y compris ses références) en une séquence d'octets, ainsi que le processus de reconstruction de ces octets en un objet actif à une date ultérieure. La sérialisation est utilisée lorsque vous souhaitez conserver l'objet. Il est également utilisé par Java RMI pour transmettre des objets entre JVM, soit en tant qu'arguments dans une invocation de méthode depuis un client vers un serveur, soit en tant que valeurs de retour depuis une invocation de méthode, soit en tant qu'exceptions lancées par des méthodes distantes. En général, la sérialisation est utilisée lorsque nous voulons que l'objet existe au-delà de la durée de vie de la machine virtuelle Java.
java.io.Serializable
est une interface de marqueur (sans corps). Il est juste utilisé pour "marquer" les classes Java comme sérialisables.
Le runtime de sérialisation associe à chaque classe sérialisable un numéro de version, appelé serialVersionUID
, utilisé lors de la désérialisation pour vérifier que l'expéditeur et le destinataire d'un objet sérialisé ont des classes chargées pour cet objet compatibles avec la sérialisation. Si le récepteur a chargé une classe pour l'objet ayant un serialVersionUID
différent de celui de la classe de l'expéditeur correspondant, la désérialisation entraînera une InvalidClassException
. Une classe sérialisable peut déclarer explicitement son propre serialVersionUID
en déclarant un champ nommé serialVersionUID
qui doit être static, final,
et de type long
:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 1L;
Comment rendre une classe éligible pour la sérialisation
Pour conserver un objet, la classe respective doit implémenter l'interface java.io.Serializable
.
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;
}
}
Comment écrire un objet dans un fichier
Maintenant, nous devons écrire cet objet dans un système de fichiers. Nous utilisons java.io.ObjectOutputStream
à cette fin.
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();
}
}
}
Comment recréer un objet à partir de son état sérialisé
L'objet stocké peut être lu à partir du système de fichiers plus tard en utilisant java.io.ObjectInputStream
comme indiqué ci-dessous:
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());
}
}
La classe sérialisée est sous forme binaire. La désérialisation peut être problématique si la définition de classe change: voir le chapitre Gestion des versions des objets sérialisés de la spécification de sérialisation Java pour plus de détails.
La sérialisation d'un objet sérialise l'ensemble du graphe d'objet dont il est la racine et fonctionne correctement en présence de graphes cycliques. Une méthode reset()
est fournie pour forcer ObjectOutputStream
à oublier les objets qui ont déjà été sérialisés.
Champs transitoires - Sérialisation
Sérialisation avec Gson
La sérialisation avec Gson est facile et produira un JSON correct.
public class Employe {
private String firstName;
private String lastName;
private int age;
private BigDecimal salary;
private List<String> skills;
//getters and setters
}
(Sérialisation)
//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"]}
Notez que vous ne pouvez pas sérialiser les objets avec des références circulaires car cela se traduira par une récursion infinie.
(Désérialisation)
//it's very simple...
//Assuming that json is the previous String object....
Employe obj2 = gson.fromJson(json, Employe.class); // obj2 is just like obj
Sérialisation avec Jackson 2
Voici une implémentation qui montre comment un objet peut être sérialisé dans sa chaîne JSON correspondante.
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;
}
}
Sérialisation:
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
}
Sortie:
{
"idx" : 1,
"name" : "abc"
}
Vous pouvez omettre l'imprimante Pretty Default si vous n'en avez pas besoin.
La dépendance utilisée ici est la suivante:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.6.3</version>
</dependency>
Sérialisation personnalisée
Dans cet exemple, nous voulons créer une classe qui générera et sortira en console, un nombre aléatoire compris entre une plage de deux nombres entiers qui seront transmis en tant qu’arguments lors de l’initialisation.
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();
}
}
}
}
Maintenant, si nous voulons rendre cette classe sérialisable, il y aura des problèmes. Le thread est l'une des classes de niveau système qui ne sont pas Serializable. Nous devons donc déclarer le thread comme transitoire . En faisant cela, nous serons en mesure de sérialiser les objets de cette classe mais nous aurons toujours un problème. Comme vous pouvez le voir dans le constructeur, nous définissons les valeurs min et max de notre randomiseur et ensuite nous démarrons le thread qui est responsable de la génération et de l'impression de la valeur aléatoire. Ainsi, lors de la restauration de l'objet persistant en appelant le readObject (), le constructeur ne sera plus exécuté car il n'y a pas de création d'un nouvel objet. Dans ce cas, nous devons développer une sérialisation personnalisée en fournissant deux méthodes à l'intérieur de la classe. Ces méthodes sont:
private void writeObject(ObjectOutputStream out) throws IOException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
Ainsi, en ajoutant notre implémentation dans readObject (), nous pouvons lancer et démarrer notre thread:
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();
}
}
Voici le principal pour notre exemple:
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();
}
}
}
Si vous exécutez la commande principale, vous verrez qu'il y a deux threads en cours d'exécution pour chaque instance RangeRandom, car la méthode Thread.start () se trouve maintenant à la fois dans le constructeur et dans readObject () .
Versioning et serialVersionUID
Lorsque vous implémentez l'interface java.io.Serializable
pour rendre une classe sérialisable, le compilateur recherche un static final
nommé serialVersionUID
de type long
. Si la classe n'a pas ce champ déclaré explicitement, le compilateur créera un tel champ et l'attribuera avec une valeur qui sort d'un calcul de serialVersionUID
dépendant de l' serialVersionUID
. Ce calcul dépend de divers aspects de la classe et suit les spécifications de sérialisation d’objet données par Sun. Cependant, la valeur n'est pas garantie pour toutes les implémentations du compilateur.
Cette valeur est utilisée pour vérifier la compatibilité des classes par rapport à la sérialisation et cela lors de la désérialisation d'un objet enregistré. Serialization Runtime vérifie que serialVersionUID
lu à partir des données serialVersionUID
et que serialVersionUID
déclaré dans la classe sont exactement les mêmes. Si ce n'est pas le cas, il génère une InvalidClassException
.
Il est fortement recommandé de déclarer et d'initialiser explicitement le champ statique de type long et nommé 'serialVersionUID' dans toutes les classes que vous souhaitez rendre Serializable au lieu de compter sur le calcul par défaut de la valeur pour ce champ même si vous ne voulez pas utiliser le contrôle de version. Le calcul 'serialVersionUID' est extrêmement sensible et peut varier d'une implémentation de compilateur à l'autre. Vous pouvez donc obtenir l' InvalidClassException
même pour la même classe simplement parce que vous avez utilisé différentes implémentations de compilateur.
public class Example implements Serializable {
static final long serialVersionUID = 1L /*or some other value*/;
//...
}
Tant que serialVersionUID
est identique, la sérialisation Java peut gérer différentes versions d'une classe. Les changements compatibles et incompatibles sont;
Changements compatibles
- Ajouter des champs: Lorsque la classe en cours de reconstitution comporte un champ qui ne figure pas dans le flux, ce champ dans l'objet sera initialisé à la valeur par défaut pour son type. Si une initialisation spécifique à une classe est nécessaire, la classe peut fournir une méthode readObject capable d'initialiser le champ à des valeurs autres que celles par défaut.
- Ajout de classes: le flux contiendra la hiérarchie de types de chaque objet du flux. La comparaison de cette hiérarchie dans le flux avec la classe en cours peut détecter des classes supplémentaires. Comme il n'y a aucune information dans le flux à partir de laquelle initialiser l'objet, les champs de la classe seront initialisés aux valeurs par défaut.
- Suppression de classes: La comparaison de la hiérarchie de classes dans le flux avec celle de la classe en cours peut détecter qu'une classe a été supprimée. Dans ce cas, les champs et les objets correspondant à cette classe sont lus dans le flux. Les champs primitifs sont ignorés, mais les objets référencés par la classe supprimée sont créés, car ils peuvent être référencés ultérieurement dans le flux. Ils seront récupérés lorsque le flux est récupéré ou réinitialisé.
- Ajouter des méthodes writeObject / readObject: Si la version lisant le flux possède ces méthodes, readObject doit, comme d'habitude, lire les données requises écrites dans le flux par la sérialisation par défaut. Il doit d'abord appeler defaultReadObject avant de lire les données facultatives. La méthode writeObject devrait normalement appeler defaultWriteObject pour écrire les données requises, puis écrire des données facultatives.
- Ajouter java.io.Serializable: Cela équivaut à ajouter des types. Il n'y aura aucune valeur dans le flux pour cette classe, donc ses champs seront initialisés aux valeurs par défaut. La prise en charge des sous-classes de classes non sérialisables nécessite que le sur-type de la classe ait un constructeur sans argument et que la classe elle-même soit initialisée aux valeurs par défaut. Si le constructeur no-arg n'est pas disponible, l'exception InvalidClassException est levée.
- Modification de l'accès à un champ: Les modificateurs d'accès public, package, protected et private n'ont aucun effet sur la capacité de la sérialisation à affecter des valeurs aux champs.
- Changer un champ statique en non statique ou transitoire en non-transitoire: lorsque vous utilisez la sérialisation par défaut pour calculer les champs sérialisables, cette modification équivaut à ajouter un champ à la classe. Le nouveau champ sera écrit dans le flux, mais les classes précédentes ignoreront la valeur car la sérialisation n'affectera pas de valeurs aux champs statiques ou transitoires.
Changements incompatibles
- Suppression de champs: Si un champ est supprimé dans une classe, le flux écrit ne contiendra pas sa valeur. Lorsque le flux est lu par une classe antérieure, la valeur du champ est définie sur la valeur par défaut car aucune valeur n'est disponible dans le flux. Cependant, cette valeur par défaut peut compromettre la capacité de la version antérieure à respecter son contrat.
- Déplacement des classes vers le haut ou le bas de la hiérarchie: cela ne peut pas être autorisé car les données du flux apparaissent dans la mauvaise séquence.
- Changer un champ non statique en statique ou un champ non-transitoire en transitoire: en cas de sérialisation par défaut, cette modification équivaut à supprimer un champ de la classe. Cette version de la classe n'écrira pas ces données dans le flux, elle ne sera donc pas disponible pour être lue par les versions antérieures de la classe. Comme lors de la suppression d'un champ, le champ de la version antérieure sera initialisé à la valeur par défaut, ce qui peut entraîner un échec inattendu de la classe.
- Modification du type déclaré d'un champ primitif: Chaque version de la classe écrit les données avec son type déclaré. Les versions antérieures de la classe qui tentent de lire le champ échoueront car le type des données du flux ne correspond pas au type du champ.
- Changer la méthode writeObject ou readObject pour qu'elle n'écrive plus ou ne lise plus les données de champ par défaut ou ne les modifie pas de manière à ce que celle-ci tente de l'écrire ou de la lire lorsque la version précédente ne l'a pas fait. Les données de champ par défaut doivent toujours apparaître ou ne pas apparaître dans le flux.
- Changer une classe de Serializable à Externalizable ou vice versa est un changement incompatible puisque le flux contiendra des données incompatibles avec l'implémentation de la classe disponible.
- Changer une classe d'un type non-enum en un type enum ou vice versa puisque le flux contiendra des données incompatibles avec l'implémentation de la classe disponible.
- La suppression de Serializable ou Externalizable est une modification incompatible car, une fois écrite, elle ne fournira plus les champs requis par les anciennes versions de la classe.
- L'ajout de la méthode writeReplace ou readResolve à une classe est incompatible si le comportement produit un objet incompatible avec une version antérieure de la classe.
Désérialisation JSON personnalisée avec Jackson
Nous consommons l'API de repos en tant que format JSON, puis la désactivons sur un POJO. Le fichier org.codehaus.jackson.map.ObjectMapper de Jackson fonctionne simplement et nous ne faisons rien dans la plupart des cas. Mais parfois, nous avons besoin d'un désérialiseur personnalisé pour répondre à nos besoins personnalisés et ce tutoriel vous guidera tout au long du processus de création de votre propre désérialiseur.
Disons que nous avons des entités suivantes.
public class User {
private Long id;
private String name;
private String email;
//getter setter are omitted for clarity
}
Et
public class Program {
private Long id;
private String name;
private User createdBy;
private String contents;
//getter setter are omitted for clarity
}
Sérialisons / marshalons un objet en premier.
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 (programme); System.out.println (json);
Le code ci-dessus produira après JSON-
{
"id": 1,
"name": "Program @# 1",
"createdBy": {
"id": 1,
"name": "Bazlur Rahman",
"email": "[email protected]"
},
"contents": "Some contents"
}
Maintenant, peut faire le contraire très facilement. Si nous avons ce JSON, nous pouvons supprimer un objet programme en utilisant ObjectMapper comme suit -
Maintenant, disons que ce n'est pas le cas réel, nous allons avoir un JSON différent d'une API qui ne correspond pas à notre classe de Program
.
{
"id": 1,
"name": "Program @# 1",
"ownerId": 1
"contents": "Some contents"
}
Regardez la chaîne JSON, vous pouvez voir, il a un champ différent qui est owenerId.
Maintenant, si vous souhaitez sérialiser ce JSON comme nous l'avons fait précédemment, vous aurez des exceptions.
Il existe deux manières d’éviter les exceptions et d’avoir cette publication en série -
Ignorer les champs inconnus
Ignorer le onwerId
. Ajouter l'annotation suivante dans la classe Programme
@JsonIgnoreProperties(ignoreUnknown = true)
public class Program {}
Ecrire un désérialiseur personnalisé
Mais il y a des cas où vous avez réellement besoin de ce champ owerId
. Disons que vous voulez le relier comme identifiant de la classe User
.
Dans ce cas, vous devez écrire un désérialiseur personnalisé.
Comme vous pouvez le voir, vous devez d'abord accéder au JsonNode
depuis le JonsParser
. Et puis, vous pouvez facilement extraire des informations d'un JsonNode
utilisant la méthode get()
. et vous devez vous assurer du nom du champ. Ce devrait être le nom exact, la faute d'orthographe entraînera des exceptions.
Et enfin, vous devez enregistrer votre ProgramDeserializer dans ObjectMapper
.
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);
Vous pouvez également utiliser l’annotation pour enregistrer le désérialiseur directement -
@JsonDeserialize(using = ProgramDeserializer.class)
public class Program {
}