CodeGym /Blog Java /Random-FR /Proxies dynamiques en Java
Auteur
Jesse Haniel
Lead Software Architect at Tribunal de Justiça da Paraíba

Proxies dynamiques en Java

Publié dans le groupe Random-FR
Salut! Aujourd'hui, nous allons aborder un sujet assez important et intéressant : la création de classes proxy dynamiques en Java. Ce n'est pas très simple, alors nous allons essayer de le comprendre à l'aide d'exemples :) Alors, la question la plus importante : qu'est-ce que les proxys dynamiques et à quoi servent-ils ? Une classe proxy est une sorte de "complément" en plus de la classe d'origine, qui nous permet de modifier le comportement de la classe d'origine si nécessaire. Que signifie « changer de comportement » et comment cela fonctionne-t-il ? Prenons un exemple simple. Supposons que nous ayons une interface Person et une simple classe Man qui implémente cette interface

public interface Person {

   public void introduce(String name);
  
   public void sayAge(int age);
  
   public void sayWhereFrom(String city, String country);
}

public class Man implements Person {

   private String name;
   private int age;
   private String city;
   private String country;

   public Man(String name, int age, String city, String country) {
       this.name = name;
       this.age = age;
       this.city = city;
       this.country = country;
   }

   @Override
   public void introduce(String name) {

       System.out.println("My name is " + this.name);
   }

   @Override
   public void sayAge(int age) {
       System.out.println("I am " + this.age + " years old");
   }

   @Override
   public void sayWhereFrom(String city, String country) {

       System.out.println("I'm from " + this.city + ", " + this.country);
   }

   // ...getters, setters, etc.
}
Notre classe Man a 3 méthodes : introduction, sayAge et sayWhereFrom. Imaginez que nous ayons cette classe dans le cadre d'une bibliothèque JAR prête à l'emploi et que nous ne puissions pas simplement réécrire son code. Mais nous devons aussi changer son comportement. Par exemple, nous ne savons pas quelle méthode pourrait être appelée sur notre objet, mais nous voulons que notre personne dise "Salut !" (personne n'aime quelqu'un qui est impoli) lorsque l'une des méthodes est appelée. Proxies dynamiques - 2Que devons-nous faire dans cette situation ? Nous aurons besoin de quelques éléments :
  1. InvocationHandler

Qu'est-ce que c'est? InvocationHandler est une interface spéciale qui nous permet d'intercepter tout appel de méthode à notre objet et d'ajouter le comportement supplémentaire dont nous avons besoin. Nous devons créer notre propre intercepteur, c'est-à-dire créer une classe qui implémente cette interface. C'est assez simple :

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PersonInvocationHandler implements InvocationHandler {
  
private Person person;

public PersonInvocationHandler(Person person) {
   this.person = person;
}

 @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

       System.out.println("Hi!");
       return null;
   }
}
Nous n'avons besoin d'implémenter qu'une seule méthode d'interface : "invoke()" . Et, soit dit en passant, il fait ce dont nous avons besoin : il intercepte tous les appels de méthode à notre objet et ajoute le comportement nécessaire (à l'intérieur de la méthode invoke() , nous affichons "Salut !" sur la console).
  1. L'objet d'origine et ses proxys.
Nous créons notre objet Man d'origine et un "add-on" (proxy) pour celui-ci :

import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       // Create the original object
       Man arnold = new Man("Arnold", 30, "Thal", "Austria");

       // Get the class loader from the original object
       ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

       // Get all the interfaces that the original object implements
       Class[] interfaces = arnold.getClass().getInterfaces();

       // Create a proxy for our arnold object
       Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

       // Call one of our original object's methods on the proxy object
       proxyArnold.introduce(arnold.getName());

   }
}
Cela n'a pas l'air très simple ! J'ai spécifiquement ajouté un commentaire pour chaque ligne de code. Regardons de plus près ce qui se passe. Dans la première ligne, nous fabriquons simplement l'objet d'origine pour lequel nous allons créer des proxys. Les deux lignes suivantes peuvent vous causer des difficultés :

 // Get the class loader from the original object
ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

// Get all the interfaces that the original object implements
Class[] interfaces = arnold.getClass().getInterfaces();
En fait, il ne se passe rien de vraiment spécial ici :) Dans la quatrième ligne, nous utilisons la classe spéciale Proxy et sa méthode statique newProxyInstance() :

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
Cette méthode crée simplement notre objet proxy. Nous passons à la méthode les informations sur la classe d'origine, que nous avons reçues à la dernière étape (son ClassLoader et une liste de ses interfaces), ainsi que l' objet InvocationHandler créé précédemment. L'essentiel est de ne pas oublier de passer notre objet arnold d'origine au gestionnaire d'invocation, sinon il n'y aura rien à "gérer" :) Qu'est-ce qu'on a fini avec ? Nous avons maintenant un objet proxy : proxyArnold . Il peut appeler n'importe quelle méthode de l' interface Person . Pourquoi? Parce que nous lui avons donné une liste de toutes les interfaces ici :

// Get all the interfaces that the original object implements
Class[] interfaces = arnold.getClass().getInterfaces();

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
Maintenant, il connaît toutes les méthodes de l' interface Person . De plus, nous avons passé à notre proxy un objet PersonInvocationHandler configuré pour fonctionner avec l' objet arnold :

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
Maintenant, si nous appelons une méthode de l' interface Person sur l'objet proxy, notre gestionnaire intercepte l'appel et exécute sa propre méthode invoke() à la place. Essayons d'exécuter la méthode main() ! Sortie console :

Hi!
Excellent! Nous voyons qu'au lieu de la méthode originale Person.introduce() , la méthode invoke() de notre PersonInvocationHandler() est appelée :

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

   System.out.println("Hi!");
   return null;
}
"Salut!" s'affiche sur la console, mais ce n'est pas exactement le comportement que nous voulions :/ Ce que nous essayions d'obtenir, c'est d'abord d'afficher "Salut !" puis appeler la méthode d'origine elle-même. En d'autres termes, l'appel de méthode

proxyArnold.introduce(arnold.getName());
doit afficher "Salut ! Je m'appelle Arnold", et pas simplement "Salut !" Comment pouvons-nous y parvenir? Ce n'est pas compliqué : nous avons juste besoin de prendre quelques libertés avec notre gestionnaire et la méthode invoke() :) Faites attention aux arguments passés à cette méthode :

public Object invoke(Object proxy, Method method, Object[] args)
La méthode invoke() a accès à la méthode invoquée à l'origine et à tous ses arguments (méthode Method, Object[] args). En d'autres termes, si nous appelons la méthode proxyArnold.introduce(arnold.getName()) pour que la méthode invoke() soit appelée à la place de la méthode introduction() , alors à l'intérieur de cette méthode nous avons accès à la méthode originale introduction() et son argumentation ! En conséquence, nous pouvons faire ceci :

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PersonInvocationHandler implements InvocationHandler {

   private Person person;

   public PersonInvocationHandler(Person person) {

       this.person = person;
   }

   @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       System.out.println("Hi!");
       return method.invoke(person, args);
   }
}
Maintenant, dans la méthode invoke(), nous avons ajouté un appel à la méthode d'origine. Si nous essayons maintenant d'exécuter le code de notre exemple précédent :

import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       // Create the original object
       Man arnold = new Man("Arnold", 30, "Thal", "Austria");

       // Get the class loader from the original object
       ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

       // Get all the interfaces that the original object implements
       Class[] interfaces = arnold.getClass().getInterfaces();

       // Create a proxy for our arnold object
       Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

       // Call one of our original object's methods on the proxy object
       proxyArnold.introduce(arnold.getName());
   }
}
alors nous verrons que maintenant tout fonctionne comme il se doit :) Sortie de la console :

Hi! My name is Arnold
Quand pourriez-vous en avoir besoin ? En fait, assez souvent. Le modèle de conception "proxy dynamique" est activement utilisé dans les technologies populaires... Ah, au fait, j'ai oublié de mentionner que Dynamic Proxy est un modèle de conception ! Félicitations, vous en avez appris un de plus ! :) Proxies dynamiques - 3Par exemple, il est activement utilisé dans les technologies et les cadres populaires liés à la sécurité. Imaginez que vous disposez de 20 méthodes qui ne doivent être exécutées que par des utilisateurs connectés à votre programme. En utilisant les techniques que vous avez apprises, vous pouvez facilement ajouter à ces 20 méthodes une vérification pour voir si l'utilisateur a entré des informations d'identification valides sans dupliquer le code de vérification dans chaque méthode. Ou supposons que vous souhaitiez créer un journal dans lequel toutes les actions des utilisateurs seront enregistrées. Ceci est également facile à faire en utilisant un proxy. Même maintenant, vous pouvez simplement ajouter du code à notre exemple ci-dessus afin que le nom de la méthode soit affiché lorsque vous appelez invoke() , et cela produirait un journal super simple de notre programme :) En conclusion, faites attention à une limitation importante. Un objet proxy fonctionne avec des interfaces, pas avec des classes. Un proxy est créé pour une interface. Jetez un oeil à ce code:

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
Ici, nous créons un proxy spécifiquement pour l' interface Person . Si nous essayons de créer un proxy pour la classe, c'est-à-dire de changer le type de référence et d'essayer de caster vers la classe Man , cela ne fonctionnera pas.

Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

proxyArnold.introduce(arnold.getName());
Exception dans le thread "main" java.lang.ClassCastException : com.sun.proxy.$Proxy0 ne peut pas être transtypé en Man Avoir une interface est une exigence absolue. Les proxys fonctionnent avec des interfaces. C'est tout pour aujourd'hui :) Eh bien, maintenant, ce serait bien de résoudre quelques tâches ! :) Jusqu'à la prochaine fois!
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION