W3docs

Proxies dynamiques Java

Créez des implémentations proxy d'interfaces au moment de l'exécution en Java avec java.lang.reflect.Proxy et InvocationHandler.

Un proxy dynamique est un objet qui implémente une ou plusieurs interfaces, mais dont chaque appel de méthode est acheminé — au moment de l'exécution — via un seul gestionnaire que vous écrivez. La JVM synthétise la classe proxy à la volée ; vous n'écrivez jamais l'implémentation. C'est le coin le plus puissant de java.lang.reflect, et c'est ainsi que fonctionnent l'AOP, la journalisation transparente, le chargement différé, les stubs RPC et les bibliothèques de mock. Ce chapitre montre comment Proxy.newProxyInstance et InvocationHandler s'articulent, et ce qu'ils peuvent ou ne peuvent pas faire.

Les deux éléments : Proxy et InvocationHandler

Un proxy dynamique nécessite trois entrées :

  1. Un class loader (où définir la classe synthétisée).
  2. Un tableau d'interfaces que le proxy va implémenter.
  3. Un InvocationHandler — la méthode unique qui reçoit chaque appel.
InvocationHandler handler = (proxy, method, args) -> {
  // called for EVERY method invoked on the proxy
  return ...;   // becomes the method's return value
};

MyService svc = (MyService) Proxy.newProxyInstance(
    MyService.class.getClassLoader(),
    new Class<?>[]{ MyService.class },
    handler);

svc est désormais un vrai objet implémentant MyService. Appeler svc.doThing(x) n'exécute aucun corps de doThing — il n'en existe pas — cela appelle handler.invoke(proxy, <Method doThing>, [x]). Le gestionnaire décide quoi faire et quoi retourner.

La signature de invoke

Object invoke(Object proxy, Method method, Object[] args) throws Throwable
  • proxy — l'instance proxy elle-même (rarement utilisée ; méfiez-vous d'appeler des méthodes dessus depuis l'intérieur de invoke, cela réentrerait dans le gestionnaire et pourrait créer une boucle infinie).
  • method — la Method qui a été appelée ; method.getName(), method.getReturnType(), ses annotations, etc. sont tous disponibles.
  • args — les arguments sous forme d'Object[] (null si la méthode n'en prend aucun) ; les types primitifs sont encapsulés.
  • retour — ce que l'appelant doit recevoir ; doit être compatible par affectation avec method.getReturnType() ou vous obtenez une ClassCastException. Pour une méthode void, retournez null.

Un motif fréquent consiste à transmettre à un vrai objet « cible » : method.invoke(target, args) — en enveloppant cet appel avec de la journalisation, du chronométrage, des transactions ou des nouvelles tentatives. Cet appel Method.invoke est le même dispatch réflexif couvert dans Java Reflection : Méthodes ; ici il est entièrement piloté par la Method que la JVM remet à votre gestionnaire. Cette forme de transmission est l'idiome décorateur via proxy, et c'est la base du Spring AOP.

Interfaces uniquement

La contrainte la plus importante : java.lang.reflect.Proxy proxifie des interfaces, pas des classes. Vous ne pouvez pas effectuer un proxy dynamique d'une classe concrète avec cette API. Si vous avez besoin de proxifier une classe, vous faites appel à une bibliothèque de bytecode (CGLIB, ByteBuddy) qui génère une sous-classe à la place — c'est pourquoi les frameworks les embarquent. Pour les conceptions basées sur des interfaces, le Proxy intégré est suffisant et ne nécessite aucune dépendance.

La classe proxy synthétisée :

  • Étend java.lang.reflect.Proxy et implémente vos interfaces.
  • Porte un nom généré comme $Proxy0.
  • Route equals, hashCode et toString (les méthodes d'Object) via invoke également — votre gestionnaire doit donc être prêt à les gérer, ou à les déléguer de manière appropriée.

Un exemple concret : un proxy de journalisation et de chronométrage

Le programme définit une interface Repository et une vraie implémentation, puis encapsule l'implémentation dans un proxy dynamique dont le gestionnaire journalise chaque appel, le chronomètre, le transmet au vrai objet et journalise le résultat — ajoutant un comportement transversal sans toucher à l'implémentation.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • repo était utilisable exactement comme un Repositoryrepo.save(...), repo.count(), repo.find(...) ont tous compilé et fonctionné — pourtant aucune classe nommée "logging repository" n'existe dans le source. La JVM a généré une classe $Proxy0 implémentant l'interface, et chaque appel a atterri dans LoggingHandler.invoke. Le proxy est un vrai Repository (instanceof a retourné true).
  • Chaque méthode métier a obtenu une journalisation d'entrée/sortie automatique et un chronométrage sans aucune modification de InMemoryRepository. Cette séparation — l'implémentation reste ignorante, la préoccupation transversale vit dans le gestionnaire — est tout l'intérêt de l'AOP, et les proxies dynamiques sont la façon dont Spring implémente @Transactional, @Cacheable et leurs équivalents pour les beans basés sur des interfaces.
  • Le gestionnaire a transmis chaque appel avec method.invoke(target, args), ce qui signifie qu'un échec de find(99) est revenu sous forme d'InvocationTargetException. Le gestionnaire l'a déencapsulée avec getCause() et a renvoyé la vraie NoSuchElementException, de sorte que l'appelant a capturé l'exception naturelle plutôt qu'un wrapper de réflexion. Un proxy qui oublie de déencapsuler expose InvocationTargetException aux appelants.
  • Les méthodes d'Object passent aussi par invoke, donc le gestionnaire a traité spécialement method.getDeclaringClass() == Object.class et les a transmises directement. Sans cette garde, toString/equals/hashCode seraient également journalisées (bruyant) ou, si vous construisiez des chaînes depuis le proxy à l'intérieur de invoke, pourraient récurser. Gérer les méthodes d'Object délibérément est une partie standard de l'écriture d'un gestionnaire de proxy.
  • Proxy.isProxyClass(repo.getClass()) a confirmé que la classe est synthétisée par la JVM, et son nom $Proxy0 montre qu'elle a été générée, pas écrite. Parce que l'API prend un Class<?>[] d'interfaces, un proxy peut en implémenter plusieurs à la fois — ce qui explique comment un seul mock ou stub peut satisfaire plusieurs contrats simultanément.

Quand utiliser quoi

  • Interface, aucune dépendance souhaitéejava.lang.reflect.Proxy. Intégré, simple, uniquement pour les interfaces.
  • Besoin de proxifier une classe concrète → ByteBuddy ou CGLIB (basé sur des sous-classes). Nécessaire car Proxy ne peut pas le faire.
  • Besoin simplement de stubber des interfaces dans les tests → une bibliothèque de mock (Mockito) construite sur ces mécanismes — ne la réinventez pas à la main.

Les proxies dynamiques clôturent la partie réflexion : depuis l'inspection d'un objet Class, jusqu'à la lecture et l'écriture de champs, l'invocation de méthodes, la construction d'instances via des constructeurs, la lecture d'annotations, et finalement la synthèse d'implémentations complètes au moment de l'exécution. Ensemble, ils constituent la boîte à outils qui permet aux frameworks d'opérer génériquement sur des types contre lesquels ils n'ont jamais été compilés — utilisés avec parcimonie et derrière des abstractions propres, ils rendent possible l'écosystème Java de conteneurs, mappeurs et runners.

Pratique

Pratique
Vous souhaitez encapsuler un service défini par une interface 'PaymentGateway' afin que chaque appel de méthode soit journalisé, sans modifier la vraie implémentation. Vous appelez 'Proxy.newProxyInstance(...)' en passant 'new Class<?>[]{ PaymentGateway.class }' et un gestionnaire. Dans 'invoke' du gestionnaire, quelle est la façon standard de produire le résultat réel de la méthode ?
Vous souhaitez encapsuler un service défini par une interface 'PaymentGateway' afin que chaque appel de méthode soit journalisé, sans modifier la vraie implémentation. Vous appelez 'Proxy.newProxyInstance(...)' en passant 'new Class<?>[]{ PaymentGateway.class }' et un gestionnaire. Dans 'invoke' du gestionnaire, quelle est la façon standard de produire le résultat réel de la méthode ?
Was this page helpful?