(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.