Classpath Scanning And Managed Components

Exploración del classpath y componentes gestionados

La mayoría de los ejemplos de este capítulo utilizan XML para especificar los metadatos de configuración que producen cada BeanDefinition dentro del contenedor de Spring. La sección anterior, sobre configuración del contenedor basada en anotaciones, muestra cómo proporcionar gran parte de esos metadatos mediante anotaciones en el código fuente. Sin embargo, incluso en esos ejemplos, las definiciones de beans base se declaran explícitamente en XML, mientras que las anotaciones solo controlan la inyección de dependencias.

Esta sección describe una opción para detectar implícitamente componentes candidatos mediante la exploración del classpath. Los componentes candidatos son clases que coinciden con criterios de filtrado y para las que se registra una definición de bean correspondiente en el contenedor. Esto elimina la necesidad de utilizar XML para registrar beans.

En su lugar, puedes usar anotaciones, como @Component, expresiones de tipo de AspectJ o criterios de filtrado personalizados para seleccionar qué clases tendrán definiciones de bean registradas en el contenedor.

También puedes definir beans con Java en lugar de archivos XML. Consulta las anotaciones @Configuration, @Bean, @Import y @DependsOn para ver ejemplos de estas funcionalidades.

@Component y otras anotaciones estereotipo

La anotación @Repository es un marcador para cualquier clase que cumpla la función o estereotipo de repositorio, también conocido como Data Access Object o DAO. Uno de los usos de este marcador es la traducción automática de excepciones.

Spring proporciona otras anotaciones estereotipo: @Component, @Service y @Controller.

@Component es un estereotipo genérico para cualquier componente gestionado por Spring. @Repository, @Service y @Controller son especializaciones de @Component para casos de uso más concretos: las capas de persistencia, servicio y presentación, respectivamente.

Por tanto, puedes anotar las clases de tus componentes con @Component. Sin embargo, al anotarlas con @Repository, @Service o @Controller, las clases quedan mejor preparadas para ser procesadas por herramientas o asociadas a aspectos. Por ejemplo, estas anotaciones estereotipo son objetivos ideales para pointcuts.

@Repository, @Service y @Controller también pueden incorporar semántica adicional en futuras versiones de Spring Framework.

Por ello, si tienes que elegir entre @Component y @Service para la capa de servicio, @Service es la mejor elección. Del mismo modo, @Repository ya está admitida como marcador para la traducción automática de excepciones en la capa de persistencia.

Uso de meta-anotaciones y anotaciones compuestas

Muchas de las anotaciones proporcionadas por Spring pueden utilizarse como meta-anotaciones en tu propio código. Una meta-anotación es una anotación que se puede aplicar a otra anotación.

Por ejemplo, @Service está meta-anotada con @Component, como muestra el siguiente ejemplo:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // (1)
public @interface Service {

	// ...
}
  1. La meta-anotación @Component hace que @Service se trate igual que @Component.

También puedes combinar meta-anotaciones para crear anotaciones compuestas. Por ejemplo, la anotación @RestController de Spring MVC está compuesta por @Controller y @ResponseBody.

Además, las anotaciones compuestas pueden volver a declarar opcionalmente atributos de sus meta-anotaciones para permitir su personalización. Esto puede ser especialmente útil cuando quieres exponer solo un subconjunto de los atributos de una meta-anotación.

Por ejemplo, la anotación @SessionScope de Spring fija el nombre del ámbito en session, pero permite personalizar el valor de proxyMode. La siguiente lista muestra su definición:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {

	/**
	 * Alias de {@link Scope#proxyMode}.
	 * <p>El valor predeterminado es {@link ScopedProxyMode#TARGET_CLASS}.
	 */
	@AliasFor(annotation = Scope.class)
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

Después puedes usar @SessionScope sin declarar proxyMode:

@Service
@SessionScope
public class SessionScopedService {
	// ...
}

También puedes sobrescribir el valor de proxyMode:

@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
	// ...
}

Detectar automáticamente clases y registrar definiciones de beans

Spring puede detectar automáticamente clases estereotipadas y registrar las instancias de BeanDefinition correspondientes en el ApplicationContext.

Por ejemplo, las dos clases siguientes pueden detectarse automáticamente:

@Service
public class SimpleMovieLister {

	private final MovieFinder movieFinder;

	public SimpleMovieLister(MovieFinder movieFinder) {
		this.movieFinder = movieFinder;
	}
}
@Repository
public class JpaMovieFinder implements MovieFinder {
	// Se omite la implementación para mayor claridad.
}

Para detectar automáticamente estas clases y registrar sus beans correspondientes, debes añadir @ComponentScan a la clase @Configuration. Configura el atributo basePackages con un paquete padre común para ambas clases.

Como alternativa, puedes especificar una lista separada por comas, punto y coma o espacios que incluya el paquete padre de cada clase:

@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
	// ...
}

Para abreviar, el ejemplo anterior podría utilizar el atributo implícito value de la anotación:

@ComponentScan("org.example")

El siguiente ejemplo utiliza 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:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
		https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context
		https://www.springframework.org/schema/context/spring-context.xsd">

	<context:component-scan base-package="org.example"/>

</beans>

El uso de <context:component-scan> habilita implícitamente la funcionalidad de <context:annotation-config>. Normalmente no es necesario incluir <context:annotation-config> cuando utilizas <context:component-scan>.

La exploración de paquetes del classpath requiere la presencia de las entradas de directorio correspondientes en el classpath. Si creas archivos JAR con Ant, asegúrate de no activar la opción files-only de la tarea JAR.

Además, los directorios del classpath pueden no estar expuestos debido a políticas de seguridad en determinados entornos.

En la ruta de módulos del sistema de módulos de Java, la exploración del classpath de Spring suele funcionar correctamente. No obstante, asegúrate de que las clases de tus componentes se exporten en los descriptores module-info.

Si esperas que Spring invoque miembros no públicos de tus clases, asegúrate de que los paquetes estén abiertos mediante una declaración opens, en lugar de una declaración exports, en el archivo module-info.

AutowiredAnnotationBeanPostProcessor y CommonAnnotationBeanPostProcessor también se incluyen implícitamente al utilizar <context:component-scan>. Esto significa que ambos componentes se detectan y conectan automáticamente, sin metadatos de configuración de beans proporcionados en XML.

Puedes deshabilitar el registro de AutowiredAnnotationBeanPostProcessor y CommonAnnotationBeanPostProcessor incluyendo el atributo annotation-config con el valor false:

<context:component-scan base-package="org.example" annotation-config="false"/>

Marcadores de posición de propiedades y patrones de estilo Ant

Los atributos basePackages y value de @ComponentScan admiten marcadores de posición de propiedades con el formato ${…}. Estos se resuelven con el Environment.

También admiten patrones de paquetes de estilo Ant, como "org.example.**".

Además, puedes especificar varios paquetes o patrones, ya sea por separado o dentro de una sola cadena. Por ejemplo:

@ComponentScan({
		"org.example.config",
		"org.example.service.**"
})

También puedes usar una sola cadena:

@ComponentScan("org.example.config, org.example.service.**")

El siguiente ejemplo utiliza el marcador de posición app.scan.packages para el atributo implícito value de @ComponentScan:

@Configuration
@ComponentScan("${app.scan.packages}") // (1)
public class AppConfig {
	// ...
}
  1. Marcador de posición app.scan.packages que se resuelve mediante el Environment.

El siguiente archivo de propiedades define app.scan.packages. En el ejemplo anterior se presupone que el archivo se ha registrado en el Environment, por ejemplo, mediante @PropertySource u otro mecanismo equivalente:

app.scan.packages=org.example.config, org.example.service.**

Usar filtros para personalizar la exploración

De forma predeterminada, las únicas clases detectadas como componentes candidatos son las anotadas con @Component, @Repository, @Service, @Controller, @RestController, @Configuration o con una anotación personalizada que, a su vez, esté anotada con @Component.

Puedes modificar y ampliar este comportamiento aplicando filtros personalizados.

Añádelos mediante los atributos includeFilters o excludeFilters de @ComponentScan, o como elementos secundarios <context:include-filter/> y <context:exclude-filter/> de <context:component-scan> en configuración XML.

Cada elemento de filtro requiere los atributos type y expression.

Tipo de filtro Expresión de ejemplo Descripción
annotation (predeterminado) org.example.SomeAnnotation Una anotación presente o meta-presente en el nivel de tipo de los componentes de destino.
assignable org.example.SomeClass Una clase o interfaz que los componentes de destino pueden extender o implementar.
aspectj org.example..*Service+ Una expresión de tipo AspectJ que debe coincidir con los componentes de destino.
regex org\.example\.Default.* Una expresión regular que debe coincidir con los nombres de clase de los componentes de destino.
custom org.example.MyTypeFilter Una implementación personalizada de la interfaz org.springframework.core.type.TypeFilter.

El siguiente ejemplo muestra una configuración @ComponentScan que excluye todas las anotaciones @Repository e incluye en su lugar repositorios cuyo nombre contenga Stub:

@Configuration
@ComponentScan(
		basePackages = "org.example",
		includeFilters = @Filter(
				type = FilterType.REGEX,
				pattern = ".*Stub.*Repository"),
		excludeFilters = @Filter(Repository.class)
)
public class AppConfig {
	// ...
}

La siguiente lista muestra el XML equivalente:

<beans>
	<context:component-scan base-package="org.example">
		<context:include-filter type="regex"
				expression=".*Stub.*Repository"/>
		<context:exclude-filter type="annotation"
				expression="org.springframework.stereotype.Repository"/>
	</context:component-scan>
</beans>

También puedes deshabilitar los filtros predeterminados estableciendo useDefaultFilters=false en la anotación o proporcionando use-default-filters="false" como atributo de <component-scan/>.

Esto desactiva la detección automática de clases anotadas o meta-anotadas con @Component, @Repository, @Service, @Controller, @RestController o @Configuration.

Asignar nombres a componentes detectados automáticamente

Cuando un componente se detecta automáticamente durante la exploración, el nombre de su bean se genera mediante la estrategia BeanNameGenerator conocida por el explorador.

De forma predeterminada, se usa AnnotationBeanNameGenerator.

Para las anotaciones estereotipo de Spring, si proporcionas un nombre mediante el atributo value de la anotación, se utiliza ese nombre en la definición de bean correspondiente. Esta convención también se aplica al usar @jakarta.inject.Named en lugar de una anotación estereotipo de Spring.

Desde Spring Framework 6.1, el nombre del atributo de anotación utilizado para especificar el nombre del bean ya no tiene que ser necesariamente value.

Las anotaciones estereotipo personalizadas pueden declarar un atributo con un nombre distinto, como name, y anotar ese atributo con:

@AliasFor(annotation = Component.class, attribute = "value")

Desde Spring Framework 6.1, se ha desaprobado el soporte para nombres de estereotipos basados en convenciones y se eliminará en una futura versión del framework. Por ello, las anotaciones estereotipo personalizadas deben usar @AliasFor para declarar un alias explícito del atributo value de @Component.

Cuando no se puede derivar un nombre de bean explícito a partir de una anotación, o para cualquier otro componente detectado —por ejemplo, uno encontrado mediante filtros personalizados—, el generador de nombres predeterminado devuelve el nombre de clase no cualificado sin mayúscula inicial.

Por ejemplo, si se detectan las siguientes clases, sus nombres serán myMovieLister y movieFinderImpl:

@Service("myMovieLister")
public class SimpleMovieLister {
	// ...
}
@Repository
public class MovieFinderImpl implements MovieFinder {
	// ...
}

Si no quieres depender de la estrategia predeterminada de asignación de nombres, puedes proporcionar una estrategia personalizada.

Primero, implementa la interfaz BeanNameGenerator o ConfigurationBeanNameGenerator y asegúrate de incluir un constructor predeterminado sin argumentos. Después, proporciona el nombre de clase totalmente cualificado al configurar el explorador:

@Configuration
@ComponentScan(
		basePackages = "org.example",
		nameGenerator = MyNameGenerator.class
)
public class AppConfig {
	// ...
}
<beans>
	<context:component-scan
			base-package="org.example"
			name-generator="org.example.MyNameGenerator"/>
</beans>

Si encuentras conflictos de nombres debido a varios componentes detectados automáticamente con el mismo nombre de clase no cualificado —por ejemplo, clases con nombres idénticos en paquetes distintos—, puedes configurar un BeanNameGenerator que utilice por defecto el nombre de clase totalmente cualificado para el nombre del bean generado.

Puedes usar FullyQualifiedAnnotationBeanNameGenerator para este propósito.

Desde Spring Framework 7.0, si encuentras conflictos de nombres entre métodos @Bean en clases @Configuration, también puedes configurar un ConfigurationBeanNameGenerator que genere nombres únicos para los métodos @Bean.

FullyQualifiedConfigurationBeanNameGenerator puede usarse para generar nombres de bean predeterminados totalmente cualificados para métodos @Bean sin un atributo name explícito. Por ejemplo:

com.example.MyConfig.myBean

Este sería el nombre generado para un método @Bean llamado myBean() declarado en la clase @Configuration com.example.MyConfig.

FullyQualifiedAnnotationBeanNameGenerator y FullyQualifiedConfigurationBeanNameGenerator pertenecen al paquete org.springframework.context.annotation.

Como regla general, considera especificar el nombre mediante la anotación cuando otros componentes puedan hacer referencias explícitas a él. En cambio, los nombres generados automáticamente son adecuados cuando el contenedor se encarga de la conexión.

Proporcionar un ámbito para componentes detectados automáticamente

Como ocurre con los componentes gestionados por Spring en general, el ámbito predeterminado y más habitual de los componentes detectados automáticamente es singleton.

Sin embargo, a veces necesitas un ámbito distinto, que puedes especificar mediante la anotación @Scope. Puedes proporcionar el nombre del ámbito dentro de la anotación:

@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
	// ...
}

Las anotaciones @Scope solo se inspeccionan en la clase concreta del bean, para componentes anotados, o en el método de fábrica, para métodos @Bean.

A diferencia de las definiciones de beans XML, no existe el concepto de herencia de definiciones de beans, y las jerarquías de herencia a nivel de clase no son relevantes para los metadatos.

También puedes componer tus propias anotaciones de ámbito utilizando el enfoque de meta-anotaciones de Spring. Por ejemplo, una anotación personalizada meta-anotada con @Scope("prototype"), que podría declarar además un modo de proxy con ámbito personalizado.

Para proporcionar una estrategia personalizada de resolución de ámbito, en vez de depender del enfoque basado en anotaciones, puedes implementar la interfaz ScopeMetadataResolver. Incluye un constructor predeterminado sin argumentos y proporciona el nombre de clase totalmente cualificado al configurar el explorador:

@Configuration
@ComponentScan(
		basePackages = "org.example",
		scopeResolver = MyScopeResolver.class
)
public class AppConfig {
	// ...
}
<beans>
	<context:component-scan
			base-package="org.example"
			scope-resolver="org.example.MyScopeResolver"/>
</beans>

Cuando usas determinados ámbitos no singleton, puede ser necesario generar proxies para los objetos con ámbito. Para ello, está disponible el atributo scoped-proxy en el elemento component-scan.

Los tres valores posibles son no, interfaces y targetClass.

Por ejemplo, la siguiente configuración genera proxies dinámicos estándar de JDK:

@Configuration
@ComponentScan(
		basePackages = "org.example",
		scopedProxy = ScopedProxyMode.INTERFACES
)
public class AppConfig {
	// ...
}
<beans>
	<context:component-scan
			base-package="org.example"
			scoped-proxy="interfaces"/>
</beans>

Proporcionar metadatos de calificadores mediante anotaciones

La anotación @Qualifier se utiliza para ajustar con precisión la autoconexión basada en anotaciones mediante calificadores.

Los ejemplos de esa sección muestran el uso de @Qualifier y de anotaciones de calificador personalizadas para controlar detalladamente la resolución de candidatos de autoconexión. Como esos ejemplos se basan en definiciones de beans XML, los metadatos de calificador se proporcionan en las definiciones de beans candidatas mediante los elementos secundarios qualifier o meta del elemento bean.

Cuando dependes de la exploración del classpath para detectar automáticamente componentes, puedes proporcionar los metadatos de calificador con anotaciones de nivel de tipo en la clase candidata.

Los siguientes ejemplos muestran esta técnica:

@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
	// ...
}
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
	// ...
}
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
	// ...
}

Como sucede con la mayoría de alternativas basadas en anotaciones, los metadatos de anotación quedan vinculados a la propia definición de clase. En cambio, con XML puedes tener varios beans del mismo tipo con variaciones en sus metadatos de calificador, porque esos metadatos se proporcionan por instancia y no por clase.

Definir metadatos de bean dentro de componentes

Los componentes de Spring también pueden aportar metadatos de definiciones de beans al contenedor. Para ello, puedes utilizar la misma anotación @Bean que se emplea para definir metadatos de beans dentro de clases anotadas con @Configuration.

El siguiente ejemplo muestra cómo hacerlo:

@Component
public class FactoryMethodComponent {

	@Bean
	@Qualifier("public")
	public TestBean publicInstance() {
		return new TestBean("publicInstance");
	}

	public void doWork() {
		// Se omite la implementación del método del componente.
	}
}

La clase anterior es un componente de Spring que contiene código específico de la aplicación en su método doWork(). Sin embargo, también aporta una definición de bean con un método de fábrica que apunta al método publicInstance().

La anotación @Bean identifica el método de fábrica y otras propiedades de la definición de bean, como un valor de calificador mediante @Qualifier.

Otras anotaciones que se pueden especificar a nivel de método son @Scope, @Lazy y anotaciones de calificador personalizadas.

Además de su función para la inicialización de componentes, puedes colocar la anotación @Lazy en puntos de inyección marcados con @Autowired o @Inject. En ese contexto, provoca la inyección de un proxy de resolución diferida.

Sin embargo, este enfoque con proxy es bastante limitado. Para interacciones diferidas más sofisticadas, especialmente en combinación con dependencias opcionales, se recomienda ObjectProvider<MyTargetBean>.

Los campos y métodos autoconectados se admiten como se ha explicado anteriormente, junto con compatibilidad adicional para la autoconexión de métodos @Bean.

El siguiente ejemplo muestra cómo hacerlo:

@Component
public class FactoryMethodComponent {

	private static int i;

	@Bean
	@Qualifier("public")
	public TestBean publicInstance() {
		return new TestBean("publicInstance");
	}

	// Uso de un calificador personalizado y autoconexión de parámetros de método.
	@Bean
	protected TestBean protectedInstance(
			@Qualifier("public") TestBean spouse,
			@Value("#{privateInstance.age}") String country) {

		TestBean tb = new TestBean("protectedInstance", 1);
		tb.setSpouse(spouse);
		tb.setCountry(country);
		return tb;
	}

	@Bean
	private TestBean privateInstance() {
		return new TestBean("privateInstance", i++);
	}

	@Bean
	@RequestScope
	public TestBean requestScopedInstance() {
		return new TestBean("requestScopedInstance", 3);
	}
}

El ejemplo autoconecta el parámetro de método String llamado country con el valor de la propiedad age de otro bean llamado privateInstance.

Un elemento de Spring Expression Language define el valor de la propiedad mediante la notación #{ <expresión> }. Para las anotaciones @Value, hay un resolvedor de expresiones preconfigurado que busca nombres de beans al resolver el texto de la expresión.

Desde Spring Framework 4.3, también puedes declarar un parámetro de método de fábrica de tipo InjectionPoint —o su subclase más específica, DependencyDescriptor— para acceder al punto de inyección solicitante que desencadena la creación del bean actual.

Ten en cuenta que esto solo se aplica a la creación real de instancias de bean, no a la inyección de instancias ya existentes. Por ello, esta funcionalidad tiene más sentido para beans con ámbito prototype.

Para otros ámbitos, el método de fábrica solo ve el punto de inyección que desencadenó la creación de una nueva instancia de bean dentro del ámbito dado. Por ejemplo, la dependencia que desencadenó la creación de un singleton con inicialización diferida.

Puedes utilizar los metadatos del punto de inyección con cuidado semántico en estos escenarios:

@Component
public class FactoryMethodComponent {

	@Bean
	@Scope("prototype")
	public TestBean prototypeInstance(InjectionPoint injectionPoint) {
		return new TestBean(
				"prototypeInstance for " + injectionPoint.getMember());
	}
}

Los métodos @Bean de un componente normal de Spring se procesan de forma diferente a sus equivalentes dentro de una clase @Configuration de Spring.

La diferencia es que las clases @Component no se mejoran con CGLIB para interceptar invocaciones de métodos y campos. El proxy CGLIB es el mecanismo que permite que la invocación de métodos o campos dentro de métodos @Bean de clases @Configuration cree referencias de metadatos de bean a objetos colaboradores.

Estos métodos no se invocan con semántica Java normal, sino que pasan por el contenedor para proporcionar la gestión habitual del ciclo de vida y los proxies de los beans de Spring, incluso cuando hacen referencia a otros beans mediante llamadas programáticas a métodos @Bean.

En cambio, invocar un método o campo dentro de un método @Bean de una clase @Component simple conserva la semántica Java estándar, sin procesamiento especial de CGLIB ni otras restricciones.

Puedes declarar métodos @Bean como static, lo que permite llamarlos sin crear una instancia de la clase de configuración que los contiene. Esto tiene especial sentido al definir beans posprocesadores, por ejemplo, de tipo BeanFactoryPostProcessor o BeanPostProcessor, ya que estos beans se inicializan pronto en el ciclo de vida del contenedor y deberían evitar activar otras partes de la configuración en ese momento.

Las llamadas a métodos @Bean estáticos nunca son interceptadas por el contenedor, ni siquiera dentro de clases @Configuration, debido a limitaciones técnicas: la creación de subclases CGLIB solo puede sobrescribir métodos no estáticos.

Como consecuencia, una llamada directa a otro método @Bean tiene semántica Java estándar y devuelve una instancia independiente directamente desde el método de fábrica.

La visibilidad Java de los métodos @Bean no afecta de forma inmediata a la definición de bean resultante en el contenedor de Spring. Puedes declarar libremente los métodos de fábrica como consideres adecuado en clases que no sean @Configuration, así como métodos estáticos en cualquier lugar.

Sin embargo, los métodos @Bean normales de las clases @Configuration deben poder sobrescribirse; es decir, no deben declararse como private ni final.

Los métodos @Bean también se detectan en las clases base de una clase de componente o configuración concreta, así como en métodos default de Java declarados en interfaces implementadas por la clase de componente o configuración.

Esto permite mucha flexibilidad al componer configuraciones complejas, e incluso hace posible la herencia múltiple mediante métodos default de Java.

Por último, una sola clase puede contener varios métodos @Bean para el mismo bean, como una disposición de varios métodos de fábrica que se utilizarán según las dependencias disponibles en tiempo de ejecución.

Se aplica el mismo algoritmo que para elegir el constructor o método de fábrica más completo en otros escenarios de configuración: durante la creación se selecciona la variante con el mayor número de dependencias que puedan satisfacerse, de forma análoga a como el contenedor elige entre varios constructores @Autowired.