Classes de proxy dynamiques

Contenu

Introduction
API de proxy dynamique
Sérialisation
Exemples

Introduction

Une classe de proxy dynamique est une classe qui implémente une liste d’interfaces spécifiées à l’exécution de telle sorte qu’une invocation de méthode via l’une des interfaces d’une instance de la classe sera codée et envoyée à un autre objet via une interface uniforme. Ainsi, une classe proxy dynamique peut être utilisée pour créer un objet proxy sécurisé pour une liste d’interfaces sans nécessiter la génération préalable de la classe proxy, par exemple avec des outils de compilation.Les invocations de méthode sur une instance d’une classe de proxy dynamique sontappariées à une seule méthode dans le gestionnaire invocationhandler de l’instance, et elles sont codées avec un objet java.lang.reflect.Method identifiant la méthode qui a été invoquée et un tableau de type Object contenant les arguments.

Les classes de proxy dynamiques sont utiles à une application ou à une bibliothèque qui doit fournir une répartition réfléchissante de type sûr des invocations sur des objets qui présentent des API d’interface. Par exemple, une application peut utiliser une classe proxy dynamique pour créer un objet qui implémente plusieurs interfaces d’écoute d’événements arbitraires – interfaces qui s’étendent java.util.EventListener – pour traiter une variété d’événements de différents types de manière uniforme, par exemple en enregistrant tous ces événements dans un fichier.

API de classe de proxy dynamique

Une classe de proxy dynamique (simplement appelée proxyclass ci-dessous) est une classe qui implémente une liste d’interfaces spécifiées lors de l’exécution lorsque la classe est créée.

Une interface proxy est une interface telle qu’elle est implémentée par une classe proxy.

Une instance proxy est une instance d’une proxyclass.

Création d’une classe Proxy

Les classes Proxy, ainsi que leurs instances, sont créées à l’aide des méthodes statiques de la classe java.lang.refléter.Proxy.

La méthode Proxy.getProxyClass renvoie l’objet java.lang.Class pour une classe proxy donnée à un chargeur de classe et à un tableau d’interfaces. La classe proxy sera définie dans le chargeur de classe spécifié et implémentera toutes les interfaces fournies. Si une classe proxy pour la même permutation d’interfaces a déjà été définie dans le chargeur de classe, la classe proxy existante sera renvoyée; sinon, une classe proxy pour ces interfaces sera générée dynamiquement et définie dans le chargeur de classe.

Il existe plusieurs restrictions sur les paramètres qui peuvent être passés à Proxy.getProxyClass:

  • Tous les objets Class du tableau interfaces doivent représenter des interfaces, des notclasses ou des types primitifs.
  • Aucun élément du tableau interfaces ne peut faire référence à des objets Class identiques.
  • Tous les types d’interface doivent être visibles par leur nom via le chargeur de classe spécifié. En d’autres termes, pour le chargeur de classe cl et chaque interface i, l’expression suivante doit être vraie:
     Class.forName(i.getName(), false, cl) == i
  • Toutes les interfaces non publiques doivent être dans le même package ; sinon, il ne serait pas possible pour la classe proxy d’implémenter toutes les interfaces, quel que soit le package dans lequel elle est définie.
  • Pour tout ensemble de méthodes membres des interfaces spécifiées qui ont la même signature:
    • Si le type de retour de l’une des méthodes est un type primitif orvoid, toutes les méthodes doivent avoir le même type de retour.
    • Sinon, l’une des méthodes doit avoir un type de retour qui peut être attribué à tous les types de retour des autres méthodes.
  • La classe proxy résultante ne doit pas dépasser les limites imposées surclasses par la machine virtuelle. Par exemple, la machine virtuelle peut limiter le nombre d’interfaces qu’une classe peut implémenter à 65535 ; dans ce cas, la taille du tableau interfaces ne doit pas dépasser 65535.

Si l’une de ces restrictions est violée, Proxy.getProxyClass lancera un IllegalArgumentException. Si l’argument de tableau interfaces ou l’un de ses éléments est null, un NullPointerException sera détruit.

Notez que l’ordre des interfaces proxy spécifiées est significatif : deux demandes pour une classe proxy avec la même combinaison d’interfaces mais dans un ordre différent se traduiront par deux classes proxy distinctes. Les classes de proxy se distinguent par le nombre de leurs interfaces de proxy afin de fournir un codage d’invocation de méthode déterministe dans les cas où deux ou plusieurs des interfaces de proxy partagent une méthode avec le même nom et la même signature de paramètres; ce raisonnement est décrit plus en détail dans la section ci-dessous intitulée Méthodes dupliquées dans plusieurs interfaces de proxy.

Pour qu’une nouvelle classe de proxy n’ait pas besoin d’être générée chaque fois que Proxy.getProxyClass est invoqué avec le même chargeur de classe et la même liste d’interfaces, l’implémentation de l’API de classe proxy dynamique doit conserver un cache de classes de proxy générées, saisies par leurs chargeurs et leur liste d’interfaces correspondants.L’implémentation doit faire attention à ne pas se référer aux chargeurs de classes, aux interfaces et aux classes proxy de manière à empêcher les chargeurs de classes et toutes leurs classes d’être collectés le cas échéant.

Propriétés de la classe Proxy

Une classe proxy possède les propriétés suivantes:

  • Les classes proxy sont publiques, finales et non abstraites.
  • Le nom non qualifié d’une classe proxy n’est pas spécifié. L’espace des noms de classes commençant par la chaîne "$Proxy" doit cependant être réservé aux classes proxy.
  • Une classe proxy s’étend java.lang.reflect.Proxy.
  • Une classe proxy implémente exactement les interfaces spécifiées lors de sa création, dans le même ordre.
  • Si une classe proxy implémente une interface non publique, elle sera définie dans le même package que cette interface. Sinon, le package d’une classe proxy n’est pas non plus spécifié. Notez que packagesealing n’empêchera pas une classe proxy d’être définie avec succès dans un package particulier au moment de l’exécution, et ni willclasses déjà définies dans le même chargeur de classe et le même paquet avec des signataires particuliers.
  • Étant donné qu’une classe proxy implémente toutes les interfaces spécifiées à sa création, invoquer getInterfaces sur son objet Class renverra un tableau contenant la même liste d’interfaces (dans l’ordre spécifié à sa création), invoquer getMethods sur son objet Class renverra un tableau d’objets Method qui incluenttoutes les méthodes dans ces interfaces, et invoquer getMethod trouvera des méthodes dans les interfaces proxy comme prévu.
  • La méthode Proxy.isProxyClass retournera true si une classe proxy est transmise – une classe renvoyée par Proxy.getProxyClass ou la classe d’un objet retournée par Proxy.newProxyInstance – et false sinon. La fiabilité de cette méthode est importante pour pouvoir l’utiliserpour prendre des décisions de sécurité, son implémentation ne doit donc pas simplement tester si la classe en question s’étend java.lang.reflect.Proxy.
  • Le java.security.ProtectionDomain d’une proxyclass est le même que celui des classes système chargées par le chargeur bootstrapclass, telles que java.lang.Object, car le code d’une classe proxy est généré par du code système de confiance. Ce domaine de protection sera généralement accordé java.security.AllPermission.

Création d’une instance de proxy

Chaque classe de proxy a un constructeur public qui prend oneargument, une implémentation de l’interface InvocationHandler.

Chaque instance de proxy a un objet de gestionnaire d’appel associé, celui qui a été transmis à son constructeur. Plutôt que d’avoir recours à l’API de réflexion pour accéder au constructeur public, un exemple de proxy peut également être créé en appelant la méthode Proxy.newProxyInstance, qui combine les actions d’appel Proxy.getProxyClass avec l’appel du constructeur avec un gestionnaire d’invocation.Proxy.newProxyInstance lance IllegalArgumentException pour les mêmes raisons que Proxy.getProxyClass.

Propriétés d’instance de proxy

Une instance de proxy possède les propriétés suivantes:

  • Étant donné une instance de proxy proxy et l’une des interfaces implémentées par sa classe de proxy Foo, l’expression suivante retournera true:
     proxy instanceof Foo

    et l’opération de conversion suivante réussira (plutôt que de lancer una ClassCastException):

     (Foo) proxy
  • La méthode static Proxy.getInvocationHandler retournera le gestionnaire d’invocation associé au proxy instancepassed comme argument. Si l’objet passé à Proxy.getInvocationHandler n’est pas une instance proxy, alors un IllegalArgumentException sera lancé.
  • Une invocation de méthode d’interface sur une instance proxy sera codée et envoyée à la méthode invoke du gestionnaire d’invocation comme décrit ci-dessous.

    L’instance de proxy elle-même sera passée en tant que premier argument de invoke, qui est de type Object.

    Le deuxième argument passé à invoke sera l’instance java.lang.reflect.Method correspondant à la méthode d’interface invoquée sur l’instance proxy. La classe déclarante de l’objet Method sera l’interface dans laquelle themethod a été déclarée, qui peut être une superinterface de l’interface proxy par laquelle la classe proxy hérite de la méthode.

    Le troisième argument passé à invoke sera un tableau d’objets contenant les valeurs des arguments passés dans l’invocation de la méthode sur l’instance proxy. Les arguments des primitivetypes sont encapsulés dans une instance de la classe primitivewrapper appropriée, telle que java.lang.Integer ou java.lang.Boolean. La mise en œuvre de la méthode invoke est libre de modifier le contenu de ce tableau.

    La valeur renvoyée par la méthode invoke deviendra la valeur de retour de l’appel de méthode sur l’instance de proxy. Si la valeur de retour déclarée de la méthode d’interface est un primitivetype, la valeur renvoyée par invoke doit être une instance de la classe wrapper primitive correspondante ; sinon, il doit s’agir d’un type assignable au type de retour déclaré. Si la valeur renvoyée par invoke vaut null et que le type de retour de la méthode d’interface est primitif, alors un NullPointerException sera lancé par la methodinvocation sur l’instance de proxy. Si la valeur renvoyée par invoke n’est pas compatible avec le type de retour déclaré de la méthode comme décrit ci-dessus, un ClassCastException sera émis par le proxy.

    Si une exception est levée par la méthode invoke, elle le sera également par l’invocation de la méthode sur l’instance de proxy.Le type de l’exception doit être assignable à l’un des types d’exception déclarés dans la signature de la méthode d’interface ou aux types d’exception non contrôlés java.lang.RuntimeException ou java.lang.Error. Si une exception vérifiée est levée par invoke qui n’est assignable à aucun des types d’exception déclarés dans la clause throws de l’interfacemethod, alors une UndeclaredThrowableException sera levée par l’invocation de la méthode sur l’instance proxy. Le UndeclaredThrowableException sera construit avecl’exception qui a été levée par la méthode invoke.

  • Une invocation des méthodes hashCode, equals ou toString déclarées dans java.lang.Object sur une instance proxy sera encodée et envoyée à la méthode invoke du gestionnaire d’invocation de la même manière que les invocations de méthode d’interface sont codées et distribuées, comme décrit ci-dessus. La classe déclarante de l’objet Method passé à invoke sera java.lang.Object. D’autres méthodes publiques d’une instance proxyinstance héritée de java.lang.Object ne sont pas overridées par une classe proxy, donc les invocations de ces méthodes se comportent comme elles le font pour les instances de java.lang.Object.

Méthodes dupliquées dans des interfaces Multiproxy

Lorsque deux interfaces ou plus d’une classe proxy contiennent une méthodeavec le même nom et la même signature de paramètre, l’ordre des interfaces de la classe proxy devient significatif. Lorsqu’une telle méthode de duplication est invoquée sur une instance de proxy, l’objet Method transmis au gestionnaire d’invocation ne sera pas nécessairement celui dont la classe déclarante peut être assignée à partir du type de référence de l’interface via laquelle la méthode du proxy a été invoquée. Cette limitation existe car l’implémentation de la méthode correspondante dans la classe proxy générée ne peut pas déterminer par quelle interface elle a été invoquée. Par conséquent, lorsqu’une méthode dupliquée est appelée sur une instance de proxy, l’objet Method de la méthode dans l’interface principale qui contient la méthode (directement ou héritée via une superinterface) dans la liste des interfaces de la classe proxy est transmis à la méthode invoke du gestionnaire d’invocation, quel que soit le type de référence à travers lequel l’invocation de la méthode s’est produite.

Si une interface proxy contient une méthode avec le même nom et la même signature de paramètre que les méthodes hashCode, equals ou toString de java.lang.Object, lorsqu’une telle méthode est invoquée sur une instance aproxy, l’objet Method passé au gestionnaire d’invocation aura java.lang.Object comme classe de déclaration. En d’autres termes, les méthodes publiques non finales de java.lang.Object précèdent logiquement toutes les interfaces proxy pour déterminer quel objet Method transmettre au gestionnaire d’invocation.

Notez également que lorsqu’une méthode dupliquée est envoyée à un gestionnaire d’invocation, la méthode invoke ne peut lancer que des types d’exception vérifiés qui peuvent être assignés à l’un des types d’exception de la clause throws de la méthode dans toutes les interfaces proxy via lesquelles elle peut être invoquée. Si la méthode invoke lève une exception vérifiée qui n’est assignable à aucun des types d’exception déclarés par la méthode dans l’une des interfaces proxy via lesquelles elle peut être invoquée, alors anunchecked UndeclaredThrowableException sera lancé lors de l’appel sur l’instance proxy. Cette restriction signifie que tous les types d’exception renvoyés en invoquant getExceptionTypes sur l’objet Method passé à la méthode invoke ne peuvent pas nécessairement être lancés avec succès par la méthode invoke.

Sérialisation

Depuis que java.lang.reflect.Proxy implémente java.io.Serializable, les instances de proxy peuvent êtresérialisées, comme décrit dans cette section. Si une instance proxy contient un gestionnaire d’appel qui n’est pas assignable à java.io.Serializable, cependant, un java.io.NotSerializableException sera lancé si une telle instance est écrite dans un java.io.ObjectOutputStream. Notez que pour les proxyclasses, l’implémentation de java.io.Externalizable a le même effet en ce qui concerne la sérialisation que l’implémentation de java.io.Serializable : les méthodes writeExternal et readExternal de l’interface Externalizable ne seront jamais invoquées sur une instance aproxy (ou un gestionnaire d’invocation) dans le cadre de son processus de sérialisation. Comme pour tous les objets Class, l’objet Class d’une classe proxy est toujours sérialisable.

Une classe proxy n’a pas de champs sérialisables et un serialVersionUID de 0L. En d’autres termes, lorsque l’objet Class d’une classe proxy est passé à la méthode statique lookup de java.io.ObjectStreamClass, l’instance ObjectStreamClass renvoyée aura les propriétés suivantes:

  • Invoquer sa méthode getSerialVersionUID retournera 0L.
  • Invoquer sa méthode getFields renverra un tableau de longueur nulle.
  • Invoquer sa méthode getField avec n’importe quel argument String renverra null.

Le protocole stream pour la sérialisation d’objets prend en charge un code type nommé TC_PROXYCLASSDESC, qui est un symbole de terminaison dans la grammaire du format stream; son type et sa valeur sont définis par le champ constant suivant dans l’interface java.io.ObjectStreamConstants:

 final static byte TC_PROXYCLASSDESC = (byte)0x7D;

La grammaire comprend également les deux règles suivantes, la première étant une extension alternative de la newClassDescrule d’origine :

newClassDesc :
TC_PROXYCLASSDESC newHandle proxyClassDescInfo

proxyClassDescInfo:
(int)<count> proxyInterfaceName classAnnotationsuperClassDesc

Nom de l’interface PROXY:
(utf)

Lorsqu’un ObjectOutputStream sérialise le classdescriptor pour une classe qui est une classe proxy, tel que déterminé en contournant son objet Class à la méthode Proxy.isProxyClass, il utilise le code de type TC_PROXYCLASSDESC au lieu de TC_CLASSDESC, en suivant les règles ci-dessus. Dans l’extension de proxyClassDescInfo, la séquence des éléments Proxyinterfacename sont les noms de toutes les interfaces implémentées par la classe proxy, dans l’ordre dans lequel elles sont renvoyées en appelant la méthode getInterfaces sur l’objet Class. Les éléments classAnnotation Etsuperclassdesc ont la même signification que dans la règle Classdescinfo. Pour une classe proxy, superClassDescis est le descripteur de classe pour sa superclasse, java.lang.reflect.Proxy ; inclure ce descripteur permet l’évolution de la représentation sérialisée de la classe Proxy pour les instances proxy.

Pour les classes non proxy, ObjectOutputStream appelle sa méthode protected annotateClass pour autoriser les sous-classes à inscrire des données personnalisées dans le flux pour une classe particulière. Pour les proxyclasses, au lieu de annotateClass, la méthode suivante dans java.io.ObjectOutputStream est appelée avec l’objet Class pour la classe proxy:

 protected void annotateProxyClass(Class cl) throws IOException;

L’implémentation par défaut de annotateProxyClass dans ObjectOutputStream ne fait rien.

Lorsqu’un ObjectInputStream rencontre le code de type TC_PROXYCLASSDESC, il désérialise le classdescriptor pour une classe proxy du flux, formaté comme décrit ci-dessus. Au lieu d’appeler sa méthode resolveClass pour résoudre l’objet Class pour le classdescriptor, la méthode suivante dans java.io.ObjectInputStream est appelée:

 protected Class resolveProxyClass(String interfaces) throws IOException, ClassNotFoundException;

La liste des noms d’interface qui ont été désérialisés dans le descripteur proxyclass est passée en tant qu’argument interfaces à resolveProxyClass.

L’implémentation par défaut de resolveProxyClass dans ObjectInputStream renvoie les résultats de l’appel de Proxy.getProxyClass avec la liste des objets Class pour les interfaces nommées dans le paramètre interfaces. L’objet Class utilisé pour chaque nom d’interface i est la valeur réajustée en appelant

 Class.forName(i, false, loader)

loaderest le premier chargeur de classe non nul dans la pile d’exécution, ounullsi aucun chargeur de classe non nul ne se trouve sur la pile. C’est le même choix de chargeur de classe effectué par le comportement par défaut de la méthoderesolveClass. Cette même valeur deloaderest également le chargeur de classe passé àProxy.getProxyClass. SiProxy.getProxyClasslance unIllegalArgumentException,resolveClasslancera unClassNotFoundExceptioncontenant leIllegalArgumentException.

Comme une classe de proxy n’a jamais ses propres champs sérialisables, theclassdata dans la représentation de flux d’une instance de proxy se compose entièrement des données d’instance de sa superclasse, java.lang.reflect.Proxy. Proxy a un champ sérialisable, h, qui contient l’invocationhandler pour l’instance proxy.

Exemples

Voici un exemple simple qui imprime un message avant et après une invocation de méthode sur un objet qui implémente une liste arbitraire d’interfaces:

public interface Foo { Object bar(Object obj) throws BazException;}public class FooImpl implements Foo { Object bar(Object obj) throws BazException { // ... }}public class DebugProxy implements java.lang.reflect.InvocationHandler { private Object obj; public static Object newInstance(Object obj) { return java.lang.reflect.Proxy.newProxyInstance( obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new DebugProxy(obj)); } private DebugProxy(Object obj) { this.obj = obj; } public Object invoke(Object proxy, Method m, Object args) throws Throwable { Object result; try { System.out.println("before method " + m.getName()); result = m.invoke(obj, args); } catch (InvocationTargetException e) { throw e.getTargetException(); } catch (Exception e) { throw new RuntimeException("unexpected invocation exception: " + e.getMessage()); } finally { System.out.println("after method " + m.getName()); } return result; }}

Pour construire une DebugProxy pour une implémentation de l’interface Foo et appeler l’une de ses méthodes:

 Foo foo = (Foo) DebugProxy.newInstance(new FooImpl()); foo.bar(null);

Voici un exemple de classe de gestionnaire d’appel d’utilitaire qui fournit un comportement de proxy par défaut pour les méthodes héritées de java.lang.Object et implémente la délégation de certaines invocations de méthode proxy à des objets distincts en fonction de l’interface de la méthode invoquée:

import java.lang.reflect.*;public class Delegator implements InvocationHandler { // preloaded Method objects for the methods in java.lang.Object private static Method hashCodeMethod; private static Method equalsMethod; private static Method toStringMethod; static { try { hashCodeMethod = Object.class.getMethod("hashCode", null); equalsMethod = Object.class.getMethod("equals", new Class { Object.class }); toStringMethod = Object.class.getMethod("toString", null); } catch (NoSuchMethodException e) { throw new NoSuchMethodError(e.getMessage()); } } private Class interfaces; private Object delegates; public Delegator(Class interfaces, Object delegates) { this.interfaces = (Class) interfaces.clone(); this.delegates = (Object) delegates.clone(); } public Object invoke(Object proxy, Method m, Object args) throws Throwable { Class declaringClass = m.getDeclaringClass(); if (declaringClass == Object.class) { if (m.equals(hashCodeMethod)) { return proxyHashCode(proxy); } else if (m.equals(equalsMethod)) { return proxyEquals(proxy, args); } else if (m.equals(toStringMethod)) { return proxyToString(proxy); } else { throw new InternalError( "unexpected Object method dispatched: " + m); } } else { for (int i = 0; i < interfaces.length; i++) { if (declaringClass.isAssignableFrom(interfaces)) { try { return m.invoke(delegates, args); } catch (InvocationTargetException e) { throw e.getTargetException(); } } } return invokeNotDelegated(proxy, m, args); } } protected Object invokeNotDelegated(Object proxy, Method m, Object args) throws Throwable { throw new InternalError("unexpected method dispatched: " + m); } protected Integer proxyHashCode(Object proxy) { return new Integer(System.identityHashCode(proxy)); } protected Boolean proxyEquals(Object proxy, Object other) { return (proxy == other ? Boolean.TRUE : Boolean.FALSE); } protected String proxyToString(Object proxy) { return proxy.getClass().getName() + '@' + Integer.toHexString(proxy.hashCode()); }}

Les sous-classes de Delegator peuvent remplacer invokeNotDelegated pour implémenter le comportement des invocations proxymethod à ne pas déléguer directement à d’autres objets, et elles peuvent remplacer proxyHashCode, proxyEquals et proxyToString pour remplacer le comportement par défaut des méthodes dont le proxy hérite java.lang.Object.

Pour construire une Delegator pour une implémentation de l’interface Foo:

 Class proxyInterfaces = new Class { Foo.class }; Foo foo = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(), proxyInterfaces, new Delegator(proxyInterfaces, new Object { new FooImpl() }));

Notez que l’implémentation de la classe Delegator donnée ci-dessus est destinée à être plus illustrative qu’optimisée; par exemple, au lieu de mettre en cache et de comparer les objets Method pour les méthodes hashCode, equals et toString, il pourrait simplement les faire correspondre par leurs noms de chaîne, car aucun de ces noms de méthode n’est surchargé dans java.lang.Object.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.