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.

SpringFramework: ProxyFactoryBean

Una manera sencilla de crear Proxy es usando SpringFramework, siempre y cuando tengas configurado tu aplicación con Spring, de otra forma, puedes buscar otras librerías que puedan ayudarte con el patrón Proxy (dinámico), (cglib y javassist) o puedes utilizar la API de java para crearlos. Revisa aquí Java Dynamic Proxy o Patrones De Diseño: Proxy.

Por ahora voy a mostrar como configurar un Proxy Dinámico con SprinFramework, configuraciones y par un de conceptos.

Conceptos

  • Target: Es la clase que será envuelta por el proxy (RealSubject)
  • Interceptor(es): Son las clases accesorias donde deberás agregar la lógica de negocio asociada al Proxy. Estos interceptores pueden ser mas de uno para un mismo target.
  • ProxyFactoryBean: Es la clase de Spring Framework para crear proxys dinámicos, al estilo Spring (con un FactoryBean). Deja un bean configurado dentro del contexto, listo para ser usado. Lo bueno de esta estrategia es que la creación del Proxy se realiza sólo una vez y cuando levanta el contexto, al contrario del Proxy que provee la API de Java, por cada invocación se debe crear el Proxy (aunque es rápido pero todo suma).

Interceptores

Los interceptores son clases que contienen la lógica de negocio del Proxy, es el símil a el InvocationHandler de la API de Java. Estas clases deben ser implementadas desde la interfaz Interceptor que es la de mas alta jerarquía o de su herencia, MethodInterceptor o ConstructorInterceptor. Estas últimas clases pertenecen a la librería de aopalliance versión 1.0 en el package org.aopalliance.intercept.

Para poder agregar las 2 funcionalidades (loggear los parámetros de entrada y validar que no sean negativos), implementaremos 2 Interceptores con la finalidad de aislar las lógicas de negocio de cada uno.

Implementación de Validador de Argumentos

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
public class ValidateArgumentsInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
for (Object arg : invocation.getArguments()) {
if (arg instanceof Integer) {
Integer number = Integer.class.cast(arg);
if (number.intValue() < 0) {
throw new InvalidNumberValueException("Invalid number ["
+ number + "]");
}
} else {
throw new InvalidNumberValueException("Invalid type");
}
}
return invocation.proceed();
}

public static class InvalidNumberValueException extends RuntimeException {

private static final long serialVersionUID = 3067612583360286918L;

/**
* @param message
*/
public InvalidNumberValueException(String message) {
super(message);
}

}
}

Implementación de Logger de Argumentos

1
2
3
4
5
6
7
8
9
10
11
public class LoggerArgumentsInterceptor implements MethodInterceptor {

@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
for (Object arg : invocation.getArguments()) {
System.out.println("Argument " + arg.toString());
}
return invocation.proceed();
}

}

Ahora que tenemos los Interceptores, juntamos todo en la configuración del contexto de Spring.

Configurar contexto Spring

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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- RealSubject o target -->
<bean id="calculadora" class="patterns.dynamicproxy.CalculadoraImpl" />

<!-- Proxy -->
<bean id="calculadoraWithProxy"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="calculadora" />
<property name="interceptorNames">
<list>
<value>validate</value>
<value>logger</value>
</list>
</property>
</bean>

<!-- Interceptores -->
<bean id="validate" class="patterns.proxyAOP.ValidateArgumentsInterceptor" />
<bean id="logger" class="patterns.proxyAOP.LoggerArgumentsInterceptor" />
</beans>

Donde:

  • calculadora: Es la clase a envolver en el Proxy (RealSubject o target).
  • calculadoraWithProxy: Proxy Factory que crea el bean configurado como Proxy. En este bean deben fijarse en las propiedades configuradas:
    • target: Es una referencia al bean con id calculadora.
    • interceptorNames: Es una lista con los id de los beans que harán de interceptores. En este caso son dos los interceptores validate y logger.

Notas

  • Como podrán ver, usé las mismas clases del post Java Dynamic Proxy y sólo agregue los Interceptores donde se concentra la lógica de negocio solicitada.
  • Recuerden que como se trata de un Proxy Dinámico se necesita que CalculadoraImpl implemente una interfaz conocida, en este caso, Calculadora.
  • La clase ProxyFactoryBean posee varios atributos mas que permiten agregarle funcionalidad y configuraciones al proxy, échenle un vistazo.

Test Unitario

Para poder probar las funcionalidades realicé el siguiente test unitario:

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
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "/proxy-application-context.xml" })
public class CalculadoraServiceAOPTest {

@Autowired
@Qualifier("calculadoraWithProxy")
private Calculadora _proxy;

@Before
public void before() {
System.out.println("----------------------------------");
assertNotNull("Calculadora cannot be null", _proxy);
}

@Test
public void calculatorBeanWithProxyFactory() {
Integer a = 1;
Integer b = 2;

Integer result = _proxy.suma(a, b);
assertNotNull(result);
assertEquals(Integer.valueOf(3), result);
}

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

}

Y finalmente la salida de la consola:

1
2
3
4
5
6
7
8
9
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running patterns.proxyAOP.CalculadoraServiceAOPTest
----------------------------------
Argument 1
Argument 2
----------------------------------
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.592 sec

Espero les sirva, comentarios bienvenidos sean.

Java Dynamic Proxy

Los proxy dinámicos fueron introducidos en la API de Java 1.3 agregando múltiples funcionalidades a la plataforma Java. En este post pretendo enseñarles algunos usos de los proxy dinámicos y cómo implementar uno sencillo.

Primero tienen que conocer la definición de proxy y sus posibles usos, que ya fueron descritas en un post anterior, puedes revisarlo antes de continuar: Patrones de Diseño: Proxy

Primero debemos conocer las clases que hacen posible la implementación de Dynamic Proxy en la API de Java, estas son java.lang.reflect.Proxy y java.lang.reflect.InvocationHandler, además debes tener conceptos básicos de Reflexión (sólo para la invocación de métodos).

java.lang.reflect.Proxy

Para crear un objeto Proxy debes utilizar la clase java.lang.reflect.Proxy de la siguiente forma:

1
Object obj = Proxy.newProxyInstance(classLoader, interfaces, handler);

Donde:

  • classLoader: classloader que contiene la definición de las interfaces que componen el objeto target (o RealSubject).
  • interfaces: Arreglo con la definición de las interfaces que implementa el objeto target (o RealSubject).
  • handler: Esta es la clase que nos permitirá implementar el Proxy, es la clase que contiene la lógica de negocio del Proxy.

java.lang.reflect.InvocationHandler

Esta interfaz debe ser implementada y tenemos el método invoke que hará las veces de interceptor, es aquí donde debemos escribir nuestra lógica de negocio.

1
2
3
4
5
public interface InvocationHandler {

public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

Reglas para crear un Dynamic Proxy

  • El objeto a “proxiar” debe tener una interfaz, no una clase concreta o abstracta, una Interfaz.
  • El arreglo de Interfaces no debe contener interfaces duplicadas.
  • Todas las interfaces deben ser visibles en el classloader que se esta utilizando (usado en la construcción del Proxy).
  • Las interfaces del proxy no deben tener conflictos de métodos. Ver la especificación de Lenguaje Java sobre sobre-escritura de métodos.

Ejemplo Práctico

Veamos un ejemplo práctico de la implementación de un proxy dinámico usando la API de Java:

1
2
3
4
5
6
7
8
9
10
public interface Calculadora {

/**
* @param a
* @param b
* @return suma de a+b
*/
Integer suma(Integer a, Integer b);

}

Ahora veamos la implementación de Calculadora:

1
2
3
4
5
6
7
8
public class CalculadoraImpl implements Calculadora {

@Override
public Integer suma(Integer a, Integer b) {
return a + b;
}

}

Supongamos que estas clases ya están creadas y necesitamos agregar un par de funcionalidades sin tener que tocar el código ya existente. La primera funcionalidad es validar que los números a y b sean mayores a 0 y la segunda funcionalidad es loggear los parámetros de entrada.

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
public class CalculadoraProxyFactory implements InvocationHandler {

private final Calculadora _target;

/**
* @param target
*/
public CalculadoraProxyFactory(Calculadora target) {
_target = target;
}

@Override
public Object invoke(Object proxy, Method method, 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");
}
}
System.out.println("Execute method [" + method.getName() + "]");
return method.invoke(_target, args);
}

/**
* Factory para crear proxy de Calculadora
*
* @param target
* @return proxy de Calculadora
*/
public static Calculadora proxyFactory(Calculadora target) {
ClassLoader classLoader = target.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
return (Calculadora) Proxy.newProxyInstance(classLoader, interfaces,
new CalculadoraProxyFactory(target));
}
}

Como podrán ver en el ejemplo, en el método invoke se hace el control y el log sobre los argumentos que entran al método suma del RealSubject. Además para simplificar la creación del Proxy, se implementó un method-factory que crea el proxy dinámico (método proxyFactory).

Ahora el test unitario que prueba el funcionamiento del 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
26
27
28
public class CalculadoraProxyFactoryTest {

private Calculadora _calculadoraProxy;

@Before
public void before() {
System.out.println("------------------------------");
_calculadoraProxy = CalculadoraProxyFactory
.proxyFactory(new CalculadoraImpl());
}

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

@Test(expected = RuntimeException.class)
public void failWrongNumber() throws Exception {
_calculadoraProxy.suma(-1, 2);
}

@Test(expected = RuntimeException.class)
public void failNullNumber() throws Exception {
_calculadoraProxy.suma(null, 2);
}
}

La salida de este test a la consola con maven es la siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running patterns.dynamicproxy.CalculadoraProxyFactoryTest
------------------------------
Invalid number [-1]
------------------------------
Invalid type
------------------------------
Parameter [1]
Parameter [2]
Execute method [suma]

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.045 sec

Espero que les haya sido útil, en un próximo post escribiré sobre otras formas de crear Proxy. Deje su comentario.

Patrones de Diseño: Proxy

El patrón Proxy

El patrón Proxy esta clasificado dentro de los Patrones Estructurales, es también conocido como, Embajador, Apoderado (surrogante). Veamos primero su definición:

Un proxy fuerza a que un método de un objeto RealSubject sea indirectamente ejecutado a través de un objeto Proxy, el cual actúa como delegado o sustituto del objeto RealSubject. Los objetos Proxy son usualmente declarados sin que el Cliente sepa de que se trata de un Objeto Proxy.

Les dejo un diagrama de clases de cómo se implementa este patrón.

Usos del patrón Proxy

  • Control de Acceso: El patrón puede ser usado para controlar el acceso a los métodos del RealSubject. La lógica del control de acceso se delegará en el proxy, de esta forma quedará mas limpio el RealSubject (en términos de código).
  • Acceso a objetos remotos: Representación de un objeto remoto de forma local, es decir, el Proxy resuelve y enmascara la forma en cómo conectarse al objeto remoto. Ejemplos de implementación de este patrón los pueden encontrar en EJB y RMI.
  • Proxy Virtual: Crea objetos costosos bajo demanda. Por ejemplo, al iniciar una aplicación se pueden instanciar con este patrón todos los iconos de dicha aplicación, pero sólo cuando éste sea solicitado realmente ira a buscarlo al disco y presentado al usuario.
  • Proxy de Referencia Inteligente: Sustituto de una referencia que hace operaciones adicionales cuando se accede a un objeto, como por ejemplo, controlar concurrencia, contar numero de instancias, cargar un objeto en memoria, manejo de cache)

Implementación de Ejemplo

Partamos con la interfaz que deben cumplir tanto RealSubject como el SubjectProxy

1
2
3
4
5
public interface Subject {

void doOperation(String username);

}

Veamos la implementación del RealSubject

1
2
3
4
5
6
7
8
public class SubjectReal implements Subject {

@Override
public void doOperation(String username) {
System.out.println("doOperation Real");
}

}

Finalmente la implementación del SubjectProxy

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
public class SubjectProxy implements Subject {

private final Subject _subjectReal;

private boolean _connected = false;

public SubjectProxy() {
_subjectReal = new SubjectReal();
}

@Override
public void doOperation(String username) {
// Control de Acceso simple
if (!username.isEmpty() && "admin".equals(username)) {
System.out.println("doOperacion proxied");
if (_connected) {
_subjectReal.doOperation(username);
} else {
connectToRemote();
_subjectReal.doOperation(username);
}
} else {
System.out.println("Access Denied");
}
}

private void connectToRemote() {
System.out.println("Connecting to remote");
_connected = true;
}
}

Nuestro cliente (quien consumirá a Subject) será un test unitario:

1
2
3
4
5
6
7
8
public class SubjectTest {

@Test
public void testSubjectSimpleUser() {
Subject subjectProxied = new SubjectProxy();
subjectProxied.doOperation("user");
}
}

Esto imprimirá lo siguiente en la consola (systemOut)

1
Access Denied

La explicación al Access Denied esta dada porque el patrón Proxy se utilizó como control de acceso al método doOperation. Sólo si el username es admin se ejecutará dicho método.

Veamos un Segundo Test Unitario

1
2
3
4
5
6
7
8
9
public class SubjectTest {

@Test
public void testSubjectAdmin() {
Subject subjectProxied = new SubjectProxy();
subjectProxied.doOperation("admin");
subjectProxied.doOperation("admin");
}
}

Esta es la salida:

1
2
3
4
5
doOperacion proxied
Connecting to remote
doOperation Real
doOperacion proxied
doOperation Real

En este caso, el usuario admin puede ejecutar la operación y la primera vez que lo haga se conectará un objeto remoto o difícil de crear (simulado para el ejemplo). Luego una vez conectado, podrá ejecutar la operación.

Nuevamente el usuario admin ejecutará la operación por segunda vez y el SubjectProxy ya posee una conexión al objeto remoto, aquí simplemente ejecutará la operación.

Espero les haya gustado y les sirva en sus proyectos, en el siguiente post les comentaré sobre Dynamic Proxy de Java.

Referencias:

Nota: Busquen el error en wikipedia español si lo encuentra me lo comenta ;)