Javassist ProxyFactory

4 minuto de lectura

Directo al grano, cómo construir un proxy con la librería javassist. Si quieres saber que es un proxy o un dynamic proxy, puedes revisar los post anteriores, Patrones De Diseño: Proxy, Java Dynamic Proxy y SpringFramework: ProxyFactoryBean.

Una de las cosas interesantes que tiene javassist es que no es necesario tener una clase con una interfaz definida, tal como se usa en JDK Dynamic Proxy o con ProxyFactoryBean (aunque también lo tiene usando cglib) para poder crear el Proxy.

Otra de las características de javassist es que posee mecanismos para mitigar la baja de rendimiento al utilizar el patrón Proxy, internamente maneja cache e intervención a nivel de bytecode para optimizar su uso.

Javassist (Java Programming Assistant) makes Java bytecode manipulation simple. It is a class library for editing bytecodes in Java; it enables Java programs to define a new class at runtime and to modify a class file when the JVM loads it. Unlike other similar bytecode editors, Javassist provides two levels of API: source level and bytecode level.

Java Programming Assistant

Javasist

Javassist ProxyFactory

Vamos al ejemplo:

public class CalculadoraProxyFactory {

  /**
   * La misma calculadora de siempre (con interfaz)
   */
  public static Calculadora createCalculadoraInstance()
      throws InstantiationException, IllegalAccessException {
    ProxyFactory pf = new ProxyFactory();
    pf.setSuperclass(CalculadoraImpl.class);
    pf.setFilter(new SumaMethodFilter());
    pf.setUseCache(true);
    Class<?> proxyClass = pf.createClass();

    Object proxyObject = proxyClass.newInstance();
    ((Proxy) proxyObject).setHandler(new CalculadoraMethodHandler());

    return (Calculadora) proxyObject;
  }

  /**
   * Una copia de la implementación de la calculadora pero sin interfaz
   */
  public static CalculadoraWithoutInterface createCalculadoraWithoutInterface()
      throws InstantiationException, IllegalAccessException {
    ProxyFactory pf = new ProxyFactory();
    pf.setSuperclass(CalculadoraWithoutInterface.class);
    pf.setFilter(new SumaMethodFilter());
    pf.setUseCache(true);
    Class<?> proxyClass = pf.createClass();

    Object proxyObject = proxyClass.newInstance();
    ((Proxy) proxyObject).setHandler(new CalculadoraMethodHandler());

    return (CalculadoraWithoutInterface) proxyObject;
  }
}

Como podrán ver, el uso del ProxyFactory de javassist es muy parecido a los otros (ProxyFactoryBean y JDK Dynamic Proxy), además usa un nuevo concepto llamado MethodFilter que permite agregar un filtro para los nombres de los métodos, con la finalidad de que el proxy sólo funciona cuando el método isHandled retorna true, de lo contrario hará una llamada directa a la clase RealSubject.

MethodFilter

Veamos la implementación de MethodFilter para este ejemplo:

final class SumaMethodFilter implements MethodFilter {

  private static final String METHOD_NAME_TO_PROXY = "suma";

  @Override
  public boolean isHandled(Method m) {
    return METHOD_NAME_TO_PROXY.equals(m.getName());
  }
}

MethodHandler

Esta clase es la que contiene la lógica de negocio del proxy, es parecida a los interceptores (MethodInterceptor) de SpringFramework para el uso de ProxyFactoryBean o de InvocationHandler para JDK Dynamic Proxy.

final class CalculadoraMethodHandler implements MethodHandler {

  @Override
  public Object invoke(Object self, Method thisMethod, Method proceed,
      Object[] args) throws Throwable {

    for (Object object : args) {
      if (object instanceof Integer) {
        Integer number = Integer.valueOf(object.toString());

        if (number.intValue() > 0) {
          System.out.println("Parameter [" + number + "]");
        } else {
          System.err.println("Invalid number [" + number + "]");
          throw new RuntimeException("Invalid number [" + number
              + "]");
        }
      } else {
        System.err.println("Invalid type");
        throw new RuntimeException("Invalid type");
      }
    }
    return proceed.invoke(self, args);
  }
}

Test Unitario

Para hacer la prueba, realicé dos test unitarios, uno usando las mismas clases que he usado en los otros ejemplos y una nueva clase calculadora que no tiene una interfaz definida, con la finalidad de probar la funcionalidad de manipulación de bytecode en tiempo de runtime de javassist (crea una intrefaz a partir de la información de la clase).

public class ProxyFactoryJavassistTest {

  private Calculadora _proxy;

  private CalculadoraWithoutInterface _proxyWithoutInterface;

  @Before
  public void before() {
    System.out.println("--------------------");
    try {
      _proxy = CalculadoraProxyFactory.createCalculadoraInstance();
      _proxyWithoutInterface = CalculadoraProxyFactory
          .createCalculadoraWithoutInterface();
      assertNotNull(_proxy);
      assertNotNull(_proxyWithoutInterface);
    } catch (Exception e) {
      fail();
    }
  }

  // Con intrefaz definida

  @Test
  public void proxyFactoryJavassist() throws Exception {
    Integer result = _proxy.suma(1, 2);
    assertNotNull(result);
    assertEquals(Integer.valueOf(3), result);
  }

  @Test(expected = RuntimeException.class)
  public void shouldFailWithProxyFactory() {
    Integer a = -1;
    Integer b = 2;
    _proxy.suma(a, b);
  }

  // Sin intrefaz definida

  @Test
  public void proxyFactoryJavassistWithoutInterface() throws Exception {
    Integer result = _proxyWithoutInterface.suma(1, 2);
    assertNotNull(result);
    assertEquals(Integer.valueOf(3), result);
  }

  @Test(expected = RuntimeException.class)
  public void shouldFailWithoutInterface() {
    Integer a = -1;
    Integer b = 2;
    _proxyWithoutInterface.suma(a, b);
  }
}

Y finalmente la salida a la consola del test unitario es la siguiente:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running patterns.proxyjavassist.ProxyFactoryJavassistTest
--------------------
Invalid number [-1]
--------------------
Parameter [1]
Parameter [2]
--------------------
Invalid number [-1]
--------------------
Parameter [1]
Parameter [2]
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.093 sec

En otro post, trataré de profundizar en la librería javassist ya que desde hace mucho la vengo mirando como dependencias de varios frameworks y aplicaciones (Hibernate, jboss, Spring Framework) y me llama la atención el cómo poder generar bytecode utilizando una API. La otra librería que hace algo similar es cglib y que Hibernate usó en los inicios del proyecto, aunque al parecer ya va en retroceso (también le echaré un vistazo).

Nota: Si ven en los debug de Hibernate algo como NombreDeClase_$$_javassist_0 son los proxys que arma Hibernate con javassist para las entidades que se cargan en modo lazy.

Comentar