XML-RMI et passage d`objets en mode valeur/résultat
Transcription
XML-RMI et passage d`objets en mode valeur/résultat
UFR 919 Sciences de l’ingénieur Master Informatique – Spécialité STL ALADYN XML-RMI et passage d’objets en mode valeur/résultat version 1.2 du 24/10/2012 Jacques Malenfant professeur des universités Résumé L’objectif de ce projet est de comprendre les possibilités offertes par l’outil Javassist par ses capacités de génération dynamique de code, et ce par la réalisation d’un système d’appel de méthodes à distance en XML pour des objets répartis en Java. 1 Appel à distance L’appel d’une méthode à distance entre deux ordinateurs, clé de la programmation répartie, demeure un maillon relativement simpliste même dans les approches les plus sophistiquées utilisées aujourd’hui dans l’industrie. En effet, la plupart des protocoles qui ont été implantés à ce jour demeurent limités (comme SOAP, par exemple) ou très lourds (comme Java RMI) dès qu’il s’agit de passer des objets en paramètres et de les recevoir en résultat. Dans ce projet, nous allons nous poser comme défi d’être capables de passer des objets en paramètres à des appels de méthodes à distance, de recevoir des objets en résultat, et de récupérer des modifications à l’état des objets passés en paramètres pendant l’exécution de la méthode distante. En Java RMI, pour ce faire, il faut que les objets passés en paramètres soient euxmêmes des objets distants, modifiés par des appels de méthodes distants en retour, ce qui est très pénalisant. Nous allons donc tenter une autre approche : un passage par valeur/résultat, c’est-à-dire que l’objet passé en paramètre sera recréé sur le site distant, modifié localement par la méthode appelée, puis retourné à l’appelant pour récupérer ses modifications en fin d’exécution de l’appel. Dans le domaine des langages de programmation, ce genre d’interaction sur les paramètres où la valeur du paramètre n’est remise à jour dans le contexte de l’appel qu’à la fin de l’exécution de la méthode appelée est généralement désignée sous le nom « d’appel par valeur/résultat ». 2 XML-RMI Pour se mettre dans un contexte de programmation répartie, nous allons nous baser sur une approche relativement simple : XML-RPC. XML-RPC est un protocle minimaliste d’appel de procédures à distance où la sérialisation des paramètres et du résultat utilise un format XML et où la communication entre hôtes distants se fait par la protocole HTTP. Sa popularité est due à cette simplicité permettant d’implanter des services Web à peu de frais. La spécification XML-RPC prévoit uniquement les types suivants comme paramètres ou résultat d’un appel : – – – – entiers (int), réels (double), booléens (boolean), chaînes de caractères (string), – – – – moments dans le temps (dateTime ISO8601), binaire (base64), des tableaux (array) de valeurs homogènes des autres types de la norme, et des valeurs structurées struct contenant des champs nommés avec des valeurs hétérogènes des autres types de la norme. Plus précisément, voici un exemple d’appel en XML-RPC : <?xml version="1.0"?> <methodCall> <methodName>examples.getStateName</methodName> <params> <param><value><int>41</int></value></param> </params> </methodCall> La spécification complète de XML-RPC se trouve à l’URL http://xmlrpc.scripting.com/ spec.html. Plutôt que le protocole HTTP pour échanger entre hôtes, nous verrons plus loin que notre utilisation se fera plutôt dans le contexte d’une communication par « sockets ». Le projet consiste à implanter ce protocole en lui ajoutant un nouveau type de données « object » pour obtenir ce qu’on va appeler du XML-RMI. La figure 1 présente un schéma Relax-NG permettant de valider le contenu XML d’une requête ou d’un résultat en XML-RMI. On constate que la spécification distingue les appels de méthodes des résultats qui seront retournés. Un appel contient un nom de méthode puis les valeurs des paramètres. Les types conservés de la spécification XML-RPC sont, comme indiqué plus haut : int (ou i4), double, boolean (mais avec les valeurs numérioques explicites 0 et 1, et non true et false), string, dateTime, base64 (binaire encodé selon la norme), array et struct. En XML-RMI, nous ajoutons le type object pour passer des objets en paramètre ou les retourner en résultat, ce qui sera discuté dans la prochaine section. 3 Des objets en XML pour XML-RMI Le cœur du projet va consister à effectivement réaliser le passage d’objets en paramètres pour ce protocole XML-RMI que nous inventons pour l’occasion. Le principe est de sérialiser les objets sous la forme d’une liste de champs (« fields ») et d’une liste de méthodes. Les champs seront définis par un nom et une valeur initiale, alors que les méthodes seront données sous la forme d’un texte représentant le code de la méthode dans un certain langage de programmation, comme Java. Quelle est l’idée ? D’une part, il s’agit d’être capables de sérialiser des objets pour les passer en paramètre, ce qui se réalise à l’aide du schéma RelaxNG de la figure 2. Certes, une telle sérialisation n’est pas nouvelle, puisque la norme RMI de Java le fait déjà. Mais ici, on ne sérialise pas l’objet, on crée plutôt un représentant de l’objet qui va être transféré côté appelé pour y être utilisé tout au long de l’exécution de la méthode appelée. Cette idée a plusieurs avantages : – ne pas avoir besoin de revenir côté appelant à chaque fois que la méthode appelée veut utiliser l’objet passé en paramètre, – permettre à l’appelant de ne passer qu’une partie de l’objet à l’appelé (ne pas y mettre tous les champs ni toutes les méthodes), ce qui est utile pour des objets dont une partie n’a pas à être vue par l’appelé (par exemple, pour des raisons de sécurité des données), – rendre le code appelé plus indépendant que dans SOAP de la structure du XML passé en paramètre en permettant d’appeler les méthodes de l’objet plutôt que d’accéder directement aux champs, et – améliorer l’interopérabilité, en ayant la possibilité de passer des objets lors d’appels entre objets répartis écrits dans différents langages de programmation (par exemple, un objet en Python peut appeler un objet écrit en Java, en prenant soin de définir en Java les méthodes des objets passés en paramètres en XML). # # # # # ***************************************************************************** ALADYN -- Architectures logicielles pour l’auto-adaptabilité dynamique. Copyright (C) 2012 <[email protected]> GPL version>=2 ***************************************************************************** start = methodcall | methodresponse #****************************************************************************** # Appel par le client #****************************************************************************** methodcall = element methodCall { methodname, element params { param+ }? } methodname = element methodName { xsd:string {pattern = "[a-zA-Z0-9_\.,/]+"} } param = element param { value } value = element value { typedvalue | text } typedvalue = integer | boolean | stringval | # string is a keyword double | datetime | base64 | array | struct | object integer = element i4 { xsd:int } | element int { xsd:int } boolean = element boolean { "0" | "1" } stringval = element string { text } double = element double { xsd:double } datetime = element dateTime.iso8601 { xsd:string {pattern = # xsd:date does not allow "20051130T12:10:56" "[0-9]{4}[0-1][0-9][0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]"} } base64 = element base64 { xsd:base64Binary } array = element array { element data { value+ } } struct = element struct { namedmember+ } namedmember = element member { membername & value } membername = element name { xsd:NMTOKEN } object = external "object.rnc" #****************************************************************************** # Retour du resultat du client #****************************************************************************** methodresponse = element methodResponse { fault | element params { param+ } } fault = element fault { faultvalue } faultvalue = element value { faultstruct } faultstruct = element struct { (faultcode, faultstring) | (faultstring, faultcode) } faultcode = element member { element name { "faultCode" } & element value { integer } } faultstring = element member { element name { "faultString" } & element value { stringval | text } } Figure 1 – Le schéma RelaxNG pour XML-RMI. # # # # # ***************************************************************************** ALADYN -- Architectures logicielles pour l’auto-adaptabilité dynamique. Copyright (C) 2005 <[email protected]> GPL version>=2 ***************************************************************************** start = object object = element object { attribute oid { xsd:anyURI }, attribute type { xsd:NMTOKEN }?, element fields { field* }, element methods { method* } } field = element field { attribute name { xsd:NMTOKEN }, parent value # value pattern as defined in the parent schema } method = element method { attribute language { xsd:NMTOKEN }?, # programming language used text # method definition } Figure 2 – Le schéma RelaxNG pour la représentation des objets en XML-RMI. Bien sûr, une fois l’appel reçu côté appelé, l’objet en XML ne peut être utilisé tel quel. C’est là où la réflexion et les approches vues en ALADYN entrent en jeu pour permettre de créer à la volée un objet Java à partir de l’objet XML. Nous allons poser comme hypothèse que toutes les méthodes qui peuvent être appelées par XML-RMI sont définies avec une signature où tous les types des paramètres sont soit des types primitifs du protocole XML-RPC, soit des objets dont le type est donné par une interface Java. Cette interface peut être déterminée soit par le contexte, lorsque l’objet correspond à un paramètre de la méthode appelée sur le serveur, ou être nommée explicitement dans l’attribut type (optionnel) de la représentation XML-RMI lorsque l’objet est utilisé dans un autre contexte (dans un tableau, comme valeur d’un champ dans un objet, ...). L’objet XML devra donner une définition pour chacune des méthodes apparaissant dans l’interface Java en question. En utilisant Javassist à la réception de l’appel en XML-RMI, vous allez créer une classe pour représenter cet objet puis instancier cette classe et initialiser l’objet obtenu avant de l’utiliser dans l’appel à la méthode de l’objet appelé. Par exemple, soit un objet réparti offrant une méthode display de la forme suivante : public void display(Stringable s) { System.out.println("Un point 2D : " + s.toString()) ; } L’interface Stringable typant le paramètre s est définie de la manière suivante : public interface Stringable { public String toString() ; } Un exemple de contenu XML-RMI pour l’appel de la méthode display avec un paramètre objet correspondant à l’interface Stringable prend la forme suivante : <methodCall> <methodName>display</methodName> <params> <param> <value> <object oid="http://www.jm.fr/1000000" type="Stringable"> <fields> <field name="x"><value><double>10.0</double></value></field> <field name="y"><value><double>20.0</double></value></field> <field name="mark"><value><string>@</string></value></field> </fields> <methods> <method language="Java"> public String toString() { return this.x + this.mark + this.y ; } </method> </methods> </object> </value> </param> </params> </methodCall> On voit que cet objet est un point 2D avec trois champs représentant l’abcisse, l’ordonnée et un caractère utilisé comme séparateur pour produire une représentation sous forme de chaîne de caractères avec le format suivant : [email protected]. Mais la production de ce format à partir des données est masquée par la méthode toString de l’interface Stringable. Notez que le schéma RelaxNG de XML-RMI diffère de celui de XML-RPC en deux points. D’abord, on introduit le type object comme déjà mentionné. Mais il y a une autre modification au niveau de la réponse. Là où XML-RPC ne prévoit qu’une valeur de retour, nous permettons plusieurs « paramètres » en retour. Par convention, le premier paramètre en retour sera toujours le résultat de la méthode appelée, mais l’appelé poura inclure tout ou partie des objets passés en paramètres lors de l’appel pour retourner à l’appelant les nouvelles valeurs des champs de ces objets après exécution de la méthode. Ainsi, l’appelant pourra mettre à jour les valeurs des champs dans les objets originaux de son côté. Mais pour cela, il faut qu’il puisse identifier les objets retournés. C’est pour cela que la représentation du type object prévoit un identifiant d’objets unique de la forme d’une URI. L’appelant peut donc mémoriser lors de l’appel la correspondance entre les identifiants uniques et les références locales à ses objets, et au retour utiliser cette correspondance pour mettre à jour l’objet original. Ainsi, dans l’exemple précédent, la réponse aurait la forme suivante : <methodResponse> <params> <param> <value>void</value> </param> <param> <value> <object oid="http://www.jm.fr/1000000"> <fields> <field name="x"><value><double>10.0</double></value></field> <field name="y"><value><double>20.0</double></value></field> <field name="mark"><value><string>@</string></value></field> </fields> <methods></methods> </object> </value> </param> </params> </methodResponse> Dans cet exemple, on constate que le retour est void, car la méthode display n’a pas de valeur de retour (mais cela aurait pu être une valeur au sens value de XML-RMI dans un autre cas). L’objet apparaissant dans le retour est identique à celui passé en paramètre lors de l’appel car on présume que display ne modifie pas la valeur de son paramètre. Néanmoins, l’appelé peut le retourner et l’appelant le traiter sans chercher à savoir s’il a été modifié ou non. Au retour, seuls les champs sont utiles, et on arrive à déterminer qu’il s’agit d’un objet précédemment passé en paramètre par l’identifiant unique oid qui est le même qu’un de ses objets locaux. Une manière systématique d’implanter un objet en Java pouvant être passé en paramètre via XML-RMI serait de lui faire implanter l’interface suivante : public interface XMLRMISerializable { public String toXML(Class<?> inter) ; public void updateFromXML(org.w3c.dom.Element theXML) ; } La méthode toXML sert à produire la sérialisation en XML souhaitée en fonction de l’interface attendue par le service (on pourra ainsi produire un XML différent en fonction de l’interface demandée), alors que la méthode updateFromXML met à jour l’objet à l’aide de la réprésentation en DOM de l’objet en XML extraite de la réponse de l’appel distant. De cette manière, le déroulement standard d’un appel distant serait : 1. Encoder l’appel en XML-RMI en utilisant les méthodes toXML des objets à passer en paramètres pour obtenir leur représentation en XML. 2. Envoyer l’appel sur le « socket » de l’objet appelé. 3. Récupérer la réponse en XML pour en décoder le résultat et mettre à jour pour chacun des objets en retour les objets originaux en appelant la méthode updateFromXML de ces derniers. Cette mécanique de mise à jour en retour ne fonctionenra que dans le cadre d’un appel. Le résultat de la méthode, bien que pouvant être un objet, ne sera pas considéré comme partagé entre l’appelé et l’appelant. Ceci pour limiter la difficulté du projet, sinon il faudra gérer ces objets partagés sur l’ensemble d’une conversation entre deux ou plusieurs objets. Mais si vous souhaitez vous lancer dans cette direction, pourquoi pas ? Par ailleurs, pour ne pas avoir à réimplanter la sérialisation des champs et la récupération des valeurs en retour pour chaque objet pouvant être passé en paramètre ou reçu en résultat, l’annotation suivante permettra de retouver les champs à sérialiser et à mettre à jour au retour : @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface XMLRMIField { String serializationName() ; // nom à utiliser dans la sérialisation XML-RMI String serializationType() ; // type XML-RMI à utiliser dans la sérialisation } Par exemple, la classe Point suivante pourrait avoir servi à produire l’objet passé en paramètre de l’exemple d’appel précédent : public class Point { @XMLRMIField{serializationName="x",serializationType="double"} protected double a ; @XMLRMIField{serializationName="y",serializationType="double"} protected double b ; @XMLRMIField{serializationName="mark",serializationType="string"} protected char marque ; } Grâce à ces annotations, il vous sera possible d’implanter une fois pour toute par réflexion la sérialisation des champs et la récupération des valeurs en retour. Notez bien que cette annotation permet d’utiliser un nom de champ et un type de valeur en XML qui est différent de celui utilisé en Java ; vous devrez faire les conversions nécessaires dans les deux sens. 4 Intégration dans un modèle d’objets répartis Pour mettre en œuvre le concept d’objets en XML dans un contexte réaliste, vous devrez le tester avec un modèle d’objet réparti volontairement simple pour rester réalisable dans le cadre de ce projet. En Java, le modèle d’objets répartis le plus simple est celui proposé autour de l’appel de méthodes à distance RMI. Un objet réparti serveur RMI implante (au moins) une interface Java qui hérite elle-même directement ou indirectement de l’interface java.rmi.Remote, ce qui lui permet de publier sa propre référence sur l’annuaire RMI (RMIRegistry) et à ses cleints de la récupérer par une requête à cet annuaire. L’appel à distance lui-même se fait en utilisant la référence RMI ainsi obtenue, et le protocole d’échange entre machines virtuelles (et éventuellement hôtes) demeurant caché dans l’implantation de RMI. Pour notre XML-RMI, l’idée sera la suivante : la référence à un objet réparti se fera grâce à l’identification Inet ou l’adresse IP de l’hôte exécutant la MV Java où se trouvbe l’appelé et du numéro de port sur lequel cet objet va écouter les requêtes qui lui seront faites sous forme d’appel de méthode à distance utilisant le protocole XML-RPI décrit précédemment. Un objet réparti sera donc un objet possédant son propre fil d’exécution (« thread ») et qui va répétitivement écouter sur un « socket » les requêtes en XML-RMI, les exécuter puis retourner le résultat dans le format XML-RMI à son appelant. Des informations sur la programmation de communication par « socket » en Java sont largement disponibles sur le WWW. Pour simplifier la réalisation du projet, vous pourrez considérer que l’adresse IP de l’hôte sur lequel se trouve l’objet et le numéro de port sur lequel ce dernier écoute sont des informations connues à l’avance et considérée comme constante par le client. Les étudiants les plus performants pourront étendre leur projet en ajoutant un annuaire pour le protocole XML-RMI permettant de centraliser les références et les requêtes pour la connexion en s’inspirant de l’annuaire RMI Registry de Java. 5 Réalisations attendues Nous vous demandons de réaliser le projet sous Eclipse en prévoyant tout le code nécessaire pour lancer l’application et dans votre rendu vous devrez également joindre des tests unitaires JUnit dans des sous-packages tests. Modalités de remise des projets : – Le projet se fait obligatoirement en binôme. Tous les fichiers du projet doivent comporter les noms (authors) de tous les auteurs. – Le projet est à rendre le vendredi 16 novembre 2012 à 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é. Donner pour nom à votre archive vos numéros de dossier d’étudiants sous la forme n-n (ex. : 2700001-2800002.tgz). Attention, peu importe le système d’exploitation sur lequel vous travaillez, il faudra que votre projet s’exécute correctement sous Eclipse en Mac Os X (que j’utilise) – Votre projet utilisera JUnit pour faire des tests de vos classes. Il comportera aussi des classes concrètes (application) permettant de réaliser les tests. Tous vos tests seront regroupés dans un répertoire spécifique tests au même niveau que le répertoire src dans votre projet Eclipse. Utilisez exactement la même structure de paquetage dans le répertoire tests que dans le répertoire src. – Le code doit être documenté en Javadoc (générée et incluse dans votre livraison dans un répertoire doc au même niveau que votre répertoire src) et commenté. Le code doit également être correctement formatté (indentation, ...). – L’évaluation comportera une soutenance d’une quinzaine de minutes où vous devrez démontrer votre projet (prévoyez dans le code envoyé tout ce qu’il faut pour faire une telle démonstration) et répondre à des questions sur ce dernier. Les soutenances auront lieu dans la semaine du 19 novembre 2012 selon un horaire de passage qui sera publié le moment venu sur le site de l’UE. Tout retard à ces soutenances entraînera son annulation et une pénalité sera appliquée sur l’évaluation du projet. – Tout manquement à ces règles élémentaires entraînera une pénalité dans la note du projet !