SpringFramework: ProxyFactoryBean

3 minuto de lectura

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

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

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

<?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:

@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:

-------------------------------------------------------
 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.

Comentar