Le Proxy.newProxyInstance
du JDK est une fonctionnalité puissante qui permet de créer des implémentations dynamiques d'interfaces Java à l'exécution. Il fait partie de l'API Reflection et est utilisé pour l'interception de méthodes.
Voici à quoi sert Proxy.newProxyInstance
:
- Création dynamique d'implémentations d'interfaces
- Interception d'appels de méthodes
- Délégation d'appels de méthodes
- Ajout de comportements avant/après l'exécution d'une méthode
- Mise en œuvre de design patterns comme le proxy, le décorateur, ou l'adaptateur
- Accès coûteux à des ressources distantes : Les appels aux bases de données, services web ou APIs tierces sont lents et consomment des ressources.
- Besoin de performances optimales : L'application doit rester réactive même lorsqu'elle dépend de ces services lents.
- Duplication de code : Implémenter manuellement un mécanisme de cache pour chaque service entraîne une duplication de code et augmente la complexité.
- Modifications intrusives : Modifier directement les services existants pour ajouter du cache rompt le principe de responsabilité unique et peut créer des dépendances non désirées.
- Manque de transparence : Les clients du service ne devraient pas avoir à se préoccuper de l'existence d'un cache ou de son fonctionnement.
L'implémentation d'un système de mise en cache pour des services coûteux sans modifier leur code source représente un défi architectural classique.
Le code de la classe de service (Interface et une implémentation) :
package com.larbotech.cache.service; public interface DataService { String fetchData(String key); void updateData(String key, String value); } -------------------------------------------- package com.larbotech.cache.service; import java.util.concurrent.TimeUnit; public class DataServiceImpl implements DataService { @Override public String fetchData(String key) { try { System.out.println("Accès à la base de données distante pour la clé: " + key); // Simulation d'un appel réseau lent (2 secondes) TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "Données pour " + key; } @Override public void updateData(String key, String value) { try { System.out.println("Mise à jour de la base de données distante pour la clé: " + key); // Simulation d'un appel réseau lent (1 seconde) TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }
}
Solution avec Proxy dynamique
Les proxys dynamiques (Proxy.newProxyInstance
) offrent une solution élégante à ce problème en permettant d'intercepter les appels aux méthodes de manière transparente, sans modifier le code des services existants.
La première étape consiste à définir et implémenter l'interface : InvocationHandler. Cette interface est définie dans le package java.lang.reflect
et contient une seule méthode :
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
Cette interface est utilisée lors de la création d'un proxy dynamique avec la méthode Proxy.newProxyInstance()
. Chaque fois qu'une méthode est appelée sur l'objet proxy, c'est la méthode invoke()
de l'InvocationHandler
associé qui est exécutée.
L'implémentation de cette interface (CacheInvocationHandler) va contenir la logique de la fonctionnalité du cache et :
- Intercepte tous les appels au proxy
- Vérifie si les données demandées sont déjà en cache
- Appelle l'implémentation réelle seulement si nécessaire
- Stocke les résultats dans le cache pour les futures requêtes
DataService
normalement, sans savoir qu'un cache est présent, et la mise en cache est ajoutée de manière transparente sans modifier le code existant.package com.larbotech.cache.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; // Gestionnaire qui implémente la logique de cache public class CachingInvocationHandler implements InvocationHandler { private final Object target; private final Map<String, Object> cache = new HashMap<>(); public CachingInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Traitement des méthodes de lecture (fetchData) if (method.getName().equals("fetchData") && args != null && args.length > 0) { String key = (String) args[0]; String cacheKey = method.getName() + "-" + key; // Vérification du cache if (cache.containsKey(cacheKey)) { System.out.println("Cache hit pour la clé: " + key); return cache.get(cacheKey); } // Cache miss: appel au service réel Object result = method.invoke(target, args); // Mise en cache du résultat cache.put(cacheKey, result); return result; } // Traitement des méthodes de modification (updateData) else if (method.getName().equals("updateData") && args != null && args.length > 1) { String key = (String) args[0]; String cacheKey = "fetchData-" + key; // Invalidation du cache if (cache.containsKey(cacheKey)) { System.out.println("Invalidation du cache pour la clé: " + key); cache.remove(cacheKey); } // Appel au service réel return method.invoke(target, args); } // Délégation par défaut return method.invoke(target, args); } }
CachingInvocationHandler
est une classe qui intercepte les appels aux méthodes d'un service et ajoute automatiquement une fonctionnalité de mise en cache. Voici comment il fonctionne en termes simples :- Interception : Il se positionne entre l'utilisateur et le service réel pour écouter tous les appels de méthodes.
- Mémoire cache : Il maintient une "mémoire" (un dictionnaire/map) qui stocke les résultats précédents.
- Vérification intelligente : Pour chaque appel à la méthode
fetchData
, il vérifie :- "Ai-je déjà le résultat pour cette clé dans ma mémoire ?"
- Si oui → il renvoie directement ce résultat stocké (rapide)
- Si non → il appelle le service réel et stocke le résultat pour la prochaine fois
- Invalidation automatique : Quand quelqu'un appelle
updateData
pour modifier des données, il comprend que les données ont changé et supprime l'entrée correspondante de sa mémoire.
Création du proxy
public static Object newProxyInstance( ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler ) throws IllegalArgumentException
- Le ClassLoader est responsable de charger la définition de classe dans la JVM ;
- Il doit avoir accès aux interfaces que le proxy va implémenter ;
- Généralement, on utilise le ClassLoader qui a chargé les interfaces ; (
SomeInterface.class.getClassLoader()
) - Si le ClassLoader spécifié ne peut pas accéder aux interfaces, une
IllegalArgumentException
sera levée.
- Chaque élément du tableau doit être une interface (pas une classe concrète) ;
- Le proxy implémentera toutes les méthodes déclarées dans toutes ces interfaces ;
- Le tableau ne peut pas être vide, sinon une
IllegalArgumentException
sera levée ; - Si une classe du tableau n'est pas une interface, une
IllegalArgumentException
sera levée.
- Chaque appel de méthode sur le proxy sera délégué à la méthode
invoke()
du handler - L'
InvocationHandler
est le cœur du mécanisme de proxy, c'est lui qui contrôle le comportement - Il doit implémenter l'interface
java.lang.reflect.InvocationHandler
- La méthode
invoke()
reçoit l'objet proxy, la méthode appelée et les arguments
package com.larbotech.cache.proxy; import java.lang.reflect.Proxy; public class CachedServiceFactory { @SuppressWarnings("unchecked") public static <T> T createCachedService(T service) { return (T) Proxy.newProxyInstance( service.getClass().getClassLoader(), service.getClass().getInterfaces(), new CachingInvocationHandler(service) ); } }
package com.larbotech.cache; import com.larbotech.cache.service.DataService; import com.larbotech.cache.proxy.CachedServiceFactory; import com.larbotech.cache.service.DataServiceImpl; public class CachingProxyDemo { public static void main(String[] args) { // Service réel DataService realService = new DataServiceImpl(); // Service amélioré avec cache via proxy DataService cachedService = CachedServiceFactory.createCachedService(realService); long start, end; // Premier appel (sans cache) start = System.currentTimeMillis(); String result1 = cachedService.fetchData("user123"); end = System.currentTimeMillis(); System.out.println("Premier appel: " + result1); System.out.println("Temps écoulé: " + (end - start) + "ms"); // Deuxième appel (avec cache) start = System.currentTimeMillis(); String result2 = cachedService.fetchData("user123"); end = System.currentTimeMillis(); System.out.println("Deuxième appel: " + result2); System.out.println("Temps écoulé: " + (end - start) + "ms"); // Mise à jour des données cachedService.updateData("user123", "Nouvelle valeur"); // Troisième appel après mise à jour (sans cache) start = System.currentTimeMillis(); String result3 = cachedService.fetchData("user123"); end = System.currentTimeMillis(); System.out.println("Troisième appel après mise à jour: " + result3); System.out.println("Temps écoulé: " + (end - start) + "ms"); } }
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package jdk.proxy1; import com.larbotech.cache.service.DataService; import java.lang.invoke.MethodHandles; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.UndeclaredThrowableException; public final class $Proxy0 extends Proxy implements DataService { private static final Method m0; private static final Method m1; private static final Method m2; private static final Method m3; private static final Method m4; public $Proxy0(InvocationHandler var1) { super(var1); } public final int hashCode() { try { return (Integer)super.h.invoke(this, m0, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final boolean equals(Object var1) { try { return (Boolean)super.h.invoke(this, m1, new Object[]{var1}); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final String toString() { try { return (String)super.h.invoke(this, m2, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final String fetchData(String var1) { try { return (String)super.h.invoke(this, m3, new Object[]{var1}); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final void updateData(String var1, String var2) { try { super.h.invoke(this, m4, new Object[]{var1, var2}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException(var4); } } static { ClassLoader var0 = $Proxy0.class.getClassLoader(); try { m0 = Class.forName("java.lang.Object", false, var0).getMethod("hashCode"); m1 = Class.forName("java.lang.Object", false, var0).getMethod("equals", Class.forName("java.lang.Object", false, var0)); m2 = Class.forName("java.lang.Object", false, var0).getMethod("toString"); m3 = Class.forName("com.larbotech.cache.service.DataService", false, var0).getMethod("fetchData", Class.forName("java.lang.String", false, var0)); m4 = Class.forName("com.larbotech.cache.service.DataService", false, var0).getMethod("updateData", Class.forName("java.lang.String", false, var0), Class.forName("java.lang.String", false, var0)); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } } private static MethodHandles.Lookup proxyClassLookup(MethodHandles.Lookup var0) throws IllegalAccessException { if (var0.lookupClass() == Proxy.class && var0.hasFullPrivilegeAccess()) { return MethodHandles.lookup(); } else { throw new IllegalAccessException(var0.toString()); } } }
Aucun commentaire:
Enregistrer un commentaire