Composing Java Based Configurations
Componer configuraciones basadas en Java
La funcionalidad de configuración basada en Java de Spring permite componer anotaciones, lo que puede reducir la complejidad de la configuración.
Uso de la anotación @Import
Al igual que el elemento <import/> se utiliza dentro de archivos XML de Spring para ayudar a modularizar configuraciones, la anotación @Import permite cargar definiciones @Bean desde otra clase de configuración, como muestra el siguiente ejemplo:
@Configuration
public class ConfigA {
@Bean
public A a() {
return new A();
}
}
@Configuration
@Import(ConfigA.class)
public class ConfigB {
@Bean
public B b() {
return new B();
}
}
Ahora, en lugar de tener que especificar tanto ConfigA.class como ConfigB.class al crear el contexto, solo debes proporcionar explícitamente ConfigB:
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(ConfigB.class);
// Ahora están disponibles los beans A y B.
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
}
Este enfoque simplifica la creación del contenedor, ya que solo debes gestionar una clase en vez de recordar un número potencialmente grande de clases @Configuration durante la construcción.
Desde Spring Framework 4.2, @Import también admite referencias a clases de componentes normales, de forma análoga al método AnnotationConfigApplicationContext.register.
Esto resulta especialmente útil si quieres evitar la exploración de componentes y utilizar unas pocas clases de configuración como puntos de entrada para definir explícitamente todos los componentes.
Inyectar dependencias en definiciones @Bean importadas
El ejemplo anterior funciona, pero es simplista. En la mayoría de escenarios prácticos, los beans tienen dependencias entre sí a través de clases de configuración.
Con XML no existe problema, porque no interviene ningún compilador y puedes declarar ref="someBean" y confiar en que Spring lo resuelva durante la inicialización del contenedor.
Con clases @Configuration, el compilador de Java impone restricciones al modelo de configuración: las referencias a otros beans deben ser sintaxis Java válida.
Por suerte, la solución es sencilla. Un método @Bean puede tener cualquier número de parámetros que describan las dependencias del bean.
Considera el siguiente escenario, más realista, con varias clases @Configuration, cada una dependiente de beans declarados en las demás:
@Configuration
public class ServiceConfig {
@Bean
public TransferService transferService(
AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
@Bean
public AccountRepository accountRepository(DataSource dataSource) {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// Devuelve un DataSource.
}
}
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(SystemTestConfig.class);
// Todo se conecta entre clases de configuración.
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
Existe otra forma de conseguir el mismo resultado. Las clases @Configuration son, en última instancia, otro bean del contenedor. Esto significa que pueden aprovechar la inyección mediante @Autowired, @Value y otras funcionalidades igual que cualquier otro bean.
Asegúrate de que las dependencias inyectadas de esta forma sean solo de la clase más sencilla. Las clases @Configuration se procesan muy pronto durante la inicialización del contexto y forzar la inyección de una dependencia de esta manera puede provocar una inicialización temprana inesperada.
Siempre que sea posible, utiliza inyección basada en parámetros, como en el ejemplo anterior.
Evita acceder a beans definidos localmente desde un método @PostConstruct de la misma clase de configuración. Esto provoca de hecho una referencia circular, porque los métodos @Bean no estáticos requieren semánticamente una instancia de clase de configuración totalmente inicializada para poder invocarse.
Cuando no se permiten referencias circulares —por ejemplo, en Spring Boot 2.6 o posterior—, esto puede desencadenar una BeanCurrentlyInCreationException.
También debes tener especial cuidado con las definiciones de BeanPostProcessor y BeanFactoryPostProcessor mediante @Bean. Normalmente deben declararse como métodos static @Bean, sin desencadenar la instanciación de la clase de configuración que los contiene.
En caso contrario, es posible que @Autowired y @Value no funcionen en la propia clase de configuración, ya que podría crearse como instancia de bean antes que AutowiredAnnotationBeanPostProcessor.
El siguiente ejemplo muestra cómo se puede autoconectar un bean a otro:
@Configuration
public class ServiceConfig {
@Autowired
private AccountRepository accountRepository;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
private final DataSource dataSource;
public RepositoryConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// Devuelve un DataSource.
}
}
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(SystemTestConfig.class);
// Todo se conecta entre clases de configuración.
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
No es necesario especificar @Autowired si el bean de destino define un único constructor.
Calificar completamente beans importados para facilitar la navegación
En el escenario anterior, utilizar @Autowired funciona bien y ofrece la modularidad deseada. Sin embargo, determinar exactamente dónde se declaran las definiciones de beans autoconectadas sigue siendo algo ambiguo.
Por ejemplo, al observar ServiceConfig, puede no estar claro dónde se declara exactamente el bean @Autowired AccountRepository. No es explícito en el código, y puede que esto sea aceptable.
La compatibilidad de Spring Tools con los IDE puede mostrar gráficos de conexión. Además, el IDE de Java puede localizar fácilmente todas las declaraciones y usos del tipo AccountRepository y mostrar la ubicación de los métodos @Bean que devuelven ese tipo.
Cuando esta ambigüedad no sea aceptable y quieras navegación directa desde el IDE entre una clase @Configuration y otra, considera autoconectar las propias clases de configuración:
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
// Navega a través de la clase de configuración hasta el método @Bean.
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
En el caso anterior, el lugar donde se define AccountRepository es completamente explícito. Sin embargo, ServiceConfig queda estrechamente acoplada a RepositoryConfig. Esa es la contrapartida.
Este acoplamiento estrecho puede mitigarse en cierta medida mediante clases @Configuration basadas en interfaces o clases abstractas. Considera el siguiente ejemplo:
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
@Configuration
public interface RepositoryConfig {
@Bean
AccountRepository accountRepository();
}
@Configuration
public class DefaultRepositoryConfig implements RepositoryConfig {
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(...);
}
}
@Configuration
@Import({ServiceConfig.class, DefaultRepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// Devuelve un DataSource.
}
}
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(SystemTestConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
Ahora ServiceConfig queda débilmente acoplada respecto a la implementación concreta DefaultRepositoryConfig, y las herramientas del IDE siguen siendo útiles: puedes obtener fácilmente una jerarquía de tipos con las implementaciones de RepositoryConfig.
De esta forma, navegar por las clases @Configuration y sus dependencias no es diferente del proceso habitual de navegar por código basado en interfaces.
Influir en el inicio de singletons definidos con @Bean
Si quieres influir en el orden de creación de determinados beans singleton al iniciar la aplicación, considera declarar algunos como @Lazy para crearlos en el primer acceso, en lugar de durante el inicio.
@DependsOn obliga a inicializar primero otros beans determinados, asegurando que los beans especificados se creen antes que el bean actual, más allá de lo que impliquen sus dependencias directas.
Inicialización en segundo plano
Desde la versión 6.2 existe una opción de inicialización en segundo plano: @Bean(bootstrap = BACKGROUND) permite seleccionar determinados beans para inicializarlos en segundo plano, cubriendo todo el paso de creación de cada uno al iniciar el contexto.
Los beans dependientes con puntos de inyección no diferidos esperan automáticamente a que se complete la instancia del bean. Todas las inicializaciones regulares en segundo plano se fuerzan a completarse al final del inicio del contexto.
Solo los beans que además se marcan con @Lazy pueden completarse más tarde, hasta el primer acceso real.
La inicialización en segundo plano suele combinarse con puntos de inyección @Lazy —o ObjectProvider— en los beans dependientes. De lo contrario, el hilo principal de arranque se bloqueará cuando deba inyectarse pronto una instancia inicializada en segundo plano.
Este tipo de inicio concurrente se aplica a beans individuales. Si uno de esos beans depende de otros, estos deben estar inicializados previamente, ya sea por haberse declarado antes o mediante @DependsOn, que obliga a inicializarlos en el hilo principal antes de activar la inicialización en segundo plano del bean afectado.
Para que el arranque en segundo plano esté activo, debes declarar un bean bootstrapExecutor de tipo Executor. De lo contrario, las marcas de inicialización en segundo plano se ignoran en tiempo de ejecución.
El ejecutor de arranque puede ser un ejecutor limitado solo para el inicio o un grupo de hilos compartido que también se utilice con otros fines.
Incluir condicionalmente clases @Configuration o métodos @Bean
A menudo resulta útil habilitar o deshabilitar condicionalmente una clase @Configuration completa o incluso métodos @Bean individuales, según un estado arbitrario del sistema.
Un ejemplo habitual consiste en utilizar la anotación @Profile para activar beans únicamente cuando se haya habilitado un perfil específico en el Environment de Spring.
La anotación @Profile se implementa utilizando una anotación más flexible llamada @Conditional.
@Conditional indica implementaciones específicas de org.springframework.context.annotation.Condition que deben consultarse antes de registrar un @Bean.
Las implementaciones de la interfaz Condition proporcionan un método matches(..) que devuelve true o false.
Por ejemplo, el siguiente listado muestra la implementación de Condition que utiliza @Profile:
@Override
public boolean matches(
ConditionContext context, AnnotatedTypeMetadata metadata) {
// Lee los atributos de la anotación @Profile.
MultiValueMap<String, Object> attrs =
metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().matchesProfiles((String[]) value)) {
return true;
}
}
return false;
}
return true;
}
Una anotación @Conditional declarada en una clase @Configuration contenedora solo se aplica al registro de una clase @Configuration anidada si el analizador llega a la clase anidada mediante la recursión desde la clase contenedora o mediante @Import.
Si una clase anidada se detecta de forma independiente de su clase contenedora —por ejemplo, mediante @ComponentScan o registrándola directamente en el contexto de aplicación—, se procesa solo con sus propias anotaciones @Conditional.
Por tanto, si quieres asegurar que las mismas anotaciones @Conditional se apliquen en esos escenarios, debes volver a declarar las anotaciones pertinentes en la clase anidada o extraerlas a una anotación compuesta que apliques tanto a la clase contenedora como a la clase anidada.
Combinar configuración Java y XML
El soporte para clases @Configuration de Spring no pretende sustituir al 100 % la configuración XML de Spring. Algunas funcionalidades, como los espacios de nombres XML de Spring, siguen siendo una forma ideal de configurar el contenedor.
Cuando XML resulte práctico o necesario, puedes elegir entre iniciar el contenedor de forma centrada en XML, usando por ejemplo ClassPathXmlApplicationContext, o iniciarlo de forma centrada en Java mediante AnnotationConfigApplicationContext y la anotación @ImportResource para importar XML según sea necesario.
Uso centrado en XML de clases @Configuration
Puede ser preferible iniciar el contenedor de Spring desde XML e incluir clases @Configuration de forma puntual.
Por ejemplo, en una base de código existente y grande que use XML de Spring, es más sencillo crear clases @Configuration cuando se necesiten e incluirlas desde los archivos XML existentes.
Declarar clases @Configuration como elementos <bean/> normales de Spring
Recuerda que las clases @Configuration son, en última instancia, definiciones de beans dentro del contenedor.
En el siguiente ejemplo se crea una clase @Configuration llamada AppConfig y se incluye en system-test-config.xml como una definición <bean/>.
Como <context:annotation-config/> está habilitado, el contenedor reconoce la anotación @Configuration y procesa correctamente los métodos @Bean declarados en AppConfig.
@Configuration
public class AppConfig {
@Autowired
private DataSource dataSource;
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
@Bean
public TransferService transferService() {
return new TransferServiceImpl(accountRepository());
}
}
El siguiente ejemplo muestra parte de un archivo system-test-config.xml:
<beans>
<!-- Habilita el procesamiento de anotaciones como @Autowired y @Configuration. -->
<context:annotation-config/>
<context:property-placeholder
location="classpath:/com/acme/jdbc.properties"/>
<bean class="com.acme.AppConfig"/>
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>
Un posible archivo jdbc.properties sería el siguiente:
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"classpath:/com/acme/system-test-config.xml");
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
En system-test-config.xml, el <bean/> de AppConfig no declara un atributo id. Sería válido hacerlo, pero no es necesario, dado que ningún otro bean hace referencia a él ni es probable que se obtenga explícitamente desde el contenedor por nombre.
De forma similar, el bean DataSource solo se autoconecta por tipo, por lo que tampoco es estrictamente necesario un id de bean explícito.
Usar <context:component-scan/> para detectar clases @Configuration
Como @Configuration está meta-anotada con @Component, las clases anotadas con @Configuration son candidatas automáticamente para la exploración de componentes.
Usando el mismo escenario del ejemplo anterior, puedes redefinir system-test-config.xml para aprovechar la exploración de componentes.
En este caso, no necesitas declarar explícitamente <context:annotation-config/>, porque <context:component-scan/> habilita la misma funcionalidad.
El siguiente ejemplo muestra el archivo system-test-config.xml modificado:
<beans>
<!-- Detecta y registra AppConfig como definición de bean. -->
<context:component-scan base-package="com.acme"/>
<context:property-placeholder
location="classpath:/com/acme/jdbc.properties"/>
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>
Uso de XML centrado en clases @Configuration con @ImportResource
En aplicaciones donde las clases @Configuration son el mecanismo principal para configurar el contenedor, puede seguir siendo necesario utilizar al menos una parte de XML.
En esos casos, puedes utilizar @ImportResource y definir solo el XML necesario. Así se consigue un enfoque centrado en Java y el XML se reduce al mínimo.
El siguiente ejemplo —que incluye una clase de configuración, un archivo XML que define un bean, un archivo de propiedades y el método main()— muestra cómo usar @ImportResource para lograr una configuración centrada en Java que use XML cuando resulte necesario:
@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
return new DriverManagerDataSource(url, username, password);
}
@Bean
public AccountRepository accountRepository(DataSource dataSource) {
return new JdbcAccountRepository(dataSource);
}
@Bean
public TransferService transferService(
AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
properties-config.xml:
<beans>
<context:property-placeholder
location="classpath:/com/acme/jdbc.properties"/>
</beans>
jdbc.properties:
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}