Cours 2 Architectures logicielles dynamiquement adaptables
Transcription
Cours 2 Architectures logicielles dynamiquement adaptables
— Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — ALASCA — Architecture logicielles avancées pour les systèmes complexes auto-adaptables Cours 2 c 2014–2017 Jacques Malenfant Architectures logicielles Master informatique, spécialité STL – UFR 919 Ingénierie dynamiquement adaptables Université Pierre et Marie Curie [email protected] 1 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Objectifs pédagogiques du cours 2 2 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Plan Introduire la notion de réflexivité logicielle pour répondre à une partie du cahier des charges de la fonction d’auto-adaptabilité. 1 Adaptabilité logicielle Comprendre et apprendre à utiliser la réflexivité structurelle de Java pour réaliser des opérations d’adaptation dynamique. 2 Réflexivité structurelle en Java Introduire la notion de réflexivité comportementale et sa déclinaison en Java. 3 Exemple : connecteur de composants par proxy Comprendre et apprendre à utiliser la réflexivité comportementale de Java pour réaliser des opérations d’adaptation dynamique. 4 Réflexivité comportementale en Java et Javassist 5 Exemple : connecteur généré avec Javassist 6 Réflexivité en BCM Comprendre et apprendre à utiliser la réflexivité architecturale dans les modèles à composants comme BCM pour réaliser des opérations d’adaptation dynamique. 3 / 60 4 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Mécanismes pour l’adaptabilité dynamique (rappel) I 2 3 Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Mécanismes pour l’adaptabilité dynamique (rappel) II Pour un système informatique, l’adaptation peut se décliner en quatre grandes familles de mécanismes : 1 — Adapt. logicielle Réfl. structurelle 4 paramétriques : changement de paramètres de configuration n’impliquant pas un impact significatif sur les ressources consommées ni sur celles nécessaires pour l’adaptation. Exemples : taille de tampons bornés, nombre de threads, etc. ressources : changement dans la quantité de ressources mises à la disposition de l’entité ayant un impact significatif à la fois sur la QdS et sur le coût d’opération. Exemples : allocation de machines virtuelles dans un centre de calcul, variation de la fréquence de processeurs, etc. fonctionnelles : modification au contenu des programmes entraînant un changement dans ses fonctionnalités et la QdS associée. Exemples : ajout/retrait/modification de code, recompilation et optimisation de code, etc. architecturaux : modification à la structure des programmes entraînant aussi un changement dans ses fonctionnalités et la QdS associée, et pouvant demander de longs délais pour être réalisées dans le cas des systèmes répartis. Exemples : remplacement de bibliothèques dynamiques, modification de l’assemblage de composants, déconnexion et reconnexion dynamique de services web, etc. Familles qui ont un recouvrement entre elles, et donc des frontières floues. Caractéristiques essentielles : impact sur la difficulté de réaliser les adaptations, leur coût, les contraintes qu’elles imposent, la prédictibilité de leurs effets. 5 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Classes et portée des mécanismes I 6 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Classes et portée des mécanismes II Outre les familles, on peut considérer un autre axe de classification des mécanismes d’adaptation : Comme le suggère la nomenclature précédente, la description et l’exécution d’adaptations à tout ou partie d’un programme, éventuellement réparti, est lourde et suggère des langages et de moteur d’exécution dédiés à ces opérations (DSL...). les opérations élémentaires, insécables et non-interruptibles, comme la modification d’un paramètre, l’allocation d’une quantité donnée d’une ressource monolithique ; les opérations composites classiques : séquentielles, conditionnelles, répétitives, etc. les opérations composites temporisées et réparties, comme des plans exécutés sur différentes entités, déployées sur différents ordinateurs et l’expression de leurs dépendances temporelles, temporisées et fonctionnelles ; les opérations coordonnées, à petite, moyenne et grande échelle nécessitant des mécanismes de propagation, de synchronisation et de gestion des dépendences de types vagues de mises à jour et front d’adaptation dans un réseau à grande échelle, par exemple. Nous allons maintenant nous concentrer sur certaines opérations élémentaires d’adaptations fonctionnelles et architecturales qui ont été développées depuis une trentaine d’années dans le domaine des langages de programmation et des modèles à composants. 7 / 60 8 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Architectures réflexives et programmation — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Réflexion et architectures à base de composants Concept lié à la modification dynamique des programmes, une pratique aussi ancienne que l’informatique elle-même. Des modèles de composants, comme BCM et Fractal, prévoient des interfaces d’introspection et d’intro-action permettant de découvrir et modifier les interfaces requises et offertes par les composants, les interconnexions entre composants, ainsi que d’autres informations sur l’achitecture de l’application. Conceptualisée en tant que discipline par Brian Smith au début des années 1980. Exemple : réflexion en Java Java définit une grande partie des éléments constitutifs d’un programme sous la forme d’objets, comme les classes, les méthodes, etc. (java.lang.reflect). Java prévoit le chargement dynamique de code ainsi que des moyens pour récupérer et remplacer le code (classes) chargé dans sa machine virtuelle (java.lang.instrumentation). Des bibliothèques externes permettent de modifier dynamiquement le code compilé des classes (Javassist). Bien que relativement élémentaires, Java offre des moyens standardisés pour observer des informations sur l’exécution des programmes (java.lang.management et JMX). La réflexion dans une architecture à base de composants doit donc permettre de modifier cette architecture dynamiquement, en ajoutant, retranchant et remplaçant des composants pendant l’exécution, pour adapter un système à base de composants à son état d’exécution courant. 9 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Plan 10 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Réifier = décrire + accéder 1 Adaptabilité logicielle 2 Réflexivité structurelle en Java 3 Exemple : connecteur de composants par proxy 4 Réflexivité comportementale en Java et Javassist 5 Exemple : connecteur généré avec Javassist 6 Réflexivité en BCM Le langage Java possède une API pour la réflexion structurelle fournie par les classes Object et Class de même que le paquetage java.lang.reflect, que nous allons maintenant rapidement découvrir. Pour réifier des entités, il faut d’abord les décrire, ou en donner une représentation : c’est le rôle des classes ! Mais pour avoir accès à la description des objets, il faut pouvoir manipuler dynamiquement les classes elle-mêmes. ) Comment faire ? ) représenter les classes comme des objets. ) mais alors, quelle sera la classe qui va décrire ces objets qui sont en réalité des classes ? Métaclasse : classe dont les instances (objets) sont des classes. Concept introduit en Smalltalk (1976), puis étudié par Cointe et Briot (1984-87) dont le problème de clôture de la régression potentiellement infinie. 11 / 60 12 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — La métaclasse java.lang.Class<T> — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Trois façons de récupérer un objet classe C’est la pseudo-métaclasse1 unique de tous les objets représentant des classes en Java, donc la méta-métaclasse de toutes les classes. Par la méthode getClass de la classe Object : Point p = new Point(0.0, 0.0) ; Class<Point> point = p.getClass() ; La variable de type T représente le type des objets instances de cette classe, donc la classe elle-même. Exemple : Class<Point> = métaclasse de Point. Par la méthode statique forName de la classe Class : Class<?> maClasse ; try { maClasse = Class.forName("Point") ; } catch(ClassNotFoundException e) { System.out.println("La classe Point n’existe pas.") ; throw e ; } C’est une classe finale, donc non-héritable. Tous les objets « classes » sont ainsi des « instances » de l’unique « pseudo-métaclasse » : java.lang.Class<T>. ) pas de nouvelles formes de métaclasses. ) pas de modification de la réprésentation ou du Par la variable statique class associée à chaque classe : Class<Point> uneAutreReferenceSurPoint = Point.class ; comportement des classes. Exemple : classes singleton qui connaissent leur unique instance. 1 C’est-à-dire, pas une métaclasse de plein droit car non-remplaçable et non-extensible. 13 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Principales méthodes de java.lang.Class<T> I 14 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Principales méthodes de java.lang.Class<T> II <A extends Annotation> getAnnotation(Class<A> annotationClass) Returns this Field[] getDeclaredFields() : Returns an array of Field objects reflecting all the fields element’s annotation for the specified type if such an annotation is present, else null. declared by the class or interface represented by this Class object. Method getDeclaredMethod(String name, Class... parameterTypes) : Returns a ClassLoader getClassLoader() : Returns the class loader for the class. Method object that reflects the specified declared method of the class or interface represented by this Class object. Constructor<T> getConstructor(Class... parameterTypes) : Returns a Constructor object that reflects the specified public constructor of the class represented by this Class object. Method[] getDeclaredMethods() : Returns an array of Method objects reflecting all the methods declared by the class or interface represented by this Class object. Constructor[] getConstructors() : Returns an array containing Constructor objects Field getField(String name) : Returns a Field object that reflects the specified public reflecting all the public constructors of the class represented by this Class object. member field of the class or interface represented by this Class object. Field[] getFields() : Returns an array containing Field objects reflecting all the accessible Constructor getDeclaredConstructor(Class... parameterTypes) : Returns a public fields of the class or interface represented by this Class object. Constructor object that reflects the specified constructor of the class or interface represented by this Class object. Class[] getInterfaces() : Determines the interfaces implemented by the class or interface represented by this object. Constructor[] getDeclaredConstructors() : Returns an array of Constructor objects Method getMethod(String name, Class... parameterTypes) : Returns a Method reflecting all the constructors declared by the class represented by this Class object. object that reflects the specified public member method of the class or interface represented by this Class object. Field getDeclaredField(String name) : Returns a Field object that reflects the specified declared field of the class or interface represented by this Class object. 15 / 60 16 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Principales méthodes de java.lang.Class<T> III — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Le « package » java.lang.reflect java.lang.Object java.lang.reflect.AccessibleObject (implements java.lang.reflect.AnnotatedElement) java.lang.reflect.Constructor<T> (implements java.lang.reflect.GenericDeclaration, java.lang.reflect.Member) java.lang.reflect.Field (implements java.lang.reflect.Member) java.lang.reflect.Method (implements java.lang.reflect.GenericDeclaration, java.lang.reflect.Member) java.lang.reflect.Array java.lang.reflect.Modifier java.security.Permission (implements java.security.Guard, java.io.Serializable) java.security.BasicPermission (implements java.io.Serializable) java.lang.reflect.ReflectPermission java.lang.reflect.Proxy (implements java.io.Serializable) java.lang.Throwable (implements java.io.Serializable) java.lang.Error java.lang.LinkageError java.lang.ClassFormatError java.lang.reflect.GenericSignatureFormatError java.lang.Exception java.lang.reflect.InvocationTargetException java.lang.RuntimeException java.lang.reflect.MalformedParameterizedTypeException java.lang.reflect.UndeclaredThrowableException Method[] getMethods() : Returns an array containing Method objects reflecting all the public member methods of the class or interface represented by this Class object, including those declared by the class or interface and and those inherited from superclasses and superinterfaces. String getName() : Returns the name of the entity (class, interface, array class, primitive type, or void) represented by this Class object, as a String. Class<? super T> getSuperclass() : Returns the Class representing the superclass of the entity (class, interface, primitive type or void) represented by this Class. boolean isAssignableFrom(Class<?> cls) : Determines if the class or interface represented by this Class object is either the same as, or is a superclass or superinterface of, the class or interface represented by the specified Class parameter. T newInstance() : Creates a new instance of the class represented by this Class object. 17 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — La classe java.lang.reflect.Field I 18 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — La classe java.lang.reflect.Field II Classe finale dont les instances représentent les champs. Champs : variables, constantes. Peuvent être de classe (statiques) ou d’instance. String getName() : Returns the name of the field represented by this Field object. Principales méthodes : Class<?> getType() : Returns a Class object that identifies the declared type for the field represented by this Field object. Object get(Object obj) : Returns the value of the field represented by this Field, on the void set(Object obj, Object value) : Sets the field represented by this Field object on specified object. the specified object argument to the specified new value. <T extends Annotation> getAnnotation(Class<T> annotationClass) : Returns this element’s annotation for the specified type if such an annotation is present, else null. Class<?> getDeclaringClass() : Returns the Class object representing the class or interface that declares the field represented by this Field object. int getModifiers() : Returns the Java language modifiers for the field represented by this Field object, as an integer. 19 / 60 20 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — La classe java.lang.reflect.Method I — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — La classe java.lang.reflect.Method II Class<?>[] getExceptionTypes() : Returns an array of Class objects that represent the Classe finale dont les instances représentent les méthodes. types of the exceptions declared to be thrown by the underlying method represented by this Method object. Ne permet pas de manipuler le code des méthodes, mais plutôt en découvrir la signature et informations associées. int getModifiers() : Returns the Java language modifiers for the method represented by this Method object, as an integer. String getName() : Returns the name of the method represented by this Method object, as a Elle offre une méthode invoke pour appliquer la méthode sur un objet avec des paramètres réels donnés. ) on touche ici à la frontière avec la réflexion de comportement ! ) c’est aussi la limite entre ce qui est visible à propos des méthodes en Java et ce qui ne l’est pas... String. Annotation[][] getParameterAnnotations() : Returns an array of arrays that represent the annotations on the formal parameters, in declaration order, of the method represented by this Method object. Class<?>[] getParameterTypes() : Returns an array of Class objects that represent the formal parameter types, in declaration order, of the method represented by this Method object. Principales méthodes : Class<?> getReturnType() : Returns a Class object that represents the formal return type <T extends Annotation> getAnnotation(Class<T> annotationClass) : Returns this of the method represented by this Method object. element’s annotation for the specified type if such an annotation is present, else null. Object invoke(Object obj, Object... args) : Invokes the underlying method represented by this Method object, on the specified object with the specified parameters. Class<?> getDeclaringClass() : Returns the Class object representing the class or interface that declares the method represented by this Method object. 21 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — La méthode invoke 22 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Forme de réflexion offerte par Java En résumé : La méthode invoke de la classe Method permet donc de faire s’exécuter une méthode à partir de l’instance de la classe Method qui la représente réflexivement. Exemple : De la réflexion structurelle limitée à l’introspection. L’objet obtenu par Class.class est lui-même une instance de Class, ce qui clôt la régression potentiellement infinie de niveaux de métaclasses. try { Method m = String.class.getMethod("length", new Class<?>[]{}) ; System.out.println(m.invoke("toto", new Object[]{})) ; } catch (Exception e) { e.printStackTrace(); } Une des limitations importantes de l’API Reflection est de ne pas fournir un accès au code des méthodes, ce qui est une partie intégrante de la réflexion structurelle. Cela montre la puissance, mais aussi la limite de l’API réflexion de Java : le code des méthodes peut être invoqué, mais il ne peut pas être introspecté et encore moins modifié. Pour pallier à ces limitations, Java offre des mécanismes plus restreints, comme l’interception des appels de méthodes par des proxys. 23 / 60 24 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Plan — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Mécanisme de proxy dynamique de Java 1 Adaptabilité logicielle Mécanisme général d’interception des appels : introduire un objet entre un client et un fournisseur qui va intercepter « tous » les appels du premier au second. 2 Réflexivité structurelle en Java En Java, il s’agit : 3 Exemple : connecteur de composants par proxy 1 2 4 Réflexivité comportementale en Java et Javassist 5 Exemple : connecteur généré avec Javassist 6 Réflexivité en BCM 3 de créer un intercepteur (proxy ) à partir des interfaces implantées par le fournisseur ; de lier à cet intercepteur un objet dit « invocation handler » qui va effectivement traiter les appels reçus par l’intercepteur ; pour ce faire, cet « invocation handler » implante une interface standard, contenant une méthode invoke. La classe java.lang.Proxy, superclasse de toutes les classes de proxy créées dynamiquement, propose des méthodes statiques pour créer des classes de proxy ou directement des objets proxy (dont la classe est créée implicitement). 25 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Énoncé du problème 26 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Interface offerte et requise Dans le modèle à composants BCM, les composants exposent et requièrent des services par leurs ports entrants et sortants respectivement, reliés par des connecteurs implantant l’interface requise et appelant le port entrant sur l’interface offerte. public interface CalculatorServicesI extends OfferedI { public double add(double x, double y) throws Exception; public double subtract(double x, double y) throws Exception ; } public interface SummingServiceI extends RequiredI { public double sum(double x, double y) throws Exception ; } La solution la plus évidente consiste à créer chaque connecteur manuellement, en les programmant. Une solution plus satisfaisante serait de créer les connecteurs automatiquement à partir des interfaces requise et offerte. La méthode requise sum est réalisée par la méthode offerte add, ce qui donne le connecteur suivant : Dans certains cas, c’est possible de le faire en utilisant les proxys dynamiques de Java. public class Connector extends AbstractConnector implements SummingServiceI { @Override public double sum(double x, double y) throws Exception { return ((CalculatorServicesI)this.offering).add(x, y) ; } } Illustrons cela sur un exemple d’un composant Calculator offrant une interface CalculatorServicesI appelé par un composant VectorSummer via une interface requise SummingServiceI. 27 / 60 28 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Comment procéder ? 1 Le proxy va jouer le rôle de connecteur ; il est donc créé sur les interfaces ConnectorI et SummingServiceI. 2 C’est l’invocation handler qui va traiter les appels, et donc il faut lui fournir une correspondance entre les méthodes de l’interface requise et celles de l’interface offerte ; par simplicité, faisons cette correspondance uniquement sur les noms en supposant que le nombre, les types et l’ordre des paramètres sont les mêmes. 3 4 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — L’invocation handler I public class ConnectorIH extends AbstractConnector implements InvocationHandler { protected HashMap<String, String> methodNamesMap ; protected ConnectorI proxy ; public ConnectorIH(HashMap<String, String> methodNamesMap) { this.methodNamesMap = methodNamesMap ; } public void setProxy(ConnectorI proxy) { this.proxy = proxy ; } @Override protected ConnectorI thisConnector() { return this.proxy ; } En tant que connecteur, chaque méthode exécutée par l’invocation handler doit toujours considérer que c’est le proxy qui joue le rôle de l’objet connecteur. protected boolean isConnectorMethod(String methodName) { Method[] connectorMethods = ConnectorI.class.getMethods() ; boolean ret = false ; for(int i = 0 ; !ret && i < connectorMethods.length ; i++ ) { ret = connectorMethods[i].getName().equals(methodName) ; } return ret ; } L’invocation handler doit distinguer les appels qui le concerne en tant que connecteur des appels qu’il doit relayer au port entrant et parmi ces derniers ceux qui font partie de l’interface offerte de ceux qui concernent le port entrant en tant que port. 29 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — L’invocation handler II 30 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Création du proxy et séquence de connexion public Object makeConnectorProxy( Class<?> requiredInterface, HashMap<String, String> methodNamesMap ) throws Exception { ClassLoader cl = this.getClass().getClassLoader() ; ConnectorIH ih = new ConnectorIH(methodNamesMap) ; ConnectorI proxy = (ConnectorI) Proxy.newProxyInstance( cl, new Class<?>[]{ConnectorI.class, requiredInterface}, ih) ; ih.setProxy(proxy) ; return proxy ; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Class<?>[] pt = null ; if (args != null) { pt = new Class<?>[args.length] ; for (int i = 0 ; i < args.length ; i++) { pt[i] = args[i].getClass() ; } } if (this.isConnectorMethod(method.getName())) { return method.invoke(this, args) ; } else { String methodName = this.methodNamesMap.get(method.getName()) ; if (methodName == null) { methodName = method.getName() ; } Method m = this.offering.getClass().getMethod(methodName, pt) ; return m.invoke(this.offering, args) ; } HashMap<String, String> methodNamesMap = new HashMap<String, String>() ; methodNamesMap.put("sum", "add") ; ConnectorI connector = (ConnectorI) this.makeConnectorProxy(SummingServiceI.class, methodNamesMap) ; p.doConnection(ServerPortURI, connector) ; } } 31 / 60 32 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Plan — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — De la réflexion de structure à la réflexion de comportement 1 Adaptabilité logicielle 2 Réflexivité structurelle en Java 3 Exemple : connecteur de composants par proxy 4 Réflexivité comportementale en Java et Javassist 5 Exemple : connecteur généré avec Javassist 6 Réflexivité en BCM La limite de la réflexion en Java apparaît au niveau de la méthode invoke de la classe Method qui permet d’exécuter un objet méthode, mais comme une boîte noire. La réflexion de comportement demande la possibilité de manipuler dynamiquement le code et l’état d’exécution. Comment donner accès au code ? La machine virtuelle Java accède au code par le contenu des fichiers .class chargé dans sa mémoire par des chargeurs de classes (« class loaders »). Le contenu d’un fichier .class est standardisé : il s’agit du code octal, plus des entêtes donnant des informations utiles à la JVM pour l’exécution, selon un format précis. Plusieurs outils ont donc été conçus et implantés pour donner un accès au code des classes via son fichier .class. Ces outils s’interposent généralement entre la machine virtuelle et les fichiers .class pour donner aux programmes la possibilité de modifier le code des classes lors de leur chargement. 33 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — BCEL, ASM et Javassist 34 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Principes généraux de Javassist Les bibliothèques BCEL et ASM, orientées JVM, partagent le même objectif général : lire et représenter le contenu des fichiers .class et en permettre la manipulation par le programme lui-même. Toutes les deux offrent une API sur cette représentation en mémoire manipulant la classe en termes de code octaux JVM (et non en source Java). La principale différence entre les deux bibliothèques tient à la représentation adoptée : BCEL ) version sérialisée du fichier .class en un graphe La JVM exécute les programmes en chargeant, grâce à des « class loaders », une représentation de ces dernières générée par le compilateur Java dans les fichiers .class. Cette représentation totalement connue permet soit de forger ex nihilo des classes, soit de modifier des classes existantes avant de les mettre dans la JVM. Javassist réifie le contenu des classes compilées sous forme d’objets d’une bibliothèque conçue pour ressembler autant que possible à java.lang.Class<?> et java.lang.reflect. Les manipulations sont possibles tant que la classe n’est pas mise à disposition de la machine virtuelle ; ensuite, la classe est gelée et rendue inaccessible. d’objets. ASM ) représentation en tableau monolithique (pour une classe). Lors du chargement des classes par Java, il est possible d’intervenir sous la forme d’un filtre entre le contenu des « fichiers » .class et la définition de la classe dans la machine virtuelle, selon un patron de conception Observateur. Javassist est une autre bibliothèque similaire, mais qui propose une interface d’introspection des fichiers .class très similaire à celle de l’API réflexion de Java, et qui permet de modifier le code sous une forme source Java plutôt que du code octal. Grâce aux API JPDA depuis Java 4.0 et java.lang.instrumentation depuis Java 6.0, on peut récupérer une classe dans la JVM, la modifier, puis la remettre dans la JVM. 35 / 60 36 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — La notion de ClassPool Pour créer une nouvelle classe, on utilise la méthode makeClass. Pour créer une classe Point vide, on fait : ClassPool p o o l = ClassPool . g e t D e f a u l t ( ) ; CtClass cc = p o o l . makeClass ( " P o i n t " ) ; Il est également possible de lire une classe puis de la renommer pour former une nouvelle classe : même format, d’assure l’unicité de l’instance de CtClass pour une classe donnée et maintient la consistence de ses transformations. Obtention d’un collecteur et réification d’une classe : / / exemple de m o d i f i c a t i o n / / é c r i t u r e s u r disque Note : la méthode toBytecode() retourne un tableau d’octets (codes octaux) plutôt que d’écrire le fichier de classe. — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Définition d’une nouvelle classe ex nihilo Chaque classe est représentée par une unique instance de la classe CtClass (pour « compile-time class »), version Javassist de la classe Class de Java. Ces instances sont créées par le truchement d’un collecteur de classes instance de ClassPool. Le collecteur de classe permet de créer des instances de CtClass en accédant à des fichiers .class ou directement à des tableaux d’octets respectant le ClassPool p o o l = ClassPool . g e t D e f a u l t ( ) ; CtClass cc = p o o l . g e t ( " t e s t . Rectangle " ) ; cc . s e t S u p e r c l a s s ( p o o l . g e t ( " t e s t . P o i n t " ) ) ; cc . w r i t e F i l e ( ) ; — Adapt. logicielle Réfl. structurelle ClassPool p o o l = ClassPool . g e t D e f a u l t ( ) ; CtClass cc = p o o l . g e t ( " P o i n t " ) ; CtClass cc1 = p o o l . g e t ( " P o i n t " ) ; / / cc1 e s t i d e n t i q u e a cc . cc . setName ( " P a i r " ) ; CtClass cc2 = p o o l . g e t ( " P a i r " ) ; / / cc2 e s t i d e n t i q u e a cc . CtClass cc3 = p o o l . g e t ( " P o i n t " ) ; / / cc3 n ’ e s t pas i d e n t i q u e a cc . 37 / 60 Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Le chargeur javassist.Loader 38 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — La méthode toClass() de CtClass Javassist offre donc son propre chargeur de classes permettant de charger des classes à partir d’un pool Javassist : import j a v a s s i s t . ⇤ ; import t e s t . Rectangle ; public class Main { public s t a t i c void main ( S t r i n g [ ] args ) throws Throwable { ClassPool p o o l = ClassPool . g e t D e f a u l t ( ) ; Loader c l = new Loader ( p o o l ) ; / / chargeur J a v a s s i s t CtClass c t = p o o l . g e t ( " t e s t . Rectangle " ) ; c t . setSuperclass ( pool . get ( " t e s t . Point " ) ) ; / / m o d i f i c a t i o n Class c = c l . l o a d C l a s s ( " t e s t . Rectangle " ) ; / / chargement Java O b j e c t r e c t = c . newInstance ( ) ; / / u t i l i s a t i o n en Java ... } } 39 / 60 Après modification des classes, la méthode toClass() de la classe CtClass fournit un autre moyen très simple pour convertir une instance de CtClass en une instance de java.lang.Class, la mettant au passage à la disposition de la JVM. Cela remplace le loadClass de l’exemple précédent par : ct.toClass() ; L’effet de bord de cette opération est de mettre la nouvelle classe dans la JVM pour utilisation dans l’exécution du programme de base. Il existe une version de toClass prenant en paramètre un chargeur instance de Loader qui force le chargement dans ce chargeur. 40 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Interception au chargement I — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Interception au chargement II L’intérêt du chargeur Javassist est de fournir un mécanisme d’interception basé sur un patron Observateur pour s’interposer entre la lecture des fichiers .class et la mise en place dans la machine virtuelle de manière à réaliser des transformations au chargement. La méthode onLoad est appelée par le chargeur de classe Javassist avec le collecteur et le nom de la classe *avant* de lire le fichier .class. Il faut donc définir : une classe de transformation qui réalise les modifications désirées à la classe dans sa méthode onLoad, puis attacher une instance de cette classe au chargeur de classes de Javassist. Un transformateur est une instance d’une classe implantant l’interface Translator. public i n t e r f a c e T r a n s l a t o r { public void s t a r t ( ClassPool p o o l ) throws NotFoundException , CannotCompileException ; public void onLoad ( ClassPool pool , S t r i n g classname ) throws NotFoundException , CannotCompileException ; } À partir de là, cette méthode onLoad sera appelée à chaque fois qu’une classe est chargée. 41 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Une transformation simple : rendre publique — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Les méthodes public class M a k e P u b l i c T r a n s l a t o r implements T r a n s l a t o r { void s t a r t ( ClassPool p o o l ) throws . . . { } void onLoad ( ClassPool pool , S t r i n g classname ) throws NotFoundException , CannotCompileException { CtClass cc = p o o l . g e t ( classname ) ; cc . s e t M o d i f i e r s ( M o d i f i e r . PUBLIC ) ; } } Les méthodes sont représentées par des instances de CtMethod. Les constructeurs sont représentés par des instances de CtConstructor. Ces classes définissent des méthodes : insertBefore() and insertAfter() pour ajouter du code public class Main { public s t a t i c void main ( S t r i n g [ ] args ) throws Throwable { T r a n s l a t o r t = new M a k e P u b l i c T r a n s l a t o r ( ) ; ClassPool p o o l = ClassPool . g e t D e f a u l t ( ) ; Loader c l = new Loader ( ) ; c l . a d d T r a n s l a t o r ( pool , t ) ; c l . run ( " MyApp " , args ) ; } } Notez ici l’utilisation de deux chargeurs (le chargeur initial implicite et celui donné par cl). L’existence de ces deux chargeurs peut faire qu’une même classe soit chargée dans les deux et alors considérées comme différentes. Dans ce cas, on peut devoir forcer le chargement de classes à se faire ensuite dans un chargeur spécifique pour préserver l’unicité de la classe chargée. 42 / 60 source dans le corps des méthodes, addCatch() pour ajouter des tratements d’exception sur l’ensemble du corps de la méthode, et La méthode insertAt() permet d’introduire du code à une ligne donnée de la méthode. Condition : la méthode doit avoir été compilée avec l’option -g (« debugging » ou déverminage) laissant plus d’information dans le fichier .class sur les numéros de lignes et les noms de variables locales. 43 / 60 44 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Contraintes sur le code source — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Exemple Soit la classe Point suivante : class P o i n t { int x , y ; void move ( i n t dx , i n t dy ) { x += dx ; y += dy ; } } Javassist inclut un compilateur Java pour traduire ce code source en code octal qui est ensuite introduit dans les méthodes. Le fait que le code source à ajouter doivent être introduit et compilé dans le contexte d’une méthode en code octal pose certaines contraintes. On ajoute une trace de move par : ClassPool p o o l = ClassPool . g e t D e f a u l t ( ) ; CtClass cc = p o o l . g e t ( " P o i n t " ) ; CtMethod m = cc . getDeclaredMethod ( " move " ) ; m. i n s e r t B e f o r e ( " { System . o u t . p r i n t l n ( $1 ) ; System . o u t . p r i n t l n ( $2 ) ; } " ) ; cc . w r i t e F i l e ( ) ; En particulier, des noms de variables spéciaux sont définis, comme $0, $1, $2, pour représenter le pseudo-argument this et les arguments de la méthode, ou encore $sig pour accéder à un tableau de type Class des types des paramètres formels ou encore $type pour accéder à l’objet de type Class représentant le type du résultat formel. Pour donner l’équivalent de : class P o i n t { int x , y ; void move ( i n t dx , i n t dy ) { { System . o u t . p r i n t l n ( dx ) ; System . o u t . p r i n t l n ( dy ) ; } x += dx ; y += dy ; } } 45 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Ajout de méthodes et de champs 46 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Plan Ajout d’une méthode : CtClass p o i n t = ClassPool . g e t D e f a u l t ( ) . g e t ( " P o i n t " ) ; CtMethod m = CtNewMethod . make ( " p u b l i c i n t xmove ( i n t dx ) { x += dx ; } " , p o i n t ) ; p o i n t . addMethod (m) ; 1 Adaptabilité logicielle 2 Réflexivité structurelle en Java 3 Exemple : connecteur de composants par proxy CtClass p o i n t = ClassPool . g e t D e f a u l t ( ) . g e t ( " P o i n t " ) ; C t F i e l d f = new C t F i e l d ( CtClass . i n t T y p e , " z " , p o i n t ) ; 4 Réflexivité comportementale en Java et Javassist puis, ajout sans initialisation : 5 Exemple : connecteur généré avec Javassist 6 Réflexivité en BCM Ajout d’un champ : Préparation : point . addField ( f ) ; ou encore avec initialisation : point . addField ( f , " 0 " ) ; / / la valeur i n i t i a l e est 0. 47 / 60 48 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Énoncé du problème 1 Reprenons l’exemple précédent : comment éviter de programmer manuellement le connecteur entre deux ports de composants ? 2 Ici, la solution sera conceptuellement plus simple, puisqu’il s’agit de créer la classe définissant le connecteur dynamiquement, mais avec le même code que la classe créée manuellement. 3 Pour créer une classe de connecteur, il faut avoir : Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Génération de la classe de connecteur I public Class<?> makeConnectorClassJavassist(String connectorCanonicalClassName, Class<?> connectorSuperclass, Class<?> connectorImplementedInterface, Class<?> offeredInterface, HashMap<String,String> methodNamesMap ) throws Exception { ClassPool pool = ClassPool.getDefault() ; CtClass cs = pool.get(connectorSuperclass.getCanonicalName()) ; CtClass cii = pool.get(connectorImplementedInterface.getCanonicalName()) ; CtClass oi = pool.get(offeredInterface.getCanonicalName()) ; CtClass connectorCtClass = pool.makeClass(connectorCanonicalClassName) ; connectorCtClass.setSuperclass(cs) ; Method[] methodsToImplement = connectorImplementedInterface.getDeclaredMethods() ; for (int i = 0 ; i < methodsToImplement.length ; i++) { String source = "public " ; source += methodsToImplement[i].getReturnType().getName() + " " ; source += methodsToImplement[i].getName() + "(" ; Class<?>[] pt = methodsToImplement[i].getParameterTypes() ; String callParam = "" ; for (int j = 0 ; j < pt.length ; j++) { String pName = "aaa" + j ; source += pt[j].getCanonicalName() + " " + pName ; callParam += pName ; if (j < pt.length - 1) { source += ", " ; callParam += ", " ; } } source += ")" ; Class<?>[] et = methodsToImplement[i].getExceptionTypes() ; if (et != null && et.length > 0) { source += " throws " ; le nom de la classe à créer, la superclasse de connecteur à utiliser, l’interface requise devant être implantée par le connecteur, l’interface offerte et implantée par le port entrant, et la correspondance entre les méthodes de l’interface requise et celles de l’interface offerte. 4 — Adapt. logicielle Réfl. structurelle Les classes et les interfaces sont représentées par des instances de la classe java.lang.Class. 49 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Génération de la classe de connecteur II 50 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Séquence de création de la classe et de connexion for (int z = 0 ; z < et.length ; z++) { source += et[z].getCanonicalName() ; if (z < et.length - 1) { source += "," ; } } } source += "\n{ return ((" ; source += offeredInterface.getCanonicalName() + ")this.offering)." ; source += methodNamesMap.get(methodsToImplement[i].getName()) ; source += "(" + callParam + ") ;\n}" ; CtMethod theCtMethod = CtMethod.make(source, connectorCtClass) ; connectorCtClass.addMethod(theCtMethod) ; HashMap<String, String> methodNamesMap = new HashMap<String, String>() ; methodNamesMap.put("sum", "add") ; Class<?> connectorClass = this.makeConnectorClassJavassist( "fr.upmc.alasca.summing.assembly.GeneratedConnector", AbstractConnector.class, SummingServiceI.class, CalculatorServicesI.class, methodNamesMap) ; p.doConnection(ServerPortURI, connectorClass.getCanonicalName()) ; } connectorCtClass.setInterfaces(new CtClass[]{cii}) ; cii.detach() ; cs.detach() ; oi.detach() ; Class<?> ret = connectorCtClass.toClass() ; connectorCtClass.detach() ; return ret ; } Ce qui génére le code suivant pour la méthode sum : public double sum(double aaa0, double aaa1) throws java.lang.Exception { return ((fr.upmc.alasca.summing.calculator.interfaces.CalculatorServicesI) this.offering).add(aaa0, aaa1) ; } 51 / 60 52 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Plan — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Réflexivité dans un modèle à composants 1 Adaptabilité logicielle 2 Réflexivité structurelle en Java 3 Exemple : connecteur de composants par proxy 4 Réflexivité comportementale en Java et Javassist 5 Exemple : connecteur généré avec Javassist 6 Réflexivité en BCM Objectif : donner la capacité d’introspecter et d’intro-agir sur les composants à l’exécution. Qu’est-ce qu’on vise à gérer ? des informations définissant le « type » de composants (interfaces offertes et requises, des informations statiques définissant les capacités du composant (passif ou actif, capable d’ordonnancer des tâches ou non, ...) ; des informations plus dynamiques sur l’état courant du composant dans son cycle de vie (initialisé, démarré, arrêté, ...) ; des informations de nature plus architecturales (les ports, leurs états, leurs connexions, ...). 53 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — L’interface ReflectionI I 54 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — L’interface ReflectionI II La réflexion en BCM est encore en développement. // Internal behaviour requests public boolean isInStateAmong(ComponentStateI[] states) throws Exception ; public boolean notInStateAmong(ComponentStateI[] states) throws Exception ; public boolean isConcurrent() throws Exception ; public boolean canScheduleTasks() throws Exception ; Pour l’instant, elle donne accès à des opérations de la machine virtuelle sur les composants. // Implemented interfaces management public Class<?>[] getInterfaces() throws Exception ; public Class<?>[] getRequiredInterfaces() throws Exception ; public Class<?>[] getOfferedInterfaces() throws Exception ; public void addRequiredInterface(Class<?> inter) throws Exception ; public void removeRequiredInterface(Class<?> inter) throws Exception ; public void addOfferedInterface(Class<?> inter) throws Exception ; public void removeOfferedInterface(Class<?> inter) throws Exception ; public boolean isInterface(Class<?> inter) throws Exception ; public boolean isRequiredInterface(Class<?> inter) throws Exception ; public boolean isOfferedInterface(Class<?> inter) throws Exception ; Elle permet de gérer les greffons, la journalisation et la trace ainsi que les interfaces et les ports, mais ne permet que d’interroger l’état dans le cycle de vie ou de gérer les threads. public interface ReflectionI extends OfferedI, RequiredI { // Plug-ins facilities public void installPlugin(PluginI plugin) throws Exception ; public boolean hasInstalledPlugins() throws Exception ; public void uninstallPlugin(String pluginId) throws Exception ; public boolean isInstalled(String pluginId) throws Exception ; // Port management public String[] findPortURIsFromInterface(Class<?> inter) throws Exception ; public String[] findInboundPortURIsFromInterface(Class<?> inter) throws Exception ; public String[] findOutboundPortURIsFromInterface(Class<?> inter) throws Exception ; public Class<?> getPortImplementedInterface(String portURI) throws Exception ; public boolean isPortConnected(String portURI) throws Exception ; public void doPortConnection(String portURI, String otherPortURI, String ccname) throws Exception ; public void doPortDisconnection(String portURI) throws Exception ; // Logging facilities public void toggleLogging() throws Exception ; public void toggleTracing() throws Exception ; public void logMessage(String message) throws Exception ; public boolean isLogging() throws Exception ; public boolean isTracing() throws Exception ; public void printExecutionLog() throws Exception ; public void printExecutionLog(PrintStream out) throws Exception ; public void printExecutionLogOnFile(String fileName) throws Exception ; } 55 / 60 56 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Intégration des capacités réflexives dans les composants — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Court exemple d’utilisation // Code de déploiement ReflectionOutboundPort rObp ; AbstractComponent comp = new AbstractComponent(1, 0) {} ; this.addDeployedComponent(comp) ; this.rObp = new ReflectionOutboundPort(new AbstractComponent() {}) ; this.rObp.publishPort() ; String[] rpIbpURI = comp.findInboundPortURIsFromInterface(ReflectionI.class) ; this.rObp.doConnection(rpIbpURI[0], ReflectionConnector.class.getCanonicalName()); Tous les composants définissent un port entrant (inbound port) offrant l’interface ReflectionI. Le constructeur le plus courant (int, int) génère une URI pour ce port, mais un autre constructeur (String,int,int) permet de fournir une URI explicitement. // Code réflexif exécuté an sein d’un composant String[] uris = this.rObp.findInboundPortURIsFromInterface(ReflectionI.class) ; System.out.println(uris[0]) ; uris = this.rObp.findInboundPortURIsFromInterface(ComponentPluginI.class) ; System.out.println(uris[0]) ; L’interface ReflectionI est ajoutée automatiquement aux interfaces offertes, et le port entrant créé et publié. Ce qui donne le résultat suivant : 57 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Récapitulons... starting... fr.upmc.components.pre.reflection.interfaces.ReflectionI-4e9a2511-2a24-49fb-af39-7e0bb18227f9 fr.upmc.components.pre.plugins.interfaces.ComponentPluginI-7e8694a5-2eaf-417a-b260-31b981642e5 shutting down... ending... 58 / 60 — Adapt. logicielle Réfl. structurelle Connecteur proxy Réfl. comportementale Connecteur Javassist Réflexivité en BCM — Pour aller plus loin : sélection de lectures recommandées Lire l’article « Reflection in logic, functional and object-oriented programming : a Short Comparative Study » disponible sur le site de l’UE. 1 L’auto-adaptabilité logicielle peut se décomposer en quatres grands types : paramétrique, de ressources, fonctionnelle et architecturale. 2 La réflexivité logicielle est une approche permettant de mettre en œuvre des opérations d’adaptation fonctionnelle et architecturale des entités logicielles. 3 Java est un langage offrant une forme de réflexion structurelle, en particulier par son package java.lang.reflect mais également une forme limitée de réflexion de comportement par son mécanisme de proxy dynamique. Lire les tutoriaux suivants sur le WWW : http://tutorials.jenkov.com/java-reflection/index.html http://onjava.com/pub/a/onjava/2007/03/15/ reflections-on-java-reflection.html 4 Pour obtenir une forme de réflexion de comportement plus complète en Java, il faut faire appel à des bibliothèques externes, comme Javassist, permettant de créer du code dynamiquement et de l’intégrer à l’application. Lire le tutoriel Javassist disponible avec la distribution du logiciel et la page Javassist du projet JBoss à l’URL http://www.jboss.org/javassist et les articles qui sont pointés par cette dernière. 5 Les principes de la réflexion s’appliquent également à la programmation par composants, où les réflexions structurelle et de comportement sont complétées par de la réflexion architecturale. Regardez la fonctionnalité réflexion en BCM. Examiner la Javadoc l’API Reflection de Java (dans la documentation standard de J2SE). Lire la description du modèle de composants Fractal dans The Fractal Component Model, E. Bruneton, T. Coupaye et J.-B. Stefani, version 2.0-3, 2004, disponible sur le site de l’UE. 59 / 60 60 / 60