(version du (...) - Master informatique

Transcription

(version du (...) - Master informatique
UFR 922 Informatique
Master Informatique – Spécialité STL
Module ALADYN
TP — Sérialisation/désérialisation pour
plates-formes à ressources très limitées
Jacques Malenfant
professeur des universités
Résumé
L’objectif de ce TP est de comprendre les possibilités offertes par l’outil Javassist par la génération de
code et l’adaptation des classes au chargement. L’idée est d’utiliser Javassist pour générer au chargement
le code de sérialisation et de désérialisation pour un ensemble de classes données. Il s’agit de générer
un code spécifique destiné à remplacer le code normal de Java pour la sérialisation et la désérialisation,
ce qui est rendu nécessaire sur des plates-formes à ressources très limitées qui exécutent des versions de
Java ne disposant pas de l’API Reflection (qui est à la base de la sérialisation/désérialisation en J2EE,
J2SE et J2ME/CDC).
1 Sérialisation en Java
La sérialisation est le procédé par lequel on transforme une structure de données complexe (avec des
pointeurs) en mémoire vers une représentation linéaire, sous la forme d’une chaîne de caractères, qui peut
être transmise via le réseau ou encore mise dans un fichier. La désérialisation est le procédé inverse, qui
prend une donnée linéaire selon un certain format et reconstruit une structure de données complexe en
mémoire isomorphe à celle qui a été sérialisée. Dans le cas de la programmaiton par objets, la sérialisation
d’un objet suppose qu’on indique son type (classe), sa taille, puis la sérialisation des valeurs de ses champs.
Notez immédiatement que l’un des points déclicats à traiter sont les cycles dans le graphe d’objets ; nous
ne vous demandons pas de traiter les cycles dans le cadre de ce projet, mais si vous le faites, vous pourrez
recevoir des points bonus.
D’abord justifié par l’appel de méthode à distance et le passage d’objets en paramètres qui y devient
nécessaire, la sérialisation en Java s’appuie sur l’API Reflection qui permet d’examiner le contenu des
objets et de leurs classes d’instatiation dynamiquement. Pourtant, certaines version de Java, comme la
version J2ME CLDC ou encore des versions plus spécifiques comme le Java pour Lego Minstorms et la
machine virtuelle Lejos 1 (que nous visons plus particulièrement dans le cadre de ce projet), n’incluent
pas l’API Reflection. Il s’agit de versions de Java conçues pour des plate-formes à ressources très limitées
(quelque centaines de kilo-octets de mémoire, par exemple), qui ne peuvent donc pas supporter l’ensemble
des API de la J2SE.
Sérialiser et désérialiser des objets dans ces contextes requiert donc de produire du code spécifique pour
effectuer ces opérations. L’idée générale de ce projet consiste à implanter une transformation de classes
qui va prendre en paramètre (principalement) une liste de classes à transformer et, par une analyse de la
structure de leurs instances, va engendrer pour chacune d’elles des méthodes serialize et deserialize
appropriées. Les classes ainsi modifiées seront sauvegardées de manière à ensuite pouvoir être déployées
sur des plates-formes à ressources très limitées
1. Le site du projet est http://lejos.sourceforge.net/
http://lejos.sourceforge.net/nxt/nxj/api/index.html.
et
la
documentation
de
l’API
se
trouve
à
2 Sérialisation et désérialisation
Considérons comme exemple 3 classes Point, ColorPoint et Segment dont on veut transmettre des
instances sur le réseau. Les définitions de ces classes sont :
package g e o m e t r y ;
public clas s Point {
do uble x , y ;
S t r i n g name ;
@Ov er r id e
public Str in g t o S tr i n g ( ) {
return " Point ( " + x + " , " + y + " ) " ;
}
}
package g e o m e t r y ;
public c l a s s ColorPoint extends Point {
String colour ;
@Ov er r id e
public Str in g t o S tr i n g ( ) {
return colour + " p o i n t ( " + x + " , " + y + " ) " ;
}
}
package g e o m e t r y ;
p u b l i c c l a s s Segment {
Point a , b ;
S t r i n g name ;
@Ov er r id e
public Str in g t o S tr i n g ( ) {
r e t u r n " Segment f r o m " + a . t o S t r i n g ( ) + " t o " +
b . toString ();
}
}
Pour pouvoir transmettre des instances de ces classes, il faut pouvoir en obtenir une représentation
textuelle. Il s’agit donc de les sérialiser. Pour ce faire, on cherche à générer des méthodes de sérialisation
et désérialisation pour les objets qu’on veut transmettre. La première étape est de modifier la définition de
toutes les classes que l’on veut transmettre pour qu’elles implantent l’interface LRSerializable avec ses
deux méthodes :
1. La méthode de sérialisation qui convertit l’objet en une chaîne de caractères :
public Str in g s e r i a l i z e
();
2. et la méthode de désérialisation qui crée un objet à partir d’un encodage sous forme de chaîne de
caractères :
public s t a t i c Object d e s e r i a l i z e ( Str in g s e r i a l i z e d ) ;
Pour sérialiser un objet, il faut encoder son type et les valeurs de ses champs. Comme les méthodes
d’encodage (sérialisation) et décodage (désérialisation) sont générées par le même logiciel, on peut imposer
des règles auxquelles chaque partie pourront se fier, comme l’ordre dans lequel les champs sont écrits
pour les lire dans le même ordre, ce qui simplifie le format d’encodage (cela évite de sérialiser les noms
de champs). Pour les champs, on distingue entre les champs de type primitif (entier, flottant, booléen,
caractère et chaîne de caractères) et les champs de type objet. Pour qu’un objet soit sérialisable, tous ses
champs doivent aussi être sérialisables.
Le choix du format d’encodage est libre, mais une première piste serait d’utiliser le format
<type de la donnée>:<taille de la donnée>:<sérialisation de la donnée>
Dans le sens inverse, d’une chaîne de caractères dans le bon format, on veut en reconstruire l’objet Java.
Sous Lejos on ne dispose pas de l’API de réflexion permettant de charger une classe dynamiquement ou
même de déterminer de quelle classe il s’agit à partir d’une chaîne de caractères donnant son nom. On sait
qu’avec l’API Reflection de Java, la méthode statique forName de java.lang.Class permet de retrouver
la classe à partir de son nom sous forme de chaîne de caractères, mais nous n’avons pas cette API sur Lejos.
Pour résoudre le problème, on suppose que pour une application donnée, on connaît a priori l’ensemble
des classes dont les instances peuvent être transmises. On pourra donc produire un code de sélection de la
classe qui, en comparant la chaîne de caractères donnant le nom de la classe avec les noms des classes de
l’application, va permettre de retrouver la classe à instancier pour désérialiser l’objet.
On peut centraliser cette résolution des noms de types (classes) en un seul endroit. Alors, pour chaque
ensemble de classes à traiter, on génère la classe Dispatcher, qui contient une seule méthode :
Objet type ( Str in g obj ) ;
Cette méthode prend une chaîne dans le format de sérialisation choisi, elle extrait le nom de la classe
qui apparaît au début de la chaîne et instancie cette classe en utilisant comme valeur de champs les valeurs
qui sont récursivement désérialisées. En fait, elle va se contenter de trouver la bonne classe pour appeler la
méthode deserialize de cette dernière pour faire le travail.
En gros, il s’agit donc d’engendrer les méthodes serialize et et deserialize sur toutes les classes
demandées et à produire une classe Dispatcher avec sa méthode type, et de rendre ces modifications
définitives sur le système de fichier dans les fichiers .class correspondants pour être déployable ensuite.
Cette transformation sera réalisée avec l’outil Javassist.
3 Réalisations attendues
Le logiciel final sera sous forme d’une archive jar Java avec exécution paramétrée en ligne de commande
capable de faire les modifications nécessaires sur les classes demandées. Pour faire en sorte que ce soit plus
sûr pour vous, le projet est construit en paliers successifs. L’attente du premier palier avec un résultat correct
et le respect des points attendues cités ci-après vous permettra d’avoir la moyenne au projet. La réalisation
des deux paliers suivants vous permettraont, en cas de réussite, d’avoir tous les points. Le dernier palier est
optionnel et donnera des points en bonus s’il est réalisé correctement.
3.1 Premier palier : génération du code de base
Pour le premier palier, on vous demande d’implanter une sérialisation simple. Pour indiquer au logiciel
les classes que l’on veut traiter, on lui passe en paramètre un fichier XML qui contient les noms des classes
à transformer avec la commande suivante :
java -jar serialization_generator <xml file>
Le format du fichier XML est décrit par la grammaire rnc :
start = class_list
class_list = element class_list { path, class* }
path = attribute path { xsd:string }
class = element class { name }
name = attribute name { xsd:string }
Pour sérialiser les 3 classes de l’exemple, on utiliserait l’XML :
< c l a s s _ l i s t path =" g e o m e tr y Classes " >
< c l a s s name= " s r c . g e o m e t r y . P o i n t " / >
< c l a s s name= " s r c . g e o m e t r y . C o l o r P o i n t " / >
< c l a s s name= " s r c . g e o m e t r y . Segment " / >
</ class_list >
Il est important de produire des méthodes de sérialisation qui puissent s’exécuter sur les Mindstorms.
Il faut donc utiliser uniquement des classes et méthodes qui sont aussi présentes dans l’API Lejos.
3.2 Deuxième palier : optimisation des sites d’appel et des noms
La première optimisation qu’on peut faire est de se rendre compte qu’on a pas toujours besoin d’encoder
le type d’une donnée et donc de passer par le Dispatcher, car on connaît souvent le type de l’argument
qu’on attend. Le seul cas quand on ne connaît pas le type de la donnée lors de la désérialisation c’est lorsque
la classe du champ qu’on désérialise possède des sous-classes. Par exemple lorsqu’on désérialise la valeur
d’un champ de type Point dans une classe, on ne peut pas savoir si cette valeur est du type Point ou du
type de la sous-classe ColorPoint.
Pour ne passer par le Dispatcher que lorsque c’est nécessaire, on va alors analyser la hiérarchie des
classes à traiter pour détecter celles qui n’ont pas de sous-classes et appeler directement la méthode de
désérialisation sur la classe ainsi identifiée sans passer par la méthode générale type de Dispatcher.
Une fois qu’on a optimisé le nombre d’accès à la méthode de comparaison, on peut facilement optimiser
la méthode de comparaison elle-même ; à la place d’encoder les noms de classe, on peut associer à chaque
classe un entier et l’utiliser pour l’encodage. L’intérêt de cette encodage est que la comparaison des entiers
est beaucoup moins coûteuse qu’une comparaison de chaînes de caractères. Dans notre exemple, on peut
donner les numéros 1, 2 et 3 à Point, ColorPoint et Segment respectivement, puis utiliser dans les
encodages que ces numéros à la place des noms de classes.
3.3 Troisième palier : options en ligne de commande
Le troisième palier vise à rendre le générateur facile à utiliser. On veut ainsi avoir la possibilité de
paramètrer le programme en ligne de commande selon le synoptique suivant :
java -jar serilization_generator [xml file] -verbose -stats -length [-inline x | -inlineall]
L’option -verbose demlande d’afficher toutes les étapes de la génération du code, avec un affichage
du code génère. Elle servira à tracer les problèmes.
L’option -stats demande d’afficher les statistiques : le nombre et la taille rprésentation sérialisées des
instances des classes en octets, la profondeur maximale de la hiérarchie de classes et le nombre maximal
de champs dans une classe. C’est utile pour optimiser le traitement des classes pour les robots Mindstorms.
L’option -length demande de calculer et d’afficher pour chaque objet la taille en octets des parties
statiques de la chaîne de caractères produite par la sérialisation de chaque objet et indique combien de
champs de type chaîne de caractères (dont on ne peut pas borner la taille) contient l’objet.
Les options -inline x et -inlineall permettent de contrôler comment le générateur met en place
les tests sur les noms de classes sur les sites d’appel de la méthode deserialize. Comme on l’a vu,
dans le cas où il y a une seule classe possible pour un objet à désérialiser, on peut optimiser le code en
appelant directement la méthode deserialize de la classe en question, sans passer par la méthode type
de Dispatcher. On peut facilement généraliser cette optimisation en introduisant à la place de l’appel à
type la série de tests à un ensemble de classes possibles, si cet ensemble est relativement petit. Par exemple,
dans le cas de la valeur d’un champ de type Point, on veut tester uniquement si l’objet à désérialiser est
de type Point ou ColorPoint et non s’il s’agit d’un Segment.
Avec la première option (-inline x), l’utilisateur peut imposer le nombre maximal de comparaisons
qu’il peut y avoir pour un champ pour que le test de type soit « inliné », alors qu’avec la seconde, on
peut aussi demander de ne jamais passer par le Dispacher quel que soit le nombre de comparaisons avec
l’option -inlineall). Notez qu’on ne peut éliminer complètement la classe Dispatcher puisqu’elle reste
nécessaire pour lancer le processus à la racine quand on reçoit un objet via le réseau.
3.4 Quatrième palier : optimisation de la méthode type
À chaque fois que l’application désérialise un objet, la méthode type fait des comparisons (de chaînes
ou d’entiers) pour déterminer la classe de cet objet. Il est possible de réduire le nombre de ces tests si les
classes qui sont utilisées le plus fréquemment sont testées en premier. Mais pour calculer ces fréquences,
il faut exécuter l’application et compter le nombre d’appels à la méthode type en fonction de la classe de
l’objet à désérialiser.
Pour ce quatrième palier, on vous demande d’introduire une option -logfrequencies qui indiquera
aiu générateur qu’il faut introduire dans la méthode type le code nécessaire pour faire ces statistiques, puis
les produire en résultat à la fin de l’exécution sous la forme d’un fichier XML. Le schéma de ce fichier
XML est
start = frequencies
frequencies = element frequencies {
frequency+
}
frequency = element frequency {
attribute class { text },
attribute freq { xsd:int }
}
Pour utiliser ces fréquences, on vous demande d’introduire l’option -frequency <fichier.xml> qui
indiquera au générateur qu’il faut trier les tests de type de classe dans la méthode type et dans les endroits
où les tests ont été « inlinés » en ordre décroissant des fréquences apparaissant dans le fichier passé en
paramètre.
3.5 Cinquième palier : traitement des graphes cycliques
Comme indiqué en introduction, les objets à sérialiser et désérialiser peuvent former un graphe cyclique
lorsqu’un objet à sérialiser contient directement ou indirectement un autre objet qui pointe vers lui. Pour
traiter le graphes cycliques, il faut attribuer à chaque objet sérialisé un identifiant unique (un entier, par
exemple) qui peut être introduit dans le format de sérialisation immédiatement après le nom du type. Avec
cet identifiant unique, on peut lors de sérialisation mémoriser la correspondance entre objets et identifiants
dans une table, puis tester avant de sérialiser si l’objet a déjà été sérialisé et alors on peut indiquer dans
le format de sérialisation qu’on ne met que le type et l’identifiant de l’objet quand celui-ci est déjà apparu
précédemment.
Lors de la désérialisation, on mémorise également la correspondance entre l’identifiant et l’objet désérialisé, ce qui permet quand on rencontre la sérialisation d’un objet de se rendre compte qu’il a déjà été
désérialisé pour rendre immédiatement le résultat de cette désérialisation précédente. Vous noterez que
pour que cela fonctionne, il faut introduire la correspondance dans la table avant de désérialiser les valeurs
des champs, pour qu’on trouve effectivement dans la table cette correspondance quand on va tomber sur le
pointeur induisant le cycle.
4 Évaluation et modalités pour rendre le projet
Pour éviter toute mésentente, voici de manière assez précises les modalités d’évaluation et de remise
du projet. Vous devriez ne pas avoir besoin d’autant de précisions tatillonnes, donc cela ne vise que les cas
problématiques.
Modalités d’évaluation :
– Le projet comptera pour 15% de la note finale de l’UE au titre du contrôle continu. Le barême attribue
un total possible de 12 points pour l’atteinte du premier palier, de 16 points pour l’atteinte du second
palier, de 20 points pour l’atteinte du troisième palier, de 24 points (donc quatre points de bonus)
pour l’atteinte du quatrième palier et de 28 points (donc quatre points de bonus supplémentaires)
pour l’atteinte du cinquième et dernier palier.
– Les critères d’évaluation sont :
– respect du cahier des charges (dont les noms de méthodes, de classes et de « packages », etc.) ;
– lisibilité et qualité du code source produit, y compris la documentation et les commentaires (le
code source sera examiné et la note du projet comportera une partie sur la qualité de ce source en
termes de lisibilité, comme le fait de pratiquer une bonne indentation, de ne jamais dépasser 80
caractères par ligne, et de respecter les conventions standard de Java, et en termes de qualité des
solutions) ;
– qualité et couverture des tests d’exécution qui devraont être faits en utilisant JUnit (pas d’inflation
inutile cependant, il vaut mieux réfléchir à quelques tests couvrant bien les différentes possibilités
plutôt que de produire des dizaines de tests) ;
– exactitude des résultats d’exécution, qui sera vérifiée lors d’une démonstration à faire (sur rendezvous, après la remise du projet).
– Les retards seront sanctionnés selon un barême linéaire dans le temps : chaque tranche de 12 heures
de retard entamée donnera lieu à un pénalité de 5 points sur 20, et donc un retard de plus de 48 heures
donnera lieu à une note de 0 à ce projet.
Modalités de remise des projets :
– Le projet se fait obligatoirement en binôme. Tous les fichiers doivent comporter les noms (authors
en Javadoc) des auteurs.
– Le projet est à rendre le vendredi 20 novembre 2009 à minuit au plus tard sous la forme d’une
archive tgz si vous travaillez sous Unix ou zip si vous travaillez sous Windows que vous m’enverrez à [email protected] comme attachement fait proprement avec votre programme de
gestion de courrier préféré.
– Votre projet utilisera JUnit pour faire des tests de vos classes. Il comportera aussi des classes concrètes
permettant de réaliser les tests. Votre documentation devra indiquer comment exécuter les tests.