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 :
- Un class loader (où définir la classe synthétisée).
- Un tableau d'interfaces que le proxy va implémenter.
- 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 Throwableproxy— l'instance proxy elle-même (rarement utilisée ; méfiez-vous d'appeler des méthodes dessus depuis l'intérieur deinvoke, cela réentrerait dans le gestionnaire et pourrait créer une boucle infinie).method— laMethodqui a été appelée ;method.getName(),method.getReturnType(), ses annotations, etc. sont tous disponibles.args— les arguments sous forme d'Object[](nullsi 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 uneClassCastException. Pour une méthodevoid, retourneznull.
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.Proxyet implémente vos interfaces. - Porte un nom généré comme
$Proxy0. - Route
equals,hashCodeettoString(les méthodes d'Object) viainvokeé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.
Ce qu'il faut retenir de l'exécution :
repoétait utilisable exactement comme unRepository—repo.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$Proxy0implémentant l'interface, et chaque appel a atterri dansLoggingHandler.invoke. Le proxy est un vraiRepository(instanceofa 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,@Cacheableet 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 defind(99)est revenu sous forme d'InvocationTargetException. Le gestionnaire l'a déencapsulée avecgetCause()et a renvoyé la vraieNoSuchElementException, 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 exposeInvocationTargetExceptionaux appelants. - Les méthodes d'
Objectpassent aussi parinvoke, donc le gestionnaire a traité spécialementmethod.getDeclaringClass() == Object.classet les a transmises directement. Sans cette garde,toString/equals/hashCodeseraient également journalisées (bruyant) ou, si vous construisiez des chaînes depuis le proxy à l'intérieur deinvoke, pourraient récurser. Gérer les méthodes d'Objectdé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$Proxy0montre qu'elle a été générée, pas écrite. Parce que l'API prend unClass<?>[]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ée →
java.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
Proxyne 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.