Devoxx France 2015 Jour 3 : Proxy 2.0

Le choix des conférences est toujours difficile lors d’un événement comme Devoxx : beaucoup de choix, des conférences intéressantes se passant en même temps, des sujets ou des orateurs tellement populaires que la salle est déjà complète lorsqu’on arrive…

Du coup, inévitablement, il arrive que l’on soit déçu par certaines conférences… Toutefois, le dernier jour de Devoxx, c’est avec confiance que je me suis dirigé vers la conférence de Rémi Forax : Proxy 2.0 : j’avais une bonne idée de ce qui m’attendait (de la magie noire “javaesque”) et j’ai été servi… De quoi terminer Devoxx en beauté.

Je n’ai pas trouvé les slides de sa présentation sur le net, mais il s’est manifestement basé sur une conférence similaire qu’il a donnée à JFokus et dont les slides se trouvent ici (à Devoxx, nous avons eu le droit à une version légèrement moins détaillée).

Les proxy dynamiques du JDK

Il a commencé par nous exposer les limites des proxy dynamiques de java (java.lang.reflect.Proxy introduit en Java 1.3).

En particulier :

  • ils sont fortement basés sur les mécanismes de réflection de Java et donc peu performants (en comparaison à un appel direct de méthode optimisable par la JVM)
  • ils n’ont pas de support spécifique sur les default methods introduites en Java 8 sur les interfaces (Recognize and Conquer Java Proxies, Default Methods and Method Handles)
  • ils sont souvent source de stacktraces interminables

“invokedynamic” à la rescousse

L’approche qu’il propose avec Proxy2 est basée sur l’utilisation de l’instruction “invokedynamic” (“indy”) introduite en Java 7. Son utilisation dans le proxy qu’il génère (à l’aide de la librairie ASM) permet à la JVM d’optimiser les appels effectués à travers ce proxy.

En effet, la JVM (ou plus exactement le JIT (Just In Time Compiler) comprend en profondeur les appels de méthodes induits par invokedynamic et est capable d’optimiser ces appels. En particulier, en lui permettant d’identifier les méthodes concrètes à appeler au final, la JVM va être capable d’inliner ces appels ; et comme nous l’a rappelé Rémi : l’inlining est la mère de toutes les optimisations (cela rend par exemple plus efficace l'”escape analysis” qui permet de réduire la durée de vie des objets et ainsi optimiser leur allocation en mémoire).

Pour pouvoir utiliser l’instruction invokedynamic, il faut lui fournir un java.lang.invoke.CallSite décrivant la ou les méthode(s) concrète(s) qui devront être appelées grâce à des java.lang.invoke.MethodHandle.

En toute première approche, un MethodHandle peut être considéré naïvement comme une simple référence exécutable vers une méthode existante. Mais la puissance de l’approche réside dans le fait qu’il est possible de créer de nouveaux MethodHandle en adaptant les signatures et/ou en combinant des MethodHandle et ainsi créer une référence exécutable vers un arbre d’opérations qui au final pourra être très complexe.

Les possibilités sont assez vastes :

Contrairement à un proxy classique dont l’InvocationHandler est appelé à chaque invocation, le CallSite associé à chaque méthode du type proxyfié n’est construit qu’une seule fois : lors du premier appel. Il est ensuite utilisé directement par la JVM lors des appels suivants (il s’agit du fonctionnement normal de invokedynamic)

Le lecteur intéressé pourra creuser le sujet en regardant cet article par exemple : Method handles and invokedynamic.

Tout comme un usage classique de java.lang.Proxy, Proxy2 (via les possibilités des MethodHandle) est en particulier capable de :

  • déléguer simplement les appels à une instance du type cible
  • de bypasser complètement l’appel et de renvoyer directement un résultat sans appeler un objet réel sous-jacent
  • ou d’appeler du code avant et/ou après l’appel à l’implémentation de l’instance cible

Les exemples d’utilisation qu’il nous a présentés montrent toutefois que l’utilisation de Proxy2 est bien plus complexe que java.lang.Proxy : les API de java.lang.invoke restent très bas niveau et la création de MethodHandle non triviaux est complexe. Rémi Forax fournit dans sa lib Proxy2 une classe MethodBuilder censée faciliter leur définition mais il reste nécessaire d’avoir une très bonne connaissance des mécanismes mis en oeuvre.

Un exemple de proxy

Voici donc un exemple “simple” : la création d’un proxy qui délègue tous les appels à la méthode println d’un PrintStream fourni en paramètre à la création du proxy :

public interface Delegation {

 public void println(String message);

 public static void main(String[] args) {

   ProxyFactory<Delegation> factory = Proxy2.createAnonymousProxyFactory(Delegation.class, new Class<?>[] { PrintStream.class },
       new ProxyHandler.Default() {
         @Override
         public CallSite bootstrap(ProxyContext context) throws Throwable {
           MethodHandle target =
             methodBuilder(context.type())
               .dropFirstParameter()
               .thenCall(publicLookup(), PrintStream.class.getMethod("println", String.class));
           return new ConstantCallSite(target);
         }
       });

   Delegation hello = factory.create(System.out);
   hello.println("hello proxy2");
 }

}

Le MethodHandle demandé par Proxy2 doit avoir un type (MethodType) précis (dérivé du type de la méthode implémentée et des paramètres de la factory) :

  • ce type peut être obtenu via l’appel à context.type()
  • ici, il correspond à un appel renvoyant void et prenant en paramètres : - l’objet proxy lui-même
  • l’objet PrintStream passé en paramètre de la Factory
  • le paramètre String (celui obtenu lors de l’appel de Delegation.println(String) sur le proxy)

Le but de ce proxy est de pouvoir appeler la méthode PrintStream.println sur l’objet System.out. Or, le MethodHandle associé à la méthode PrintStream.println est un appel renvoyant void et ne prenant en paramètre que :

  • l’objet PrintStream sur lequel appeler la méthode println
  • le (vrai) paramètre String à passer à la méthode println

C’est pour cela que la construction du MethodHandle dans la méthode bootstrap commence par “dropper” (ignorer) le premier paramètre (qui contient l’objet proxy ici) : cela permet d’adapter la liste des paramètres aux types nécessaires à l’appel de PrintStream.println.

Quelques bonus

En bonus, Rémi nous a fait du teasing sur certaines fonctionnalités internes cachées de la JVM qu’il utilise dans sa librairie :

  • le proxy qu’il génère est une classe qui n’est associée à aucun classloader (?!) - Il utilise pour cela la méthode interne sun.misc.Unsafe.defineAnonymousClass()
  • contrairement à la notion de classe anonyme du langage java (qui n’est pas vraiment anonyme pour la JVM), c’est une classe “anonyme” pour la JVM elle-même: au sens où elle n’a pas d’identité en terme de classloading
  • si une exception est renvoyée par le code appelé par le proxy, on ne verra pas le proxy lui-même dans la stack trace : pourtant, il y a bien un objet proxy mis en oeuvre et qui porte les instructions invokedynamic … - Rémi utilise pour cela l’annotation interne java.lang.invoke.LambdaForm.Hidden dont la JVM se sert pour cacher certains détails d’implémentation des lambdas

Bien sûr, le commun des mortels n’est pas censé utiliser ces fonctionnalités mais Rémi, lui, n’en fait pas partie 😉

Et pour terminer en beauté, Rémi nous a expliqué comment il a rendu sa librairie compatible avec Java 7.

En effet, il utilise des lambdas dans son code ce qui le rend incompatible avec Java 7. Il a donc écrit un rewriter de bytecode capable de transformer les lambdas utilisées dans sa librairie pour les rendre compatibles avec Java 7 (et pour cela il utilise bien évidemment invokedynamic déjà présent dans Java 7 mais aussi Proxy2 lui-même qui sert à “caster” le code des lambdas vers l’interface fonctionnelle cible (“target type”)).

Conclusion

L’implémentation de Rémi reste “fragile“ : elle utilise des fonctionnalités cachées et non supportées de la JVM. L’idéal serait qu’il réussisse à faire intégrer ce nouveau proxy dans la JVM elle-même. A priori, il y travaille.

Toutefois, vue la complexité de son utilisation, il est probable que ce type de proxy reste réservé aux développeurs de frameworks mais les gains pourraient être intéressants.

Au final, comme je vous le disais en introduction, ce fut une conférence à la hauteur de mes espérances… Même si, comme certains, j’ai été un peu frustré qu’il ne nous ait pas montré le moindre bout de bytecode cette fois-ci 😉

Author image
Fort de plus de 20 ans d'expérience autour de l'écosystème JavaEE, Fabien accompagne ses clients dans la conception et le cadrage de leurs projets.