Dans le développement logiciel, il est souvent nécessaire d'ajouter des fonctionnalités transversales comme la journalisation ou la mesure du temps d'exécution sans modifier le code source des classes existantes. Cet article explore comment utiliser le patron de conception proxy, en particulier les proxies dynamiques en Java, pour résoudre ce problème.
Considérons une interface simple pour un déplacement et une classe qui l'implémente :
package com.example.proxy;
public interface Deplacement {
void seDeplacer();
void arreter();
}
package com.example.proxy;
import java.util.Random;
public class Vehicule implements Deplacement {
@Override
public void seDeplacer() {
System.out.println("Véhicule en mouvement...");
try {
Thread.sleep(new Random().nextInt(5000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void arreter() {
System.out.println("Véhicule arrêté.");
}
}
Si nous ne pouvons pas modifier la classe Vehicule, nous devons créer un proxy pour mesurer le temps d'exécution des méthodes. Une approche par héritage peut être utilisée, mais elle manque de flexibilité :
package com.example.proxy;
public class VehiculeProxyTempsHeritage extends Vehicule {
@Override
public void seDeplacer() {
long debut = System.currentTimeMillis();
super.seDeplacer();
long fin = System.currentTimeMillis();
System.out.println("Durée: " + (fin - debut) + " ms");
}
@Override
public void arreter() {
long debut = System.currentTimeMillis();
super.arreter();
long fin = System.currentTimeMillis();
System.out.println("Durée: " + (fin - debut) + " ms");
}
}
Une meilleure approche est la composition, où le proxy implémente la même interface et enveloppe l'objet cible :
package com.example.proxy;
public class ProxyTemps implements Deplacement {
private Deplacement cible;
public ProxyTemps(Deplacement cible) {
this.cible = cible;
}
@Override
public void seDeplacer() {
long debut = System.currentTimeMillis();
cible.seDeplacer();
long fin = System.currentTimeMillis();
System.out.println("Durée: " + (fin - debut) + " ms");
}
@Override
public void arreter() {
long debut = System.currentTimeMillis();
cible.arreter();
long fin = System.currentTimeMillis();
System.out.println("Durée: " + (fin - debut) + " ms");
}
}
Cette composition permet d'empiler facilement plusieurs proxies, comme un proxy de journalisation :
package com.example.proxy;
public class ProxyJournalisation implements Deplacement {
private Deplacement cible;
public ProxyJournalisation(Deplacement cible) {
this.cible = cible;
}
@Override
public void seDeplacer() {
System.out.println("Journalisation: début seDeplacer");
cible.seDeplacer();
System.out.println("Journalisation: fin seDeplacer");
}
@Override
public void arreter() {
System.out.println("Journalisation: début arreter");
cible.arreter();
System.out.println("Journalisation: fin arreter");
}
}
Pour utiliser ces proxies ensemlbe, nous pouvons les chaîner :
package com.example.proxy;
public class ClientProxyStatique {
public static void main(String[] args) {
Deplacement vehicule = new Vehicule();
ProxyTemps proxyTemps = new ProxyTemps(vehicule);
ProxyJournalisation proxyJournal = new ProxyJournalisation(proxyTemps);
proxyJournal.seDeplacer();
proxyJournal.arreter();
}
}
Cependant, si l'interface Deplacement a de nombreuses méthodes, le proxy doit redéfinir chacune d'entre elles avec une logique similaire, ce qui entraîne de la duplication. Pour résoudre cela, nous pouvons utiliser des proxies dynamiques qui génèrent du code à l'exécution.
Pour simuler un proxy dynamique, nous générons une classe source sous forme de chaîne, la compilons avec l'API Compiler de Java, puis la chargeons avec un URLClassLoader. Voici un exemple simplifié :
package com.example.proxy;
import java.io.File;
import java.io.FileWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
public class GenerateurProxy {
public static Object creerProxy(Class> iface, Deplacement cible) throws Exception {
StringBuilder methodesSrc = new StringBuilder();
for (Method m : iface.getMethods()) {
methodesSrc.append(" @Override\n");
methodesSrc.append(" public void ").append(m.getName()).append("() {\n");
methodesSrc.append(" long debut = System.currentTimeMillis();\n");
methodesSrc.append(" cible.").append(m.getName()).append("();\n");
methodesSrc.append(" long fin = System.currentTimeMillis();\n");
methodesSrc.append(" System.out.println(\"Temps pour ").append(m.getName()).append(": \" + (fin - debut) + \" ms\");\n");
methodesSrc.append(" }\n");
}
String src = "package com.example.proxy;\n" +
"public class ProxyDynamique implements " + iface.getName() + " {\n" +
" private Deplacement cible;\n" +
" public ProxyDynamique(Deplacement cible) {\n" +
" this.cible = cible;\n" +
" }\n" +
methodesSrc +
"}\n";
String chemin = System.getProperty("user.dir") + "/src/com/example/proxy/ProxyDynamique.java";
File fichier = new File(chemin);
FileWriter writer = new FileWriter(fichier);
writer.write(src);
writer.flush();
writer.close();
JavaCompiler compilateur = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager gestionnaire = compilateur.getStandardFileManager(null, null, null);
Iterable extends JavaFileObject> unites = gestionnaire.getJavaFileObjects(chemin);
compilateur.getTask(null, gestionnaire, null, null, null, unites).call();
gestionnaire.close();
URL[] urls = {new URL("file:/" + System.getProperty("user.dir") + "/src")};
URLClassLoader chargeur = new URLClassLoader(urls);
Class> classe = chargeur.loadClass("com.example.proxy.ProxyDynamique");
Constructeur constructeur = classe.getConstructor(Deplacement.class);
return constructeur.newInstance(cible);
}
}
Pour rendre le proxy plus générique, nous pouvons utiliser un gestionnaire d'invocation pour personnaliser le comportement. Java fournit l'interface InvocationHandler à cet effet :
package com.example.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class GestionnaireTemps implements InvocationHandler {
private Object cible;
public GestionnaireTemps(Object cible) {
this.cible = cible;
}
@Override
public Object invoke(Object proxy, Method methode, Object[] args) throws Throwable {
long debut = System.currentTimeMillis();
Object resultat = methode.invoke(cible, args);
long fin = System.currentTimeMillis();
System.out.println("Temps pour " + methode.getName() + ": " + (fin - debut) + " ms");
return resultat;
}
}
Avec l'API de proxy dynamique de Java, nous pouvons créer des proxies pour n'importe quelle interafce :
package com.example.proxy;
import java.lang.reflect.Proxy;
public class ClientProxyDynamique {
public static void main(String[] args) {
Deplacement vehicule = new Vehicule();
Deplacement proxy = (Deplacement) Proxy.newProxyInstance(
vehicule.getClass().getClassLoader(),
vehicule.getClass().getInterfaces(),
new GestionnaireTemps(vehicule)
);
proxy.seDeplacer();
proxy.arreter();
}
}
Cette approche dynamique élimine la nécessité de créer des classes proxy statiques pour chaque classe cible, offrant une solution élégante et réutilisable pour l'ajout de fonctionnalités transversales.