Comprendiendo algunos conceptos avanzados de Spring.
Este artículo pretende explicar de una manera comprensible los siguientes conceptos avanzados de Spring.
- BeanPostProcessors
- BeanFactoryPostprocessors.
- Introductions.
- AOP con anotaciones.
El enfoque del artículo será la realización de un pequeño ejemplo que ponga de manifiesto los conceptos anteriores y su utilidad.
El artículo supone cierta familiarización con los conceptos principales del Framework Spring en su versión 2.5.
La idea del siguiente ejemplo es permitir que un Bean tenga una referencia a su ApplicationContext, sin implementar (ni en desarrollo ni en compilación) ninguna interfaz dependiente de Spring. Luego este ApplicationContext será accedido desde un Aspecto que imprimirá su “displayName”. Una funcionalidad que en principio no parece que aporte mucho, pero nos sirve para ilustrar los conceptos.
1.- Extendiendo la Interfaz ApplicationContextAware.
La interfaz ApplicationContextAware es una interfaz auto detectada por Spring. Cuando un Bean definido en un ApplicationContext implementa esta interfaz, Spring, al cargar el contexto, inyectara el ApplicationContext al bean a través del método.
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
Para nuestro ejemplo, necesitamos obtener el ApplicationContext asociado a un bean, pero no desde el mismo bean, sino desde un Aspecto. Por tanto extenderemos la interfaz ApplicationContextAware para incluir un getter.
public interface ApplicationContextAwareWithGetter extends ApplicationContextAware{
ApplicationContext getApplicationContext();
void setApplicationContext(ApplicationContext beanFactory);
}
Además, como dijimos antes, los beans de servicio no deben tener dependencia directa a Spring, por lo que dichos beans no implementaran ApplicationContextAware. Entonces necesitamos definir una implementación por defecto para ApplicationContextAware, que luego en tiempo de ejecución será añadida a los beans.
public class ApplicationCntextAwareDefaultImpl implements ApplicationContextAwareWithGetter{
public ApplicationContext context;
public ApplicationContext getApplicationContext() {
return context;
}
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context=context;
}
}
2. Definiendo la Introduction.
Una Introduction o Mixin en Aspectj y en Spring AOP, es un tipo de Aspecto, que permite añadir, en tiempo de ejecución, una interfaz y su respectiva implementación a un objeto existente. Esto causa la impresión de que el objeto utilizara herencia múltiple.
Esto es precisamente lo que necesitamos. Necesitamos añadir la Interfaz ApplicationContextAwareWithGetter y la implementación ApplicationCntextAwareDefaultImpl a los beans de servicio que queramos que tengan referencia a su ApplicationContext.
Definiimos el Aspecto Introduccion de la siguiente forma:
@Aspect
@Order(1)
public class ApplicationContextAwareIntroductionAdder{
@DeclareParents(defaultImpl=test.business.ApplicationCntextAwareDefaultImpl.class,value="test.business.services.*+")
public static ApplicationContextAwareWithGetter applicationContextAware;
}
Esta introduction define que todos los beans dentro del paquete test.business.services implementaran la interfaz ApplicationContextAwareWithGetter y delegaran sus métodos a la implementación ApplicationCntextAwareDefaultImpl.
3. Seteando el ApplicationContext a los beans:
Es necesario ahora setear el ApplicationContext a los beans de servicio. Que en tiempo de ejecución implementaran la interfaz ApplicationContextAwareWithGetter. Para setear el ApplicationContext en los beans utilizaremos el siguiente BeanPostProcessor.
public class AddApplicationContextPostProcessor implements BeanPostProcessor {
ApplicationContext context;
public AddApplicationContextPostProcessor(ApplicationContext context) {
this.context = context;
}
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ApplicationContextAwareWithGetter) {
((ApplicationContextAwareWithGetter) bean).setApplicationContext(context);
}
return bean;
}
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
Un BeanPostProcessor es un tipo especial de bean que permite tratar los bean de un ApplicationContext uno a uno luego de que estos son instanciados e inicializados.
En nuestro caso estamos haciendo lo siguiente. Luego de que cada bean en el ApplicationContext sea inicializado, revisamos si el bean implementa la interfaz ApplicationContextAwareWithGetter. De ser asi, le seteamos el aplication context que tenemos guardado en una variable de instancia (que luego mostraremos como la seteamos).
4. Registrando el BeanPostProcessor.
El BeanPostProcessor anterior se podría registrar directamente en el applicationContext.xml como un bean mas, sin embargo, como vimos, el PostProcessor necesita una referencia al ApplicationContext, para setearsela luego a los beans de servicio. Si hacemos que el PostProcessor implemente ApplicationContextAware, tampoco sirve, ya que se invocara al método setApplicationContext muy tarde, cuando ya se han “postProcesado” los beans por lo que el contexto inyectado a los beans es null.
Por tanto es necesario implementar otra estrategia. La solución mas obvia resulta implementar un BeanFactoryPostProcessor y registrar el BeanPostProcessor en el ApplicationContext manualmente.
4.1 El BeanFactoryPostProcessor.
Un BeanFactoryPostProcessor es un tipo especial de Bean que es invocado por Spring luego de cargar la definición de beans del ApplicationContext (o BeanFactory) pero antes de cualquier instanciación o inicialización. De esta forma, un BeanFactoryPostProcessor tiene acceso a la BeanFactory donde es definido y puede modificar la BeanFactory de diversas formas. Por ejemplo definiendo nuevos beans, sustituyendo elementos de definición, etc.
Cuando se implementa la interfaz BeanFactoryPostProcessor, es necesario implementar el método public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException.
La clase ConfigurableListableBeanFactory ofrece un API bastante amplio para trabajar con la BeanFactory.
La versión final de nuestro BeanFactoryPostProcessor queda de la siguiente forma:
public class RegisterBeanPostProcessorFactoryPostProcessor implements BeanFactoryPostProcessor,ApplicationContextAware{
private ApplicationContext context;
public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
BeanDefinitionRegistry registry=(BeanDefinitionRegistry)factory;
RootBeanDefinition definition=
new RootBeanDefinition(AddApplicationContextPostProcessor.class);
ConstructorArgumentValues constArguments=new ConstructorArgumentValues();
constArguments.addGenericArgumentValue(context);
definition.setConstructorArgumentValues(constArguments);
registry.registerBeanDefinition("postProcessor", definition);
}
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context=applicationContext;
}
}
Como se puede ver, la clase implementa ApplicationContextAware, para recibir la inyección del ApplicationContext, que es con el cual se instanciará luego el BeanPostProcessor.
ConfigurableListableBeanFactory posee un método para registrar BeanPostProcessors llamado addBeanPostProcessor. Sin embargo, si registráramos el BeanPostProcessor utilizando este método, no habría forma de colocar este PostProcessor al final en la lista de los PostProcessors que se aplicaran a los beans. Es decir, registrándolo de esta forma, el BeanPostProcessor sería invocado antes de que se invocara el BeanPostProcessor encargado de añadir las Introductions a los Beans, por lo que los beans que le llegarían no estarían implementado la interfaz ApplicationContextAwareWithGetter, de hecho nuestro bean PostProcessor procesaría incluso los BeansPostProcessors definidos en el fichero de configuración.
Por lo expuesto anteriormente, se decidió como solución registrar manualmente el Bean en la BeanFactory como cualquier otro Bean. Definiendo un RootBeanDefinition y registrándolo en la BeanFactory, añadiéndole como argumento del constructor el ApplicationContext.
5. Interceptando las llamadas a los servicios y accediendo a su AplicationContext.
El último paso que nos queda para comprobar el funcionamiento de lo que acabamos de hacer es la creación de un Aspecto que intercepte las llamadas a los servicios, y acceda al ApplicationContext de dichos servicios a través de la interfaz ApplicationContextAwareWithGetter.
El Aspecto es el siguiente:
@Aspect
public class ServiceInterceptor {
@Around ("execution(public * test.business.services.*.*(String))")
public Object intercept(ProceedingJoinPoint pjp) throws Throwable {
ApplicationContextAwareWithGetter appContextAware =
(ApplicationContextAwareWithGetter) pjp.getThis();
System.out.println("Display Name " + appContextAware.getApplicationContext().getDisplayName());
return pjp.proceed();
}
}
Este aspecto, lo único que hace es interceptar las llamadas a cualquier método publico de los servicios (definidos en el paquete test.business.services) que reciben un String como parámetro, e imprimir el “displayName” asociado a su AplicationContext. Nótese el Casting a ApplicationContextAwareWithGetter. Recordemos que nuestros servicios no implementan esta interfaz. Es la Introduction la que en tiempo de ejecución creó el Proxy que sí la implementa.
Esto es todo. Ya tenemos completa la funcionalidad. Hicimos que un POJO implementara en tiempo de ejecución una interfaz (con su implementación por defecto). Logramos definir un BeanPostProcessor programáticamente e incluirlo manualmente en la BeanFactory, y Post Procesamos nuestros Beans para añadirles una propiedad que no tenían, dependiendo de si implementaban una interfaz.
A continuación copio todo el código fuente del ejemplo.
Clase Main.java
package test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import test.business.services.Service1;
public class Main {
/**
* @param args
*/
public static void main(String[] args) {
ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");
((Service1)context.getBean("servicio1")).executeService("BUU");
((Service1)context.getBean("servicio2")).executeService("BUU");
}
}
Clase ApplicationContextAwareIntroductionAdder.java
package test.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import test.business.ApplicationContextAwareWithGetter;
@Aspect
@Order(1)
public class ApplicationContextAwareIntroductionAdder{
@DeclareParents(defaultImpl=test.business.ApplicationCntextAwareDefaultImpl.class,value="test.business.services.*+")
public static ApplicationContextAwareWithGetter applicationContextAware;
}
Clase ServiceInterceptor.java
package test.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.Order;
import test.business.ApplicationContextAwareWithGetter;
@Aspect
@Order(1)
public class ServiceInterceptor {
private String[] contextsToIntercept = { "context2" };
@Around("execution(public * test.business.services.*.*(String))")
public Object intercept(ProceedingJoinPoint pjp) throws Throwable {
ApplicationContextAwareWithGetter appContextAware = (ApplicationContextAwareWithGetter) pjp.getThis();
System.out.println("Display Name " + appContextAware.getApplicationContext().getDisplayName());
return pjp.proceed();
}
}
Clase AddApplicationContextPostProcessor.java
package test.beans;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import test.business.ApplicationContextAwareWithGetter;
public class AddApplicationContextPostProcessor implements BeanPostProcessor {
ApplicationContext context;
public AddApplicationContextPostProcessor(ApplicationContext context) {
this.context = context;
}
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ApplicationContextAwareWithGetter) {
((ApplicationContextAwareWithGetter) bean).setApplicationContext(context);
}
return bean;
}
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
Clase RegisterBeanPostProcessorFactoryPostProcessor.java
package test.beans;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class RegisterBeanPostProcessorFactoryPostProcessor implements BeanFactoryPostProcessor,ApplicationContextAware{
private ApplicationContext context;
public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
BeanDefinitionRegistry registry=(BeanDefinitionRegistry)factory;
RootBeanDefinition definition=new RootBeanDefinition(AddApplicationContextPostProcessor.class);
ConstructorArgumentValues constArguments=new ConstructorArgumentValues();
constArguments.addGenericArgumentValue(context);
definition.setConstructorArgumentValues(constArguments);
registry.registerBeanDefinition("postProcessor", definition);
}
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context=applicationContext;
}
}
Interface ApplicationContextAwareWithGetter.java
package test.business;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public interface ApplicationContextAwareWithGetter extends ApplicationContextAware{
ApplicationContext getApplicationContext();
void setApplicationContext(ApplicationContext beanFactory);
}
Clase ApplicationContextAwareDefaultImpl.java
package test.business;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
public class ApplicationCntextAwareDefaultImpl implements ApplicationContextAwareWithGetter{
public ApplicationContext context;
public ApplicationContext getApplicationContext() {
return context;
}
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context=context;
}
}
Interface Service.java
package test.business.services;
public interface Service {
String executeService(String cadena);
}
Clase ServiceImpl.java
package test.business.services;
public class ServiceImpl implements Service {
public String executeService(String cadena) {
return "hola "+cadena;
}
}
Fichero ApplicationContext.xml
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
Bibliografia
Spring in Action, Second Edition. Manning.
Spring 2.5 Recipes. Appress
Spring Reference Manual