Javassist ProxyFactory

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.

Javassist ProxyFactory

Vamos al ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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:

1
2
3
4
5
6
7
8
9
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-------------------------------------------------------
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.

¿Por que FactoryBean es útil?

En este post voy a tratar de explicar porque FactoryBean es increíblemente útil para alambrar (aplicar DI) aplicaciones hechas con Spring Framework.

La interfaz de FactoryBean dice lo siguiente:

1
2
3
4
5
6
7
8
public interface FactoryBean<T> {

T getObject() throws Exception;

Class getObjectType();

boolean isSingleton();
}

La utilidad de la implementación de esta interfaz y su posterior declaración como Bean dentro del contexto de la aplicación, es que puede ser usada (como su nombre lo indica) como Factory para inyectar algún valor en alguna propiedad de otro Bean.

¿Dónde y cómo funciona? Dentro del ciclo de vida de la carga del Contexto de spring, la ejecución de la implementación de esta interfaz, esta justo antes de hacer los setters de las propiedades de un Bean. El funcionamiento es simple, siempre se ejecutará el método getObject() que retornará la instancia del tipo T.

Veamos una implementación simple y una configuración de contexto de spring:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RandomNumberFactoryBean implements FactoryBean<integer> {

public Integer getObject() throws Exception {
return Integer.valueOf(new Random().nextInt());
}

public Class getObjectType() {
return Integer.class;
}

public boolean isSingleton() {
return true;
}

}

Esta simple implementación de FactoryBean retorna un Integer en el método getObject(), simplemente eso (obviamente en este método debes poner todo tu talento para resolver el valor que andas buscando, esto es un ejemplo simple).

El método getObjectType() es usado internamente por el framework para validar que el retorno de getObject() sea correcto o que este dentro de la jerarquía de clases, esto es en caso de que quieras restringir por tipo.

El último método isSingleton() es si quieres que esta clase (no importa la cantidad de declaraciones que tengas, siempre será la misma), es un tanto dificil de entender pero les sugiero que lean sobre el concepto de Singleton que tiene SpringFramework. En el ejemplo de configuración espero que les quede claro, de lo contrario, bienvenidas las preguntas.

Esta es la definición del Bean de ejemplo, en donde se hará uso del FactoryBean:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ExampleBean {

private Integer _randomValue;

public final Integer getRandomValue() {
return _randomValue;
}

public final void setRandomValue(Integer randomValue) {
_randomValue = randomValue;
}

}

Aquí las configuraciones de contexto de spring.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemalocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="exampleBean1" class="cl.pcollaog.factory.ExampleBean">
<property name="randomValue">
<bean class="cl.pcollaog.factory.RandomNumberFactoryBean" />
</property>
</bean>

<bean id="randomNumberFactoryBean" class="cl.pcollaog.factory.RandomNumberFactoryBean"/>

<bean id="exampleBean21" class="cl.pcollaog.factory.ExampleBean">
<property ref="randomNumberFactoryBean" name="randomValue"></property>
</bean>

<bean id="exampleBean22" class="cl.pcollaog.factory.ExampleBean">
<property ref="randomNumberFactoryBean" name="randomValue"></property>
</bean>

</beans>

Ejemplo 1:

Se declara el Bean exampleBean1 y que tiene como propiedad el atributo randomValue y cuyo valor será producto de la ejecución del Bean RandomNumberFactoryBean en el momento que el contexto de Spring es cargado. En este caso se utiliza la técnica de InnerBean, es decir, no se tiene una instancia del Bean para ser reutilizada sino que esta en la misma declaración del setter del atributo.

Ejemplo 2:

Este ejemplo hace lo mismo indicado arriba, sólo que no se aplica InnerBean sino que se extrae y es reutilizado en el bean exampleBean21 y exampleBean22. En este ejemplo entra en juego el método isSingleton(), si es true, quiere decir que el valor que entregará el FactoryBean siempre será el mismo para ambos Beans. Ahora bien, si se cambia a false, por cada setter, es decir, para exampleBean21 y exampleBean22 se realizara la ejecución de RandomNumberFactoryBean 2 veces. (En este caso, nos entregará dos valores aleatorios para cada bean)

En pocas palabras para cada Bean del ejemplo 2 habrá dos instancias de RandomNumberFactoryBean que entregará cada una un Integer para los atributos de los beans declarados.

Algo interesante de las implementaciones de FactoryBean es que luego de la carga del contexto, estas instancias son desechadas.

Espero les sea útil y cualquier pregunta sera bienvenida.