Patrón Proxy, como lo hace Spring framework
En el artículo anterior hablábamos del Patrón Proxy y vimos su implementación en Java, en esta entrada abordaremos la implementación de ese patrón pero desde como lo maneja el framework Spring. Este es uno de los patrones más usados en Spring y probablemente menos conocido por los desarrolladores que emplean dicho marco de trabajo. Palabras como “cglib” y “dynamic proxy” son mostradas a menudo en el stack de mensajes de una excepción cuando desarrollamos con Spring y algo sale mal ¿Pero que significan estas palabras?
Para contextualizar, recordemos primero que el Patrón Proxy en realidad hace referencia al concepto de Proxied object, o objeto proxy y proxiado.
“Un objeto proxy podemos definirlo como un objeto que agrega lógica adicional por encima del objeto que está siendo «proxiado» sin necesidad de modificar el código del objeto «proxiado». Un objeto proxy tiene los mismos métodos públicos que el objeto «proxiado» y debería ser lo más posible indistinguible. Cuando un método es invocado en el objeto proxy, el código adicional es usualmente ejecutado antes y/o después de ser ejecutado el método en el objeto «proxiado».”
Tipos de Proxy en Spring framework
Spring tiene la capacidad de crear dos tipos de objetos proxies en dependencia del origen de los mismos. La tarea de instanciar objetos es computacionalmente costosa, por ello muchos frameworks implementan el patrón de inyección de dependencias. Spring por su parte en el proceso de arranque iniciará todos los objetos de las clases marcadas para instanciar (@Component, @Service, @Repsitory, etc) y pondrá estos objetos a disposición de la aplicación para ser usados, inyectándolos de acuerdo a lo programado. Por defecto los objetos creados se crean como Singleton.
En el proceso de crear los objetos, Spring definirá que tipo de proxy usar de los 2 disponibles.
- JDK Dynamic Proxy: Es el proxy por defecto, se empleará si el objeto objetivo a instanciar implementa una interfaz.
- CGLib Proxy: Este es el proxy que será usado cuando el objeto a crear no implementa ninguna interfaz.
Cada uno de estos proxies posee algunas limitaciones que veremos a continuación
Limitaciones de JDK Dynamic Proxy
- Requiere que la clase que define los objetos implemente al menos una interfaz.
- Solo los métodos definidos en el contrato de la interfaz pueden ser proxiados.
- No esta permitida la auto-invocación de un método a otro desde dentro del mismo objeto.
Limitaciones de CGLib Proxy
- No funciona en clases finales (final class)
- No funciona o aplica en métodos finales.
- No esta permitida la auto-invocación de un método a otro desde dentro del mismo objeto.
Ejemplo con JDK Dynamic Proxy
Aunque Spring realiza el proxiado de forma dinámica en el proceso de instanciación de los Beans, también podremos usar el mecanismo que emplea Spring para hacerlo manualmente y poder comprenderlo mejor.
JDK Dynamic Proxy se implementa haciendo uso del API Reflection de Java, es independiente a Spring o a bibliotecas de terceros, específicamente es definido por la interfaz InvocationHandler, disponible en:
java.lang.reflect.InvocationHandler
Para usar este Proxy en nuestras aplicaciones solo debemos implementar en la clase la interfaz InvocationHandler. Veamos el siguiente ejemplo para el cual hemos usado varias clases que explicaremos.
Primero una clase POJO que define una entidad Product de ejemplo.
public class Product { private Long id; private String nombre; public Product(Long id, String nombre) { super(); this.id = id; this.nombre = nombre; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getNombre() { return nombre; } public void setNombre(String nombre) { this.nombre = nombre; } @Override public String toString() { return "Product [id=" + id + ", nombre=" + nombre + "]"; } }
En segunda instancia como en la entrada anterior, usaremos una simulación de DAO simplificada a modo de ejemplo.
public interface ProductDao { public Product findById(Long id); public Product save(Product product); }
public class ProductDaoImpl implements ProductDao { public Product findById(Long id) { System.out.println("Finding product with id = " + id); return new Product(id, "Sample"); } public Product save(Product product) { System.out.println("Saving product ...." ); return product; } }
Luego de tener nuestra interfaz que define el contrato del DAO y su implementación respectiva pasaremos entonces a implementar la clase que definirá los Proxy Object para la implementación del DAO usando JDK Dynamic Proxy, como comentamos al inicio nuestra clase debe implementar InvocationHandler, específicamente el método invoke(Object proxy, Method method, Object[] args), este método recibe 3 parámetros fundamentales:
- Object proxy: Que sería la referencia al objeto que va a hacer proxiado.
- Method method: La referencia al método en particular que se invoca del objeto proxiado (Es genérico, una referencia a método usando el objeto Method del API Reflection de Java).
- Object[] args: Los parámetros de la firma del método anterior.
public class ProductDaoInvocationHandler implements InvocationHandler { private final ProductDao target; public ProductDaoInvocationHandler(ProductDao target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Before execute method = " + method.getName()); Object res = method.invoke(target, args); System.out.println("After execute method ...."); return res; } }
Si nos fijamos, esta implementación de proxy a diferencia de la realizada en el ejemplo del artículo anterior es genérica y nos permite que podamos realizar cambios en el objeto proxiado respecto a agregar métodos nuevos o cambiar la firma de los existentes sin necesidad de cambiar esta implementación de proxy. Este proxy es totalmente genérico e independiente de la definición del objeto que proxea. Podemos ponernos un poco más creativos y usar tipo genérico “T” si queremos.
Ahora corresponde invocar para usar este proxy que hemos implementado.
ProductDao productDao = (ProductDao) Proxy.newProxyInstance(ProductDao.class.getClassLoader(), ProductDaoImpl.class.getInterfaces(), new ProductDaoInvocationHandler(new ProductDaoImpl()) ); Product p1 = productDao.findById(5L); Product p2 = productDao.save(p1);
La forma de crear el objeto es usando Proxy.newProxyInstance(…) del paquete java.lang.reflect.Proxy. Debemos proveer la clase objetivo que vamos proxear, las interfaces que implementa esa clase (recordar que JDK Dynamic Proxy requiere que la clase objetivo implemente al menos una interfaz) y la instancia en particular de la clase objetivo indicada. Con estos tres parámtros creamos el proxy.
Al ejecutar el código anterior, se muestra la siguiente salida de ejecución:
Before execute method = findById Finding product with id = 5 After execute method .... Before execute method = save Saving product .... After execute method ....
Como se muestra en el output siempre se ejecuta el método dentro de la implementación del proxy. Todo este ejemplo se encuentra en el proyecto de GitHub bajo el paquete de JDK Proxy.
Ejemplo con Spring CGLib Proxy
La implementación de proxy con Spring CGLib como hemos hablado se realiza sobre objetos cuya clase de origen no implementa interfaces, el uso de este proxy requiere de una librería de terceros que ya forma parte de Spring framework.
Para entender un poco mejor CGLib significa “Code Generation Library”, Spring CGLib es una implementación “wrapper” de CGLib pero que ya está incluida como parte del framework, en ambos casos se basan en la clase principal Enhancer, que veremos durante el ejemplo a continuación. Similar al punto anterior esta biblioteca Spring la usa de forma transparente pero también podemos diseñar un Proxy de forma manual si quisiéramos algo particular.
Para los más duchos en la materia de los proxies, en muchos casos esto lo podemos resolver usando Programación orientada a aspectos con Spring AOP, que al final, por detrás hace proxies. Puedes ver esta entrada donde hablamos de Spring AOP.
Para ejemplificar vamos a crear una clase Sale, una SaleDAO (con un mock básico) y una SaleDaoInterceptor donde se encuentra nuestro Proxy CGLib. Veamos las definiciones de clases.
public class Sale { private float price; private String reason; public Sale() { super(); // TODO Auto-generated constructor stub } public Sale(float price, String reason) { super(); this.price = price; this.reason = reason; } public float getPrice() { return price; } public void setPrice(float price) { this.price = price; } public String getReason() { return reason; } public void setReason(String reason) { this.reason = reason; } }
public class SaleDao { public Sale findById(Long id) { System.out.println("Finding sale with id = " + id); return new Sale(id, "Good sale"); } public Sale save(Sale sale) { System.out.println("Saving sale ...." ); return sale; } }
public class SaleDaoInterceptor implements MethodInterceptor { @Override public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable { System.out.println("Before execute method = " + arg3.getSuperName()); Object res = arg3.invokeSuper(arg0, arg2); System.out.println("After execute method ...."); return res; } }
La cosa interesante, sin dudas está en esta última clase, como puede verse la misma implementa la interfaz MethodInterceptor, del paquete org.springframework.cglib.proxy.MethodInterceptor, y nos obliga a sobreescribir el método intercept, que recibe a su vez 4 parámetros.
- Object arg0: Una referencia al objeto proxiado.
- Method arg1: El método interceptado.
- Object[] arg2: La lista de parámtros del método a invocar.
- MethodProxy arg3: Una referencia al método proxiado pero en la clase superior (parent). Útil para casos de herencia.
Para crear el objeto proxiado lo haremos usando la clase Enhancer, que es la clase más importante de CGLib.
Enhancer enhancer = new Enhancer(); enhancer.setCallback(new SaleDaoInterceptor()); enhancer.setSuperclass(SaleDao.class); SaleDao saleDao = (SaleDao) enhancer.create(); Sale s1 = saleDao.findById(7L); Sale s2 = saleDao.save(s1);
Cuando ejecutamos el código anterior, la salida es la siguiente:
Before execute method = CGLIB$findById$1 Finding sale with id = 7 After execute method .... Before execute method = CGLIB$save$0 Saving sale .... After execute method ....
Como se ilustra, claramente el proxy actuó y cuando imprimimos el mensaje adicionalmente se muestra que está usando el Proxy de Spring CGLib. Puedes ver este ejemplo completo en este paquete del código publicado en GitHub.
JDK Dynamic Proxy & CGLib en Spring, en la vida real
Veamos ahora, en la vida real la magia de Spring para ocultarnos todo esto que hemos hecho a mano, para ello vamos a hacer un ejemplo que consta de algunas clases e interfaces para probar con cada Proxy y poner en práctica las limitantes.
Una simple clase de la que luego crearemos, manualmente un bean con @Bean.
public class SpringBean { }
Una interfaz, solo para marcar a una clase como que implementa una interfaz. Si quieres saber más sobre interfaces lee acá.
public interface InterfaceBean { }
Una clase ahora que implementa dicha interfaz.
public class InterfaceBeanImpl implements InterfaceBean { }
Implementemos ahora, un componente Spring cuya funcionalidad es crear un Bean y ponerlo en el BeanContainer de Spring en el momento de inicialización de nuestra aplicación.
@ComponentScan @Configuration public class SpringComponent { @Bean @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) SpringBean springBean() { return new SpringBean(); } @Bean @Scope(proxyMode = ScopedProxyMode.INTERFACES) InterfaceBean interfaceBean() { return new InterfaceBeanImpl(); } }
Pudieramos poner solo @Bean encima de los métodos y funcionaría para crear el Bean, pero quise agregar la anotación de Scope que entre otras opciones nos permite definir a la hora de crear un Bean decirle que manejador de Proxy usar ¡ Esto es genial !
Hemos agregado, además una clase más, SpringService que tiene la única finalidad de cargar nuestros Beans junto al ApplicationContext de Spring y mostrarnos que Proxy finalmente esta usando cada objeto creado por Spring.
@ComponentScan @Configuration public class SpringService { @Autowired private SpringBean springBean; @Autowired private InterfaceBean interfaceBean; @Autowired private ApplicationContext context; @PostConstruct public void init() { System.out.println(context.getBean("springBean").getClass()); System.out.println(context.getBean("interfaceBean").getClass()); } }
Como se puede observar arriba hemos puesto un println en el @PostConstruct para que muestre luego de que se crean los Beans la información que andamos buscando sobre el proxy usado. La salida que muestra Spring en el proceso de arranque es:
2020-12-16 23:54:07.405 INFO 2277 --- [ main] c.s.spring.proxy.SpringProxyApplication : Starting SpringProxyApplication using Java 15 on scv with PID 2277 (/home/yoandypv/Documents/workspace-spring-tool-suite-4-4.8.1.RELEASE/spring-proxy/target/classes started by yoandypv in /home/yoandypv/Documents/workspace-spring-tool-suite-4-4.8.1.RELEASE/spring-proxy) 2020-12-16 23:54:07.408 INFO 2277 --- [ main] c.s.spring.proxy.SpringProxyApplication : No active profile set, falling back to default profiles: default class com.sacavix.spring.proxy.spring.SpringBean$$EnhancerBySpringCGLIB$$3537ebf9 // VER 1 class com.sun.proxy.$Proxy31 // VER 2 2020-12-16 23:54:08.078 INFO 2277 --- [ main] c.s.spring.proxy.SpringProxyApplication : Started SpringProxyApplication in 1.113 seconds (JVM running for 2.053)
¿Qué ha pasado?
Fijarse dentro de la salida en los comentarios que hemos introducido (Ver 1 y Ver 2).
El valor de Ver 1 es com.sacavix.spring.proxy.spring.SpringBean$$EnhancerBySpringCGLIB$$3537ebf9, como se muestra está usando Spring CGLib y tiene sentido, porque además de que se lo dijimos al crear el bean, que usara como proxyMode = ScopedProxyMode.TARGET_CLASS la clase SpringBean no implementa interfaces, así que tiene que ir por CGLib porque es la única forma que hay.
En Ver 2, pasa algo similar, muestra: class com.sun.proxy.$Proxy31, el valor $Proxy nos indica que esta usando JDK Dynamic Proxy, y le hemos especificado que emplee proxyMode = ScopedProxyMode.INTERFACES, por lo que todo funciona de acuerdo a lo que vimos en la teoría.
Probemos ahora algo diferente voy a cambiar la definición de los beans en diferentes escenarios. Indicando diferentes tipos de Proxies.
Escenario 1: Ambos usando ScopedProxyMode.TARGET_CLASS sin cambiar el código original, solo la anotación de Scope.
@ComponentScan @Configuration public class SpringComponent { @Bean @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) SpringBean springBean() { return new SpringBean(); } @Bean @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) InterfaceBean interfaceBean() { return new InterfaceBeanImpl(); } }
Salida:
class com.sacavix.spring.proxy.spring.SpringBean$$EnhancerBySpringCGLIB$$9a72ff28 class com.sun.proxy.$Proxy31
Le hemos dicho que usar TARGET_CLASS … ¿Por qué sigue usando JDK Dynamic Proxy ?. La respuesta se sencilla, en el método el objeto resultado sigue siendo una referencia a la interfaz InterfaceBean, por lo que Spring sigue usando por defecto el Proxy para los objetos de clases que implementan una interfaz, debemos entonces, al crear el bean indicarle que es del tipo de clase objetivo, veamos el siguiente escenario.
Escenario 2: Ambos usando ScopedProxyMode.TARGET_CLASS cambiando el tipo de la interfaz por la clase objetivo.
@ComponentScan @Configuration public class SpringComponent { @Bean @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) SpringBean springBean() { return new SpringBean(); } @Bean @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) InterfaceBeanImpl interfaceBean() { return new InterfaceBeanImpl(); } }
Salida:
class com.sacavix.spring.proxy.spring.SpringBean$$EnhancerBySpringCGLIB$$aacf681c class com.sacavix.spring.proxy.spring.InterfaceBeanImpl$$EnhancerBySpringCGLIB$$f8c15a04
Ahora si tenemos el resultado deseado, aunque el objeto a Proxiar, implementa una interfaz, el Bean al crearse se origina con el tipo de la clase objetivo, entonces puede usar CGLib.
Escenario 3: En este escenario tratemos de obligar a la clase SpringBean que no implementa interfaz a usar JDK Dynamic Proxy.
@ComponentScan @Configuration public class SpringComponent { @Bean @Scope(proxyMode = ScopedProxyMode.INTERFACES) SpringBean springBean() { return new SpringBean(); } @Bean @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) InterfaceBeanImpl interfaceBean() { return new InterfaceBeanImpl(); } }
Salida:
Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'springService': Unsatisfied dependency expressed through field 'springBean'; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'springBean' is expected to be of type 'com.sacavix.spring.proxy.spring.SpringBean' but was actually of type 'com.sun.proxy.$Proxy31' 2020-12-17 00:10:17.616 INFO 3535 --- [ main] ConditionEvaluationReportLoggingListener : Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2020-12-17 00:10:17.640 ERROR 3535 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPLICATION FAILED TO START *************************** Description: The bean 'springBean' could not be injected as a 'com.sacavix.spring.proxy.spring.SpringBean' because it is a JDK dynamic proxy that implements: Action: Consider injecting the bean as one of its interfaces or forcing the use of CGLib-based proxies by setting proxyTargetClass=true on @EnableAsync and/or @EnableCaching.
Como se puede observar, obviamente no es posible y hasta el propio Spring nos recomienda que forcemos a usar Spring CGLib.
Bueno, esto ha sido todo, con este artículo terminamos la serie sobre el Patrón Proxy, resumiendo lo que hemos visto en los siguientes puntos:
- ¿Qué es un Proxy?
- ¿Qué es el patrón Proxy?
- ¿Qués un objeto Proxy?
- Ejemplo de implementación del pratrón proxy en Java (manual), sin utilitarios.
- Tipos de Proxy en Spring.
- Proxy JDK Dynamic.
- Spring CGLib Proxy.
- Ejemplos de creación de Proxies en Spring.
- Casos de proxies con Spring usando la anotación Scope.
Si llegaste a este punto, entonces puedes ver el código completo acá.
Éxitos !
2 thoughts on “Patrón Proxy, como lo hace Spring framework”
Comments are closed.