Accès rapide :
La vidéo
Qu'est-ce que la sérialisation ?
Sérialisation/désérialisation JSON de données simples
Sérialisation de données simples
Désérialisation de données simples
Sérialisation/désérialisation JSON de tableaux et de collections
Sérialisation de tableaux et de collections
Désérialisation de tableaux et de collections
Sérialisation/désérialisation JSON d'un objet Java
Sérialisation d'objets
Désérialisation d'objets à partir d'un flux JSON
Conclusion
Cette vidéo vous montre comment coder un moteur de sérialisation/désérialisation, au format JSON, en utilisant la réflexion Java.
En POO (Programmation Orientée Objet), la sérialisation est le mécanisme qui permet de transformer un objet (quelle que soit sa complexité) en un flux séquentiel d'octets. Cela permet notamment de transférer un objet sur une machine distante ou encore de sauvegarder un objet dans un fichier. L'opération inverse, la désérialisation, est le mécanisme qui permet de restructurer un objet à partir d'un flux séquentiel d'octets.
Java propose déjà des mécanismes de sérialisation et de désérialisation. Pour autant, et dans le but d'apprendre à utiliser le moteur de réflexion, nous allons coder notre propre système de sérialisation/désérialisation au format JSON. Effectivement, pour pouvoir sauvegarder n'importe quelle instance Java dans un format JSON, il est nécessaire de pouvoir en comprendre sa structure.
Comme proposé, nous allons travailler avec le format JSON (JavaScript Object Notation). C'est un format très simple, basé sur la syntaxe de JavaScript (et donc un peu de Java), concurrent du format XML. Une paire d'accolades représente un objet et chaque attribut y apparaît sous forme d'une association clé/valeur. Voici un exemple de flux JSON contenant les informations d'un article (pour un site de vente en ligne, pourquoi pas).
{ "idArticle": 1, "description": "an article", "brand": "a brand", "price": 15 }
Sérialiser un objet Java quelconque en JSON n'est pas si compliqué que cela, mais ce n'est pas complétement simple non plus. C'est pour cette raison que nous allons travailler en plusieurs étapes.
Dans un premier temps, nous allons traiter les données simples, telles que les types primitifs ou les chaînes de caractères.
Ensuite nous verrons comment traiter les tableaux séquentiels (tableaux d'entiers, de flottants, ...) ou des collections Java (ArrayList
, ...).
La dernière étape consistera à traiter les objets Java. Nous utiliserons la récursivité pour sérialiser la valeur de chaque attribut.
Dans un premier temps, nous ne considérerons que des données simples telles que les types primitifs Java ou les chaînes de caractères.
Comme nous l'avons précisé dans certains chapitres précédents, les types primitifs ne sont pas des objets, mais peuvent quand même être contenue par des
instances des classes enveloppantes (wrapper classes ; classe Integer
pour le type int
, ...).
On parle d'auto-boxing pour l'acte d'envelopper un type primitif dans une classe enveloppante, et d'unboxing pour l'opération inverse.
Dans un souci de simplification, je ne vais pas faire jouer la surcharge, mais plutôt utilisé l'auto-boxing, l'unboxing et le polymorphisme pour gérer les types primitifs.
Le code suivant gère l'écriture de données simple dans un fichier texte au format JSON.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
package fr.koor.json; import java.io.File; import java.io.FileReader; import java.io.PrintWriter; public class SerializationEngine { public static void writeObject( Object object, PrintWriter writer ) throws Exception { Class<?> metadata = object.getClass(); if ( metadata == Byte.class || metadata == Short.class || metadata == Integer.class || metadata == Long.class || metadata == Float.class || metadata == Double.class || metadata == Boolean.class ) { writer.print( "" + object ); } else if ( metadata == String.class || metadata == Character.class ) { writer.print( "\"" + object + "\"" ); } else { throw new Exception( "Not actually implemented" ); } } public static void main( String[] args ) throws Exception { String file = "./file.json"; try ( PrintWriter writer = new PrintWriter( file ) ) { //SerializationEngine.writeObject( 3, writer ); //SerializationEngine.writeObject( 3.1415, writer ); SerializationEngine.writeObject( "Hello", writer ); } } } |
char
de Java. Nous transformerons donc les char
Java en chaînes de caractères JSON.
Pour rappel, la classe enveloppante du type char
est java.lang.Character
.
Il nous faut maintenant coder l'opération inverse : la relecture de données typées à partir d'un fichier JSON.
Par symétrie avec l'opération de sérialisation, je vais proposer une méthode de désérialisable travaillant sur un Reader
.
Pour autant, cette méthode s'appuiera sur une seconde méthode opérant sur une instance de type java.util.Scanner
: cela nous simplifiera
grandement la tâche pour décoder les données du flux JSON.
java.util.Scanner
permet de sortir des éléments syntaxiques (des tokens) d'un flux textuel en se basant sur des expressions régulières.
Bien que la classe Scanner
expose directement des méthodes d'extraction de données basées sur de types primitifs, nous n'allons pas
les utiliser. La raison principale est que ces méthodes nécessitent un séparateur (un blanc) à la suite de la donnée à extraire. Cela nous posera des
problèmes un peu plus tard. A la place, nous allons utiliser la méthode findInLine
qui accepte une expression régulière pour qualifier la
donnée à extraire.
Ajouter ces deux méthodes à votre classe SerializationEngine
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
private static Object readObject( Class<?> metadata, Scanner scanner ) throws Exception { if ( metadata == Byte.class || metadata == byte.class ) { return Byte.parseByte( scanner.findInLine( "[0-9]+" ) ); } else if ( metadata == Short.class || metadata == short.class ) { return Short.parseShort( scanner.findInLine( "[0-9]+" ) ); } else if ( metadata == Integer.class || metadata == int.class ) { return Integer.parseInt( scanner.findInLine( "[0-9]+" ) ); } else if ( metadata == Long.class || metadata == long.class ) { return Long.parseLong( scanner.findInLine( "[0-9]+" ) ); } else if ( metadata == Float.class || metadata == float.class ) { return Float.parseFloat( scanner.findInLine( "[0-9]+(\\.[0-9]+)?" ) ); } else if ( metadata == Double.class || metadata == double.class ) { return Double.parseDouble( scanner.findInLine( "[0-9]+(\\.[0-9]+)?" ) ); } else if ( metadata == Boolean.class || metadata == boolean.class ) { return Boolean.parseBoolean( scanner.findInLine( "true|false" ) ); } else if ( metadata == String.class || metadata == Character.class || metadata == char.class ) { String value = scanner.findInLine( "\".*?\"" ); return value.substring( 1, value.length()-1 ); } else { throw new Exception( "Not actually implemented" ); } } // J'introduis ici la généricité pour plus de confort. @SuppressWarnings( "unchecked" ) public static <T> T readObject( Class<T> metadata, Reader reader ) throws Exception { try ( Scanner scanner = new Scanner( reader ) ) { scanner.useLocale(Locale.US); // double stocké avec le caractère . (et non la virgule) return (T) readObject( metadata, scanner ); } } |
Et modifiez maintenant votre méthode main
ainsi afin de tester quelques cas.
1 2 3 4 5 6 7 8 9 10 11 |
public static void main( String[] args ) throws Exception { String file = "./file.json"; try ( FileReader reader = new FileReader( new File( file ) ) ) { //double data = SerializationEngine.readObject( Double.class, reader ); String data = SerializationEngine.readObject( String.class, reader ); System.out.println( data ); } } |
Nous allons maintenant modifier notre code afin de permettre la gestion des tableaux Java ou des collections compatibles avec l'interface
java.util.List
.
Pour rappel, le format JSON introduit un tableau de données via la syntaxe [ ]
. Voici un exemple de flux JSON contenant un tableau de
chaînes de caractères.
["Java", "Python", "C#", "C++"]
Dans le but de nous simplifier la vie (ça ne reste qu'un prototype), nous allons gérer quasiment de la même manière les tableaux et les collections. Pour ce faire nous allons transformer les tableaux Java en collections et nous les sérialiserons dans le flux. Mais nous avons une difficulté. Il n'est pas trivial de transformer un tableau basé sur un type primitif en une collection de wrapper. Je propose de créer une méthode utilitaire prenant en charge ce besoin : en voici son code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
package fr.koor.json; import java.util.ArrayList; import java.util.List; public class ArrayUtils { public static List<Object> toObjectList( Object array ) throws Exception { List<Object> result = new ArrayList<>(); if ( array instanceof byte[] ) { for ( byte value : (byte[]) array ) result.add( value ); } else if ( array instanceof short[] ) { for ( short value : (short[]) array ) result.add( value ); } else if ( array instanceof int[] ) { for ( int value : (int[]) array ) result.add( value ); } else if ( array instanceof long[] ) { for ( long value : (long[]) array ) result.add( value ); } else if ( array instanceof float[] ) { for ( float value : (float[]) array ) result.add( value ); } else if ( array instanceof double[] ) { for ( double value : (double[]) array ) result.add( value ); } else if ( array instanceof boolean[] ) { for ( boolean value : (boolean[]) array ) result.add( value ); } else if ( array instanceof char[] ) { for ( char value : (char[]) array ) result.add( value ); } else if ( array.getClass().isArray() ) { for ( Object value : (Object[]) array ) result.add( value ); } else { throw new Exception( "Not supported" ); } return result; } } |
Nous pouvons maintenant modifier le code de notre méthode de sérialisation pour y ajouter la gestion des collections et des tableaux. Pour chaque élément de l'ensemble, nous ré-invoquerons récursivement la méthode de sérialisation. Voici le code cette méthode.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public static void writeObject( Object object, PrintWriter writer ) throws Exception { Class<?> metadata = object.getClass(); if ( metadata == Byte.class || metadata == Short.class || metadata == Integer.class || metadata == Long.class || metadata == Float.class || metadata == Double.class || metadata == Boolean.class ) { // --- On gère les types de bases --- writer.print( "" + object ); } else if ( metadata == String.class || metadata == Character.class ) { // --- On gère les chaînes de caractères --- writer.print( "\"" + object + "\"" ); } else if ( metadata.isArray() || object instanceof List ) { // --- On gère les tableaux et les collections --- @SuppressWarnings("rawtypes") List collection = object instanceof List ? (List) object : ArrayUtils.toObjectList( object ) ; int size = collection.size(); int i = 0; writer.print( "[" ); for( Object value : collection ) { writeObject( value, writer ); if ( i++ < size - 1 ) writer.print( "," ); } writer.print( "]" ); } else { // --- On gère les objets Java --- throw new Exception( "Not actually implemented" ); } } |
Il ne reste plus qu'à tester la sérialisation d'un tableau de chaînes de caractères.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public static void main( String[] args ) throws Exception { String file = "./file.json"; try ( PrintWriter writer = new PrintWriter( file ) ) { //double [] values = { 14.2, 15.3, 16.4 }; //SerializationEngine.writeObject( values, writer ); //List<String> languages = Arrays.asList( "Java", "Python", "C#", "C++" ); //SerializationEngine.writeObject( languages, writer ); String [] languages = { "Java", "Python", "C#", "C++" }; SerializationEngine.writeObject( languages, writer ); } } |
Et voici le contenu du fichier produit par cet exemple.
["Java","Python","C#","C++"]
L'opération inverse (la relecture de tableaux ou de collections à partir d'un flux JSON) est un petit peu plus complexe et va bien au-delà de ce que je veux vous montrer ici. Je préfère donc ignorer cette partie.
En guise d'exemple, je vous propose de travailler avec une classe Article
: elle contient quatre attributs
(un entier, deux chaînes de caractères et un flottant). En voici son code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class Article { private int idArticle; private String description; private String brand; private double price; // Requis pour le moteur de sérialisation/désérialisation JSON public Article() { this( 1, "unknown", "unknown", 0 ); } public Article( int idArticle, String description, String brand, double price ) { this.idArticle = idArticle; this.description = description; this.brand = brand; this.price = price; } @Override public String toString() { return "Article [idArticle=" + idArticle + ", description=" + description + ", brand=" + brand + ", price=" + price + "]"; } } |
Comme vous l'aurez remarqué, les attributs de cette classe sont privés : il va nous falloir forcer l'accès à ces attributs via le moteur de réflexion. Pour chaque attribut, nous rappellerons récursivement la méthode sérialisation pour envoyé sa valeur dans le flux JSON.
Je vous propose de modifier ainsi le code de la méthode de sérialisation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public static void writeObject( Object object, PrintWriter writer ) throws Exception { Class<?> metadata = object.getClass(); if ( metadata == Byte.class || metadata == Short.class || metadata == Integer.class || metadata == Long.class || metadata == Float.class || metadata == Double.class || metadata == Boolean.class ) { // --- On gère les types de bases --- writer.print( "" + object ); } else if ( metadata == String.class || metadata == Character.class ) { // --- On gère les chaînes de caractères --- writer.print( "\"" + object + "\"" ); } else if ( metadata.isArray() || object instanceof List ) { // --- On gère les tableaux et les collections --- @SuppressWarnings("rawtypes") List collection = object instanceof List ? (List) object : ArrayUtils.toObjectList( object ) ; int size = collection.size(); int i = 0; writer.print( "[" ); for( Object value : collection ) { writeObject( value, writer ); if ( i++ < size - 1 ) writer.print( "," ); } writer.print( "]" ); } else { // --- On gère les objets Java --- writer.write( "{" ); Field [] fields = metadata.getDeclaredFields(); for ( int i=0; i<fields.length; i++ ) { Field field = fields[i]; field.setAccessible( true ); writer.write( String.format( "\"%s\": ", field.getName() ) ); writeObject( field.get( object ), writer ); if ( i < fields.length-1 ) writer.write( ", " ); } writer.write( "}" ); } } |
Modifions la méthode main
afin de tester l'envoi d'un objet dans le flux JSON.
1 2 3 4 5 6 7 8 |
public static void main( String[] args ) throws Exception { String file = "./file.json"; try ( PrintWriter writer = new PrintWriter( file ) ) { Article article = new Article(); SerializationEngine.writeObject( article, writer ); } } |
Voici le contenu du fichier JSON produit par cet exemple.
{"idArticle": 1, "description": "unknown", "brand": "unknown", "price": 0.0}
Nous l'avons vu, la méthode writeObject
est récursive. Donc si une classe définie des attributs basés sur d'autres classes,
normalement, la grappe d'objets doit être envoyée sur le flux. Considérons cette classe de démonstration (le tableau et juste là pour tester ce cas particulier).
1 2 3 4 5 |
class CommandLine { private Article article = new Article(); private int [] data = { 10, 20, 30 }; private int quantity; } |
Modifions de nouveau le main afin d'envoyer une instance de la classe CommandLine
.
1 2 3 4 5 6 7 8 |
public static void main( String[] args ) throws Exception { String file = "./file.json"; try ( PrintWriter writer = new PrintWriter( file ) ) { CommandLine commandLine = new CommandLine(); SerializationEngine.writeObject( commandLine, writer ); } } |
Et voici le contenu du nouveau fichier JSON produit.
{"article": {"idArticle": 1, "description": "unknown", "brand": "unknown", "price": 0.0}, "data": [10,20,30], "quantity": 0}
Nous allons maintenant voir comment recharger un objet à partir d'un flux JSON :
je vous propose de modifier le code de la méthode readObject
ainsi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
private static Object readObject( Class<?> metadata, Scanner scanner ) throws Exception { if ( metadata == Byte.class || metadata == byte.class ) { return Byte.parseByte( scanner.findInLine( "[0-9]+" ) ); } else if ( metadata == Short.class || metadata == short.class ) { return Short.parseShort( scanner.findInLine( "[0-9]+" ) ); } else if ( metadata == Integer.class || metadata == int.class ) { return Integer.parseInt( scanner.findInLine( "[0-9]+" ) ); } else if ( metadata == Long.class || metadata == long.class ) { return Long.parseLong( scanner.findInLine( "[0-9]+" ) ); } else if ( metadata == Float.class || metadata == float.class ) { return Float.parseFloat( scanner.findInLine( "[0-9]+(\\.[0-9]+)?" ) ); } else if ( metadata == Double.class || metadata == double.class ) { return Double.parseDouble( scanner.findInLine( "[0-9]+(\\.[0-9]+)?" ) ); } else if ( metadata == Boolean.class || metadata == boolean.class ) { return Boolean.parseBoolean( scanner.findInLine( "true|false" ) ); } else if ( metadata == String.class || metadata == Character.class || metadata == char.class ) { String value = scanner.findInLine( "\".*?\"" ); return value.substring( 1, value.length()-1 ); } else if ( metadata.isArray() || List.class.isAssignableFrom( metadata ) ) { // --- Désérialisation de tableaux ou de collections --- throw new Exception( "Not actually implemented" ); } else { // --- Désérialisation d'un objet --- Object object = metadata.newInstance(); scanner.findInLine( "\\{\\s*" ); while( ! scanner.hasNext( "\\s*\\}" ) ) { String attributeName = (String) readObject( String.class, scanner ); scanner.findInLine( ":\\s*" ); Field field = metadata.getDeclaredField( attributeName ); field.setAccessible( true ); Object value = readObject( field.getType(), scanner ); field.set( object, value ); } scanner.findInLine( "\\s*\\}" ); return object; } } |
Et voici un exemple d'utilisation de notre méthode de désérialisation d'objet pour recharger un objet de type Article.
1 2 3 4 |
try ( FileReader reader = new FileReader( new File( file ) ) ) { Article article = SerializationEngine.readObject( Article.class, reader ); System.out.println( article ); } |
Bien entendu, ce moteur de sérialisation JSON reste perfectible (ce n'est qu'une démonstration) et je vous invite à poursuivre son amélioration. Les points majeurs à améliorer sont :
Il faudrait terminer la désérialisation des tableaux.
En cas de sérialisation d'objets, nous ne traitons actuellement que les attributs directement définis dans la classe considérée. Mais en cas d'héritage il faudrait aussi tenir compte des attributs des classes parentes.
Cela sera un très bon exercice pour vous de poursuivre son implémentation. N'hésitez pas à proposer vous améliorations dans les commentaires ci-dessous afin d'en faire profiter les autres lecteurs.
Sachez aussi qu'il existe déjà de nombreuses implémentations de moteurs de sérialisation prêtes à l'emploi : on peut citer, en autres, Jackson, Gson, Genson ... Personnellement, j'utilise beaucoup Genson qui est très simple d'emploi : http://genson.io/.
Pour finir, voici le code complet de l'exemple proposé dans ce tuto.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
package fr.koor.json; import java.io.File; import java.io.FileReader; import java.io.PrintWriter; import java.io.Reader; import java.lang.reflect.Field; import java.util.List; import java.util.Locale; import java.util.Scanner; public class SerializationEngine { public static void writeObject( Object object, PrintWriter writer ) throws Exception { Class<?> metadata = object.getClass(); if ( metadata == Byte.class || metadata == Short.class || metadata == Integer.class || metadata == Long.class || metadata == Float.class || metadata == Double.class || metadata == Boolean.class ) { // --- On gère les types de bases --- writer.print( "" + object ); } else if ( metadata == String.class || metadata == Character.class ) { // --- On gère les chaînes de caractères --- writer.print( "\"" + object + "\"" ); } else if ( metadata.isArray() || object instanceof List ) { // --- On gère les tableaux et les collections --- @SuppressWarnings("rawtypes") List collection = object instanceof List ? (List) object : ArrayUtils.toObjectList( object ) ; int size = collection.size(); int i = 0; writer.print( "[" ); for( Object value : collection ) { writeObject( value, writer ); if ( i++ < size - 1 ) writer.print( "," ); } writer.print( "]" ); } else { // --- On gère les objets Java --- writer.write( "{" ); Field [] fields = metadata.getDeclaredFields(); for ( int i=0; i<fields.length; i++ ) { Field field = fields[i]; field.setAccessible( true ); writer.write( String.format( "\"%s\": ", field.getName() ) ); writeObject( field.get( object ), writer ); if ( i < fields.length-1 ) writer.write( ", " ); } writer.write( "}" ); } } private static Object readObject( Class<?> metadata, Scanner scanner ) throws Exception { if ( metadata == Byte.class || metadata == byte.class ) { return Byte.parseByte( scanner.findInLine( "[0-9]+" ) ); } else if ( metadata == Short.class || metadata == short.class ) { return Short.parseShort( scanner.findInLine( "[0-9]+" ) ); } else if ( metadata == Integer.class || metadata == int.class ) { return Integer.parseInt( scanner.findInLine( "[0-9]+" ) ); } else if ( metadata == Long.class || metadata == long.class ) { return Long.parseLong( scanner.findInLine( "[0-9]+" ) ); } else if ( metadata == Float.class || metadata == float.class ) { return Float.parseFloat( scanner.findInLine( "[0-9]+(\\.[0-9]+)?" ) ); } else if ( metadata == Double.class || metadata == double.class ) { return Double.parseDouble( scanner.findInLine( "[0-9]+(\\.[0-9]+)?" ) ); } else if ( metadata == Boolean.class || metadata == boolean.class ) { return Boolean.parseBoolean( scanner.findInLine( "true|false" ) ); } else if ( metadata == String.class || metadata == Character.class || metadata == char.class ) { String value = scanner.findInLine( "\".*?\"" ); return value.substring( 1, value.length()-1 ); } else if ( metadata.isArray() || List.class.isAssignableFrom( metadata ) ) { // --- Désérialisation de tableaux ou de collections --- throw new Exception( "Not actually implemented" ); } else { // --- Désérialisation d'un objet --- Object object = metadata.newInstance(); scanner.findInLine( "\\{\\s*" ); while( ! scanner.hasNext( "\\s*\\}" ) ) { String attributeName = (String) readObject( String.class, scanner ); scanner.findInLine( ":\\s*" ); Field field = metadata.getDeclaredField( attributeName ); field.setAccessible( true ); Object value = readObject( field.getType(), scanner ); field.set( object, value ); } scanner.findInLine( "\\s*\\}" ); return object; } } // J'introduis ici la généricité pour plus de confort. @SuppressWarnings( "unchecked" ) public static <T> T readObject( Class<T> metadata, Reader reader ) throws Exception { try ( Scanner scanner = new Scanner( reader ) ) { scanner.useLocale(Locale.US); // double stocké avec le caractère . (et non la virgule) return (T) readObject( metadata, scanner ); } } public static void main( String[] args ) throws Exception { String file = "./file.json"; try ( PrintWriter writer = new PrintWriter( file ) ) { SerializationEngine.writeObject( new Article(), writer ); } try ( FileReader reader = new FileReader( new File( file ) ) ) { System.out.println( SerializationEngine.readObject( Article.class, reader ) ); } } } |
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :