CodeGym /Blog Java /Random-ES /Proxies dinámicos en Java
Autor
Jesse Haniel
Lead Software Architect at Tribunal de Justiça da Paraíba

Proxies dinámicos en Java

Publicado en el grupo Random-ES
¡Hola! Hoy consideraremos un tema bastante importante e interesante: la creación de clases proxy dinámicas en Java. No es muy simple, así que intentaremos resolverlo usando ejemplos :) Entonces, la pregunta más importante: ¿qué son los proxies dinámicos y para qué sirven? Una clase proxy es una especie de "complemento" sobre la clase original, que nos permite cambiar el comportamiento de la clase original si es necesario. ¿Qué significa "cambiar el comportamiento" y cómo funciona? Considere un ejemplo simple. Supongamos que tenemos una interfaz Person y una clase Man simple que implementa esta interfaz.

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.
}
Nuestra clase Man tiene 3 métodos: introducir, decirEdad y decirDeDónde. Imagine que tenemos esta clase como parte de una biblioteca JAR lista para usar y no podemos simplemente reescribir su código. Pero también necesitamos cambiar su comportamiento. Por ejemplo, no sabemos a qué método se puede llamar en nuestro objeto, pero queremos que nuestra persona diga "¡Hola!" (a nadie le gusta alguien que es descortés) cuando se llama a cualquiera de los métodos. Proxies dinámicos - 2¿Qué debemos hacer en esta situación? Necesitaremos algunas cosas:
  1. Manejador de invocación

¿Qué es esto? InvocationHandler es una interfaz especial que nos permite interceptar cualquier llamada de método a nuestro objeto y agregar el comportamiento adicional que necesitamos. Necesitamos crear nuestro propio interceptor, es decir, crear una clase que implemente esta interfaz. Esto es bastante 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;
   }
}
Necesitamos implementar solo un método de interfaz: invocar() . Y, por cierto, hace lo que necesitamos: intercepta todas las llamadas de método a nuestro objeto y agrega el comportamiento necesario (dentro del método de invocación() , enviamos un "¡Hola!" a la consola).
  1. El objeto original y sus proxies.
Creamos nuestro objeto Man original y un "complemento" (proxy) para él:

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());

   }
}
¡Esto no parece muy simple! Agregué específicamente un comentario para cada línea de código. Echemos un vistazo más de cerca a lo que está pasando. En la primera línea, simplemente creamos el objeto original para el que crearemos proxies. Las siguientes dos líneas pueden causarle dificultades:

 // 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 realidad, no sucede nada realmente especial aquí :) En la cuarta línea, usamos la clase Proxy especial y su método estático newProxyInstance() :

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
Este método simplemente crea nuestro objeto proxy. Pasamos al método la información sobre la clase original, que recibimos en el último paso (su ClassLoader y una lista de sus interfaces), así como el objeto InvocationHandler creado previamente. Lo principal es no olvidar pasar nuestro objeto arnold original al controlador de invocación, de lo contrario no habrá nada que "manejar" :) ¿Con qué terminamos? Ahora tenemos un objeto proxy: proxyArnold . Puede llamar a cualquier método de la interfaz Person . ¿Por qué? Porque le dimos una lista de todas las interfaces aquí:

// 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));
Ahora conoce todos los métodos de la interfaz Person . Además, le pasamos a nuestro proxy un objeto PersonInvocationHandler configurado para trabajar con el objeto arnold :

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
Ahora bien, si llamamos a cualquier método de la interfaz Person en el objeto proxy, nuestro controlador intercepta la llamada y ejecuta su propio método de invocación() en su lugar. ¡Intentemos ejecutar el método main() ! Salida de la consola:

Hi!
¡Excelente! Vemos que en lugar del método Person.introduce() original , se llama al método invocar() de nuestro PersonInvocationHandler() :

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

   System.out.println("Hi!");
   return null;
}
"¡Hola!" se muestra en la consola, pero este no es exactamente el comportamiento que queríamos: / Lo que estábamos tratando de lograr es mostrar primero "¡Hola!" y luego llamar al método original en sí mismo. En otras palabras, la llamada al método

proxyArnold.introduce(arnold.getName());
debe mostrar "¡Hola! Mi nombre es Arnold", no simplemente "¡Hola!" ¿Cómo podemos lograr esto? No es complicado: solo tenemos que tomarnos algunas libertades con nuestro controlador y el método de invocación() :) Preste atención a los argumentos que se pasan a este método:

public Object invoke(Object proxy, Method method, Object[] args)
El método invocar() tiene acceso al método invocado originalmente ya todos sus argumentos (método Método, argumentos Objeto[]). En otras palabras, si llamamos al método proxyArnold.introduce(arnold.getName()) para que se llame al método de invocación() en lugar del método de introducción() , entonces dentro de este método tenemos acceso al método de introducción() original y su argumento! Como resultado, podemos hacer esto:

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);
   }
}
Ahora, en el método de invocación () hemos agregado una llamada al método original. Si ahora intentamos ejecutar el código de nuestro ejemplo anterior:

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());
   }
}
luego veremos que ahora todo funciona como debe :) Salida de la consola:

Hi! My name is Arnold
¿Cuándo podrías necesitar esto? En realidad, bastante a menudo. El patrón de diseño "proxy dinámico" se usa activamente en tecnologías populares... Ah, por cierto, olvidé mencionar que Dynamic Proxy es un patrón de diseño. ¡Felicitaciones, aprendiste uno más! :) Proxies dinámicos - 3Por ejemplo, se usa activamente en tecnologías y marcos populares relacionados con la seguridad. Imagine que tiene 20 métodos que solo deben ejecutar los usuarios que han iniciado sesión en su programa. Usando las técnicas que ha aprendido, podría agregar fácilmente a estos 20 métodos una verificación para ver si el usuario ha ingresado credenciales válidas sin duplicar el código de verificación en cada método. O suponga que desea crear un registro donde se registrarán todas las acciones del usuario. Esto también es fácil de hacer usando un proxy. Incluso ahora, podría simplemente agregar código a nuestro ejemplo anterior para que el nombre del método se muestre cuando llame a invoque() , y eso produciría un registro súper simple de nuestro programa :) En conclusión, preste atención a una limitación importante. Un objeto proxy funciona con interfaces, no con clases. Se crea un proxy para una interfaz. Echa un vistazo a este código:

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
Aquí creamos un proxy específicamente para la interfaz de Persona . Si intentamos crear un proxy para la clase, es decir, cambiar el tipo de referencia e intentar convertir a la clase Man , no funcionará.

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

proxyArnold.introduce(arnold.getName());
Excepción en el subproceso "principal" java.lang.ClassCastException: com.sun.proxy.$Proxy0 no se puede convertir a Man Tener una interfaz es un requisito absoluto. Los proxies funcionan con interfaces. Eso es todo por hoy :) Bueno, ¡ahora sería bueno resolver algunas tareas! :) ¡Hasta la proxima vez!
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION