Container Extension Points
Puntos de extensión del contenedor
Normalmente, un desarrollador de aplicaciones no necesita extender las clases de implementación de ApplicationContext. En su lugar, el contenedor IoC de Spring puede ampliarse incorporando implementaciones de interfaces especiales de integración. Las secciones siguientes describen estas interfaces de integración.
Personalizar beans mediante BeanPostProcessor
La interfaz BeanPostProcessor define métodos de devolución de llamada que puedes implementar para proporcionar tu propia lógica —o sustituir la lógica predeterminada del contenedor— para la creación de instancias, la resolución de dependencias y otros aspectos.
Si quieres implementar lógica personalizada después de que el contenedor de Spring termine de instanciar, configurar e inicializar un bean, puedes incorporar una o varias implementaciones personalizadas de BeanPostProcessor.
Puedes configurar varias instancias de BeanPostProcessor y controlar su orden de ejecución estableciendo la propiedad order. Solo puedes establecer esa propiedad si el BeanPostProcessor implementa la interfaz Ordered. Si escribes tu propio BeanPostProcessor, también deberías considerar implementar Ordered.
Las instancias de BeanPostProcessor trabajan sobre instancias de beans u objetos: el contenedor IoC de Spring crea una instancia de bean y, a continuación, los BeanPostProcessor realizan su trabajo.
Las instancias de BeanPostProcessor tienen ámbito por contenedor. Esto solo es relevante si utilizas jerarquías de contenedores. Si defines un BeanPostProcessor en un contenedor, solo posprocesa los beans de ese contenedor. Los beans definidos en otro contenedor no son posprocesados por un BeanPostProcessor definido en el primero, aunque ambos formen parte de la misma jerarquía.
Para modificar la definición real de un bean —es decir, el plano que define el bean— debes utilizar un BeanFactoryPostProcessor.
La interfaz org.springframework.beans.factory.config.BeanPostProcessor consta exactamente de dos métodos de devolución de llamada. Cuando se registra una clase de este tipo como posprocesador en el contenedor, recibe una devolución de llamada para cada instancia de bean creada: una antes de los métodos de inicialización del contenedor, como InitializingBean.afterPropertiesSet() o cualquier método init declarado, y otra después de las devoluciones de llamada de inicialización.
El posprocesador puede realizar cualquier acción con la instancia de bean, incluida ignorar por completo la devolución de llamada. Un posprocesador de beans suele comprobar interfaces de devolución de llamada o envolver un bean con un proxy. Algunas clases de infraestructura de Spring AOP se implementan como posprocesadores de beans para proporcionar la lógica de creación de proxies.
Un ApplicationContext detecta automáticamente todos los beans definidos en los metadatos de configuración que implementen BeanPostProcessor. El ApplicationContext los registra como posprocesadores para invocarlos más adelante, durante la creación de beans. Los posprocesadores de beans se pueden desplegar en el contenedor igual que cualquier otro bean.
Al declarar un BeanPostProcessor mediante un método de fábrica @Bean en una clase de configuración, el tipo de retorno del método debe ser la propia clase de implementación o, al menos, la interfaz org.springframework.beans.factory.config.BeanPostProcessor. De lo contrario, el ApplicationContext no puede detectarlo automáticamente por tipo antes de crearlo por completo.
Como un BeanPostProcessor debe instanciarse pronto para aplicarse a la inicialización de otros beans del contexto, esta detección temprana por tipo es fundamental.
Además, cuando registres un BeanPostProcessor mediante un método de fábrica @Bean, declara el método como static y, preferiblemente, sin dependencias. Así se evita la inicialización anticipada de la clase de configuración y de otros beans, que los haría no aptos para un posprocesamiento completo, como la creación automática de proxies.
Registrar BeanPostProcessor mediante programación
Aunque el enfoque recomendado para registrar BeanPostProcessor es la detección automática de ApplicationContext, también puedes registrarlos mediante programación en un ConfigurableBeanFactory con el método addBeanPostProcessor.
Esto puede ser útil cuando necesitas evaluar lógica condicional antes del registro o para copiar posprocesadores de beans entre contextos de una jerarquía.
Sin embargo, las instancias de BeanPostProcessor añadidas mediante programación no respetan la interfaz Ordered. En este caso, el orden de registro determina el orden de ejecución. Además, los BeanPostProcessor registrados mediante programación se procesan siempre antes que los registrados mediante detección automática, independientemente de cualquier orden explícito.
BeanPostProcessor e inicialización temprana
Las clases que implementan BeanPostProcessor son especiales y el contenedor las trata de forma diferente. Todas las instancias de BeanPostProcessor y los beans a los que hacen referencia directamente se instancian durante el arranque, como parte de una fase especial de inicio del ApplicationContext. Después, todas las instancias de BeanPostProcessor se registran ordenadamente y se aplican a los demás beans del contenedor.
Como la creación automática de proxies AOP se implementa mediante un BeanPostProcessor, ni las instancias de BeanPostProcessor ni los beans a los que hacen referencia directamente pueden optar a la creación automática de proxies; por tanto, no se tejen aspectos en ellos. En general, cualquier bean instanciado durante esta fase temprana no es apto para el posprocesamiento completo por parte de todos los BeanPostProcessor.
Para esos beans, deberías ver un mensaje de registro de nivel WARN parecido al siguiente:
Bean 'someBean' of type [org.example.SomeType] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying).
Para minimizar el número de beans afectados, registra un BeanPostProcessor mediante un método @Bean static sin dependencias. Si conectas beans a tu BeanPostProcessor mediante autoconexión o @Resource —que puede recurrir a la autoconexión—, Spring puede acceder a beans inesperados al buscar candidatos de dependencia compatibles por tipo. Como consecuencia, esos beans pueden dejar de ser aptos para la creación automática de proxies u otros tipos de posprocesamiento.
Por ejemplo, cuando tienes una dependencia anotada con @Resource cuyo nombre de campo o de setter no coincide directamente con el nombre declarado de un bean y no se utiliza el atributo name, Spring accede a otros beans para compararlos por tipo.
Los ejemplos siguientes muestran cómo escribir, registrar y utilizar instancias de BeanPostProcessor en un ApplicationContext.
Ejemplo: Hola mundo con BeanPostProcessor
Este primer ejemplo ilustra el uso básico. Muestra una implementación personalizada de BeanPostProcessor que invoca el método toString() de cada bean a medida que el contenedor lo crea e imprime la cadena resultante en la consola del sistema.
La siguiente lista muestra la clase de implementación personalizada de BeanPostProcessor:
package scripting;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {
// Devuelve simplemente el bean instanciado sin cambios.
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean; // Aquí podríamos devolver cualquier referencia de objeto.
}
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("Bean '" + beanName + "' creado: " + bean);
return bean;
}
}
Puedes registrar InstantiationTracingBeanPostProcessor mediante configuración Java usando un método @Bean static —recomendado para evitar la inicialización anticipada de la clase de configuración y de otros beans—:
@Configuration
public class AppConfig {
@Bean
public static InstantiationTracingBeanPostProcessor
instantiationTracingBeanPostProcessor() {
return new InstantiationTracingBeanPostProcessor();
}
// ... otras definiciones de beans
}
Como alternativa, InstantiationTracingBeanPostProcessor puede registrarse con el elemento bean mediante configuración XML:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang
https://www.springframework.org/schema/lang/spring-lang.xsd">
<lang:groovy id="messenger"
script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
<lang:property name="message" value="Fiona Apple Is Just So Dreamy."/>
</lang:groovy>
<!--
Cuando se instancia el bean anterior (messenger), esta implementación
personalizada de BeanPostProcessor imprime ese hecho en la consola.
-->
<bean class="scripting.InstantiationTracingBeanPostProcessor"/>
</beans>
Observa que InstantiationTracingBeanPostProcessor se limita a definirse. Ni siquiera tiene nombre y, como es un bean, puede recibir dependencias mediante inyección igual que cualquier otro bean. La configuración anterior también define un bean respaldado por un script de Groovy.
La siguiente aplicación Java ejecuta el código y la configuración anteriores:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;
public final class Boot {
public static void main(final String[] args) throws Exception {
ApplicationContext ctx =
new ClassPathXmlApplicationContext("scripting/beans.xml");
Messenger messenger = ctx.getBean("messenger", Messenger.class);
System.out.println(messenger);
}
}
La salida de la aplicación es parecida a la siguiente:
Bean 'messenger' creado: org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961
Ejemplo: AutowiredAnnotationBeanPostProcessor
El uso de interfaces de devolución de llamada o anotaciones junto con una implementación personalizada de BeanPostProcessor es una forma habitual de extender el contenedor IoC de Spring.
Un ejemplo es AutowiredAnnotationBeanPostProcessor, una implementación de BeanPostProcessor incluida en la distribución de Spring, que conecta automáticamente campos, métodos setter y métodos de configuración arbitrarios anotados.
Personalizar metadatos de configuración mediante BeanFactoryPostProcessor
El siguiente punto de extensión es org.springframework.beans.factory.config.BeanFactoryPostProcessor. La semántica de esta interfaz es similar a la de BeanPostProcessor, con una diferencia importante: BeanFactoryPostProcessor opera sobre los metadatos de configuración de los beans.
Es decir, el contenedor IoC de Spring permite que un BeanFactoryPostProcessor lea los metadatos de configuración y, potencialmente, los modifique antes de que el contenedor cree cualquier bean, excepto las instancias de BeanFactoryPostProcessor.
Puedes configurar varias instancias de BeanFactoryPostProcessor y controlar su orden de ejecución estableciendo la propiedad order. Sin embargo, solo puedes establecer esa propiedad si el BeanFactoryPostProcessor implementa la interfaz Ordered. Si escribes tu propio BeanFactoryPostProcessor, también deberías considerar implementar Ordered.
Si quieres modificar las instancias reales de bean —los objetos creados a partir de los metadatos de configuración— debes usar un BeanPostProcessor. Aunque técnicamente es posible trabajar con instancias de beans dentro de un BeanFactoryPostProcessor, por ejemplo mediante BeanFactory.getBean(), hacerlo provoca una instanciación prematura y rompe el ciclo de vida estándar del contenedor. Esto puede producir efectos negativos, como omitir el posprocesamiento de beans.
Las instancias de BeanFactoryPostProcessor tienen ámbito por contenedor. Esto solo es relevante si utilizas jerarquías de contenedores. Si defines un BeanFactoryPostProcessor en un contenedor, solo se aplica a las definiciones de beans de ese contenedor. Las definiciones de beans de un contenedor no son posprocesadas por instancias de BeanFactoryPostProcessor de otro contenedor, aunque ambos formen parte de la misma jerarquía.
Un posprocesador de fábrica de beans se ejecuta automáticamente cuando se declara dentro de un ApplicationContext, para aplicar cambios a los metadatos de configuración que definen el contenedor. Spring incluye varios posprocesadores de fábrica de beans predefinidos, como PropertyOverrideConfigurer y PropertySourcesPlaceholderConfigurer. También puedes usar un BeanFactoryPostProcessor personalizado, por ejemplo, para registrar editores de propiedades personalizados.
Un ApplicationContext detecta automáticamente los beans desplegados que implementan BeanFactoryPostProcessor. Los utiliza como posprocesadores de fábrica de beans en el momento oportuno. Puedes desplegar estos beans igual que cualquier otro.
Al registrar un BeanFactoryPostProcessor mediante un método de fábrica @Bean en una clase @Configuration, declara el método como static para evitar conflictos de ciclo de vida con el procesamiento de anotaciones —como @Autowired, @Value y @PostConstruct— en la clase de configuración.
Para cualquier método de fábrica @Bean no estático cuyo tipo de retorno sea BeanFactoryPostProcessor, deberías ver un mensaje de registro de nivel INFO parecido al siguiente:
@Bean method MyConfig.myBfpp is non-static and returns an object assignable to Spring’s BeanFactoryPostProcessor interface. This will result in a failure to process annotations such as @Autowired, @Resource, and @PostConstruct within the method’s declaring @Configuration class. Add the 'static' modifier to this method to avoid these container lifecycle issues; see @Bean javadoc for complete details.
Al igual que con las instancias de BeanPostProcessor, normalmente no debes configurar BeanFactoryPostProcessor para inicialización diferida. Si ningún otro bean hace referencia a un Bean(Factory)PostProcessor, ese posprocesador no llegaría a instanciarse. Por ello, marcarlo para inicialización diferida se ignora y se instanciará de forma anticipada aunque establezcas default-lazy-init="true" en el elemento <beans/>.
Ejemplo: sustitución de marcadores de posición mediante PropertySourcesPlaceholderConfigurer
Puedes utilizar PropertySourcesPlaceholderConfigurer para externalizar valores de propiedades de una definición de bean en un archivo separado con el formato estándar de Java Properties.
Así, la persona que despliega la aplicación puede personalizar propiedades específicas del entorno —como URL y contraseñas de bases de datos— sin la complejidad ni el riesgo de modificar el archivo principal de definiciones XML del contenedor.
Considera el siguiente fragmento de metadatos de configuración XML, donde se define un DataSource con valores de marcador de posición:
<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<property name="locations" value="classpath:com/something/jdbc.properties"/>
</bean>
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
El ejemplo muestra propiedades configuradas desde un archivo externo Properties. En tiempo de ejecución, se aplica un PropertySourcesPlaceholderConfigurer a los metadatos que reemplaza algunas propiedades del DataSource.
Los valores que se sustituyen se indican como marcadores de posición con el formato ${nombre-propiedad}, siguiendo el estilo de Ant, log4j y JSP EL.
Los valores reales proceden de otro archivo con el formato estándar de Java Properties:
jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root
Por tanto, la cadena ${jdbc.username} se sustituye en tiempo de ejecución por el valor sa, y lo mismo ocurre con otros marcadores que coincidan con claves del archivo de propiedades.
PropertySourcesPlaceholderConfigurer busca marcadores de posición en la mayoría de las propiedades y atributos de una definición de bean. También puedes personalizar el prefijo, el sufijo, el separador de valor predeterminado y el carácter de escape de los marcadores.
Además, puedes cambiar o deshabilitar globalmente el carácter de escape predeterminado configurando la propiedad spring.placeholder.escapeCharacter.default mediante una propiedad del sistema de la JVM o mediante el mecanismo SpringProperties.
Con el espacio de nombres context, puedes configurar marcadores de posición de propiedades con un elemento específico. Puedes proporcionar una o varias ubicaciones como lista separada por comas en el atributo location:
<context:property-placeholder
location="classpath:com/something/jdbc.properties"/>
PropertySourcesPlaceholderConfigurer no solo busca propiedades en el archivo Properties indicado. De forma predeterminada, si no encuentra una propiedad en los archivos especificados, la busca en las propiedades Environment de Spring y en las propiedades normales del sistema Java.
Solo debe definirse un elemento de este tipo para una aplicación, con las propiedades que necesite. Puedes configurar varios marcadores de propiedades siempre que utilicen una sintaxis distinta, como ${…}.
Si necesitas modularizar el origen de las propiedades utilizadas para la sustitución, no debes crear varios marcadores de propiedades. En su lugar, debes crear tu propio bean PropertySourcesPlaceholderConfigurer que reúna las propiedades que se usarán.
Puedes utilizar PropertySourcesPlaceholderConfigurer para sustituir nombres de clase, lo que a veces resulta útil cuando debes elegir una clase de implementación concreta en tiempo de ejecución:
<bean class="org.springframework.beans.factory.config.PropertySourcesPlaceholderConfigurer">
<property name="locations">
<value>classpath:com/something/strategy.properties</value>
</property>
<property name="properties">
<value>custom.strategy.class=com.something.DefaultStrategy</value>
</property>
</bean>
<bean id="serviceStrategy" class="${custom.strategy.class}"/>
Si la clase no puede resolverse en tiempo de ejecución como una clase válida, la resolución del bean falla cuando está a punto de crearse, durante la fase preInstantiateSingletons() de un ApplicationContext para un bean sin inicialización diferida.
Ejemplo: PropertyOverrideConfigurer
PropertyOverrideConfigurer, otro posprocesador de fábrica de beans, se parece a PropertySourcesPlaceholderConfigurer. No obstante, a diferencia de este último, las definiciones originales pueden tener valores predeterminados o ningún valor para las propiedades de los beans. Si un archivo Properties de sobrescritura no contiene una entrada para una propiedad de bean concreta, se utiliza la definición predeterminada del contexto.
La definición de bean no sabe que se está sobrescribiendo. Por tanto, no resulta inmediatamente evidente en el archivo de definición XML que se esté utilizando el configurador de sobrescritura. Si varias instancias de PropertyOverrideConfigurer definen valores distintos para la misma propiedad de bean, prevalece la última debido al mecanismo de sobrescritura.
Las líneas de configuración del archivo Properties tienen el siguiente formato:
nombreBean.propiedad=valor
El siguiente listado muestra un ejemplo:
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb
Este archivo puede utilizarse con una definición de contenedor que incluya un bean llamado dataSource con las propiedades driverClassName y url.
También se admiten nombres de propiedades compuestas, siempre que todos los componentes de la ruta, salvo la propiedad final que se sobrescribe, ya sean distintos de null, presumiblemente inicializados por los constructores.
En el siguiente ejemplo, la propiedad sammy de la propiedad bob de la propiedad fred del bean tom se establece en el valor escalar 123:
tom.fred.bob.sammy=123
Los valores de sobrescritura especificados son siempre valores literales. No se convierten en referencias a beans. Esta convención también se aplica cuando el valor original de la definición XML del bean especifica una referencia a otro bean.
Con el espacio de nombres context, puedes configurar la sobrescritura de propiedades con un elemento específico:
<context:property-override location="classpath:override.properties"/>
Personalizar la lógica de instanciación mediante FactoryBean
Puedes implementar la interfaz org.springframework.beans.factory.FactoryBean para objetos que son, a su vez, fábricas.
La interfaz FactoryBean es un punto de extensibilidad dentro de la lógica de instanciación del contenedor IoC de Spring. Si tienes código de inicialización complejo que se expresa mejor en Java que mediante una cantidad potencialmente extensa de XML, puedes crear tu propio FactoryBean, escribir en esa clase la inicialización compleja e incorporar tu FactoryBean personalizado al contenedor.
La interfaz FactoryBean<T> proporciona tres métodos:
T getObject(): devuelve una instancia del objeto que crea esta fábrica. La instancia puede compartirse, según la fábrica devuelva singletons o prototypes.boolean isSingleton(): devuelvetruesi esteFactoryBeandevuelve singletons, ofalseen caso contrario. La implementación predeterminada devuelvetrue.Class<?> getObjectType(): devuelve el tipo de objeto retornado porgetObject()onullsi el tipo no se conoce de antemano.
El concepto y la interfaz FactoryBean se utilizan en muchos lugares dentro de Spring Framework. Spring incluye más de 50 implementaciones de FactoryBean.
Cuando necesitas solicitar al contenedor la instancia real de un FactoryBean, en lugar del bean que produce, antepone el símbolo de ampersand (&) al id del bean al invocar el método getBean() del ApplicationContext.
Por ejemplo, para un FactoryBean con id myBean, la llamada getBean("myBean") devuelve el producto del FactoryBean, mientras que getBean("&myBean") devuelve la propia instancia de FactoryBean.