lunes, 28 de noviembre de 2011

Usando el Proxy e InvocationHandler de Java para leer Anotaciones



Primero que nada, gracias a este post parece que pude integrar un marcador de sintaxis decente. Gracias :D !

Bueno. Cuando estoy realizando entrevistas técnicas orientadas a Java, generalmente sale el tema de inversión de control, o del patrón de Proxy. En ambos casos, me extraña la cantidad de gente que desconoce el uso del InvocationHandler en Java (incluso, la manera de crear anotaciones). Y no sólo en entrevistas. Pero bueno, no me voy a clavar en cómo crear anotaciones porque creo que hay chorrocientos tutoriales en la red, pero no he encontrado tantos recursos para el Proxy y el InvocationHandler. Así que ahí va.

El InvocationHandler es una clase sumamente poderosa para interceptar accesos a nuestras clases, controlar errores, hacer inversión de control... Usado en conjunto con un proxy, permite "envolver" una interfaz determinada -y su implementación- y manejar los accesos a sus métodos.


 

Supongamos que tenemos la siguiente interfaz:

interface IPersona {
  public String getNombre();
}

Así mismo, la siguiente anotación:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Anotacion {
  boolean loggear();
}

Y se la aplicamos a la implementación de la interfaz:

class Persona implements IPersona {
  @Override
  @Anotacion(loggear = true)
  public String getNombre() {
    return "Pepito";
  }
}

¿Cómo hacemos para evaluar el acceso al método getNombre, la existencia de la anotación, y el valor de la misma? Ahí va.

Primero, implementamos la interfaz InvocationHandler. Forzosamente tenemos que sobreescribir el método "invoke". Recibe 3 parámetros:

  1. El "proxy" desde el cual se accedió (más de esto en un momento)
  2. El método que se quiso ejecutar
  3. Los parámetros del método (un arreglo de objetos)
Ahora, el mentado "Proxy" no es sino el envoltorio para nuestro objeto original (la implementación de IPersona, en este caso). Para no perder referencia a él, me he tomado la libertad de crear un constructor que lo recibe y lo almacena como una variable de instancia. Pongo aquí la implementación, y ahorita vemos más a detalle qué hace:

class Handler implements InvocationHandler {
  private IPersona objetivo;
  public Handler(IPersona objetivo) {
    this.objetivo = objetivo;
  }
  @Override
  public Object invoke(Object proxy, Method method, Object[] params)
   throws Throwable {
    method = objetivo.getClass().getMethod(method.getName(), method.getParameterTypes());
    if (method.getAnnotation(Anotacion.class) != null) {
      System.out.println("Anotacion presente");
      if (method.getAnnotation(Anotacion.class).loggear()) {
        System.out.println("Alguien accedio al nombre de la persona");
      }
    }
    return method.invoke(objetivo, params);
  }
}

En realidad, lo ideal sería no sobreescribir "method", sino almacenarlo en una variable nueva. Pero... Meh. El punto es que busco el método equivalente en el objetivo, e inspecciono si tiene declarada la anotación. De ser así, veo si el atributo loggear está como verdadero. E imprimo los textos correspondientes. Al final, evidentemente, tengo que seguir con la invocación del método.

Aquí es donde decía que el InvocationHandler es poderoso: ¿qué pasa si mi anotación hace algo más que loggear? Validar, por ejemplo. ¿Qué pasa si pongo chequeo de errores en el "return"? ¿Si lo uso para auditar? Las posibilidades son muchas.

Ahora, ¿cómo meto el objeto en un Proxy y le adjunto el InvocationHandler? Yo hice un método "envolver" para facilitarme la existencia.

public static IPersona envolver(IPersona objetivo) {
  Handler h = new Handler(objetivo);
  return (IPersona) Proxy.newProxyInstance(objetivo.getClass()
   .getClassLoader(), new Class[] { IPersona.class }, h);
}

Evidentemente, si esto fuera un ambiente real, también tendría que jugar con el constructor de la Persona para evitar que este proceso de envolver se pasara por alto. Probablemente metiéndolo en un Factory. Pero de por sí este ejemplo es algo extendido, no voy a ir más allá.

El método "newProxyInstance" recibe 3 parámetros:
  1. El ClassLoader de la clase objetivo a "envolver"
  2. Un arreglo de clases con las interfaces (ojo: si hay alguna clase aquí te va a mandar a volar, tienen que ser interfaces) que implementa el objetivo, o de perdida, aquellas que desees monitorear
  3. El handler que se va a adjuntar
Por eso es importante intentar programar con interfaces...

Mi clase principal completa sería algo como:

public class Prueba {
  public static void main(String args[]) {
    IPersona persona = new Persona();
    persona = envolver(persona);
    persona.getNombre();
  }
  public static IPersona envolver(IPersona objetivo) {
    Handler h = new Handler(objetivo);
    return (IPersona) Proxy.newProxyInstance(objetivo.getClass()
     .getClassLoader(), new Class[] { IPersona.class }, h);
  }
}

Y la salida en consola:

Anotacion presente
Alguien accedio al nombre de la persona

Espero que esto sea de utilidad. En caso de tener dudas, por favor dejen un comentario.

Saludos,

Posts relacionados