Fine Tuning Annotation Based Autowiring With Qualifiers

Ajustar con precisión la autoconexión basada en anotaciones mediante calificadores

@Primary y @Fallback son formas eficaces de utilizar la autoconexión por tipo cuando existen varias instancias y puede determinarse un candidato principal —o uno que no sea de reserva—.

Cuando necesitas más control sobre el proceso de selección, puedes usar la anotación @Qualifier de Spring. Puedes asociar valores de calificador a argumentos concretos, restringiendo el conjunto de coincidencias por tipo para que se elija un bean específico para cada argumento.

En el caso más sencillo, puede ser un valor descriptivo simple:

public class MovieRecommender {

	@Autowired
	@Qualifier("main")
	private MovieCatalog movieCatalog;

	// ...
}

También puedes especificar @Qualifier en argumentos individuales de un constructor o en parámetros de métodos:

public class MovieRecommender {

	private MovieCatalog movieCatalog;

	private CustomerPreferenceDao customerPreferenceDao;

	@Autowired
	public void prepare(@Qualifier("main") MovieCatalog movieCatalog,
			CustomerPreferenceDao customerPreferenceDao) {
		this.movieCatalog = movieCatalog;
		this.customerPreferenceDao = customerPreferenceDao;
	}

	// ...
}

El siguiente ejemplo muestra las definiciones de beans correspondientes:

<?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:annotation-config/>

	<bean class="example.SimpleMovieCatalog">
		<qualifier value="main"/>
		<!-- Inyecta las dependencias que requiera este bean. -->
	</bean>

	<bean class="example.SimpleMovieCatalog">
		<qualifier value="action"/>
		<!-- Inyecta las dependencias que requiera este bean. -->
	</bean>

	<bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>

El bean con el valor de calificador main se conecta con el argumento que tiene el mismo valor de calificador. Del mismo modo, el bean con el valor action se conecta con el argumento calificado con ese valor.

Como coincidencia de reserva, el nombre del bean se considera un valor de calificador predeterminado. Por tanto, puedes definir el bean con el identificador main en lugar de utilizar el elemento qualifier anidado y obtener el mismo resultado.

Sin embargo, aunque esta convención permite referirse a beans concretos por nombre, @Autowired se basa fundamentalmente en inyección guiada por tipo con calificadores semánticos opcionales.

Esto significa que los valores de calificador, incluso cuando se recurre al nombre del bean, siempre restringen el conjunto de coincidencias por tipo. No expresan semánticamente una referencia a un id único de bean.

Buenos valores de calificador son main, EMEA o persistent, ya que expresan características de un componente específico independientes del identificador del bean, que puede generarse automáticamente en el caso de una definición anónima como la del ejemplo anterior.

Los calificadores también se aplican a colecciones tipadas, por ejemplo, a Set<MovieCatalog>. En ese caso, todos los beans que coincidan con los calificadores declarados se inyectan como colección.

Por tanto, los calificadores no tienen que ser únicos: constituyen criterios de filtrado. Por ejemplo, puedes definir varios beans MovieCatalog con el mismo valor de calificador action; todos ellos se inyectan en un Set<MovieCatalog> anotado con @Qualifier("action").

Permitir que los valores de calificador seleccionen respecto de los nombres de beans de destino, dentro de los candidatos coincidentes por tipo, no requiere una anotación @Qualifier en el punto de inyección.

Si no existe otro indicador de resolución —como un calificador, una marca de principal o una marca de reserva—, en una situación de dependencia no única Spring compara el nombre del punto de inyección, es decir, el nombre del campo o parámetro, con los nombres de bean de destino. Si existe un candidato con el mismo nombre —ya sea por nombre del bean o por un alias asociado—, lo selecciona.

Desde la versión 6.1, esto requiere que esté presente la opción del compilador Java -parameters. Desde la versión 6.2, el contenedor aplica una resolución rápida para las coincidencias por nombre de bean: omite el algoritmo completo de coincidencia por tipo cuando el nombre del parámetro coincide con el nombre del bean y ninguna condición de tipo, calificador, principal o reserva anula la coincidencia. Por ello, es recomendable que los nombres de los parámetros coincidan con los nombres de bean de destino.

Como alternativa para la inyección por nombre, considera la anotación JSR-250 @Resource, cuya semántica identifica un componente de destino concreto por su nombre único; el tipo declarado no interviene en el proceso de coincidencia.

@Autowired tiene una semántica bastante distinta: después de seleccionar los beans candidatos por tipo, el valor String del calificador se considera únicamente dentro de esos candidatos seleccionados por tipo; por ejemplo, haciendo coincidir el calificador account con beans marcados con esa misma etiqueta.

Para los beans que se definen como colección, Map o array, @Resource es una buena opción para referirse a la colección o array específico por nombre único. Aun así, también puedes hacer coincidir tipos de colección, Map y array mediante el algoritmo de coincidencia por tipo de @Autowired, siempre que la información del tipo de elemento se conserve en las firmas de retorno de @Bean o en las jerarquías de herencia de colecciones.

En ese caso, puedes usar valores de calificador para seleccionar entre colecciones del mismo tipo.

@Autowired también considera autorreferencias para la inyección, es decir, referencias al bean que se está inyectando actualmente.

@Autowired se aplica a campos, constructores y métodos con varios argumentos, y permite restringir mediante anotaciones de calificador a nivel de parámetro. En cambio, @Resource solo se admite en campos y métodos setter de propiedades con un solo argumento. Por ello, debes usar calificadores cuando el destino de inyección sea un constructor o un método con varios argumentos.

Crear anotaciones de calificador personalizadas

Puedes crear tus propias anotaciones de calificador. Para ello, define una anotación e incluye @Qualifier en su definición:

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Genre {

	String value();
}

Después puedes utilizar el calificador personalizado en campos y parámetros autoconectados:

public class MovieRecommender {

	@Autowired
	@Genre("Action")
	private MovieCatalog actionCatalog;

	private MovieCatalog comedyCatalog;

	@Autowired
	public void setComedyCatalog(@Genre("Comedy") MovieCatalog comedyCatalog) {
		this.comedyCatalog = comedyCatalog;
	}

	// ...
}

A continuación, puedes proporcionar la información para las definiciones de beans candidatos. Puedes añadir etiquetas <qualifier/> como subelementos de <bean/> y especificar el type y el value que deben coincidir con las anotaciones de calificador personalizadas.

El tipo se compara con el nombre de clase totalmente cualificado de la anotación. Como alternativa, si no existe riesgo de conflicto de nombres, puedes utilizar el nombre corto de la clase. El siguiente ejemplo muestra ambos enfoques:

<?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:annotation-config/>

	<bean class="example.SimpleMovieCatalog">
		<qualifier type="Genre" value="Action"/>
		<!-- Inyecta las dependencias que requiera este bean. -->
	</bean>

	<bean class="example.SimpleMovieCatalog">
		<qualifier type="example.Genre" value="Comedy"/>
		<!-- Inyecta las dependencias que requiera este bean. -->
	</bean>

	<bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>

También existe una alternativa basada en anotaciones para proporcionar los metadatos de calificadores, dentro de la exploración del classpath y de los componentes gestionados.

Calificadores sin valor

En algunos casos, puede bastar con usar una anotación sin valor. Esto resulta útil cuando la anotación tiene un propósito más general y puede aplicarse a varios tipos de dependencias.

Por ejemplo, puedes proporcionar un catálogo sin conexión que pueda consultarse cuando no haya conexión a Internet. Primero, define una anotación simple:

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {
}

Después añade la anotación al campo o propiedad que se va a autoconectar:

public class MovieRecommender {

	@Autowired
	@Offline
	private MovieCatalog offlineCatalog;

	// ...
}

Ahora la definición del bean solo necesita un calificador type:

<bean class="example.SimpleMovieCatalog">
	<qualifier type="Offline"/>
	<!-- Inyecta las dependencias que requiera este bean. -->
</bean>

Calificadores personalizados con varios atributos

También puedes definir anotaciones de calificador personalizadas que acepten atributos con nombre además del atributo simple value, o en lugar de este.

Si se especifican varios valores de atributos en un campo o parámetro que se va a autoconectar, una definición de bean debe coincidir con todos ellos para considerarse candidata de autoconexión.

Por ejemplo, considera la siguiente definición de anotación:

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {

	String genre();

	Format format();
}

En este caso, Format es una enumeración:

public enum Format {
	VHS, DVD, BLURAY
}

Los campos que se van a autoconectar se anotan con el calificador personalizado e incluyen valores para los dos atributos, genre y format:

public class MovieRecommender {

	@Autowired
	@MovieQualifier(format = Format.VHS, genre = "Action")
	private MovieCatalog actionVhsCatalog;

	@Autowired
	@MovieQualifier(format = Format.VHS, genre = "Comedy")
	private MovieCatalog comedyVhsCatalog;

	@Autowired
	@MovieQualifier(format = Format.DVD, genre = "Action")
	private MovieCatalog actionDvdCatalog;

	@Autowired
	@MovieQualifier(format = Format.BLURAY, genre = "Comedy")
	private MovieCatalog comedyBluRayCatalog;

	// ...
}

Por último, las definiciones de beans deben contener valores de calificador coincidentes. Este ejemplo también muestra que puedes usar atributos meta de bean en lugar de elementos <qualifier/>.

Cuando está disponible, el elemento <qualifier/> y sus atributos tienen prioridad. No obstante, el mecanismo de autoconexión recurre a los valores proporcionados por las etiquetas <meta/> si no existe un calificador, como ocurre con las dos últimas definiciones de beans del siguiente ejemplo:

<?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:annotation-config/>

	<bean class="example.SimpleMovieCatalog">
		<qualifier type="MovieQualifier">
			<attribute key="format" value="VHS"/>
			<attribute key="genre" value="Action"/>
		</qualifier>
		<!-- Inyecta las dependencias que requiera este bean. -->
	</bean>

	<bean class="example.SimpleMovieCatalog">
		<qualifier type="MovieQualifier">
			<attribute key="format" value="VHS"/>
			<attribute key="genre" value="Comedy"/>
		</qualifier>
		<!-- Inyecta las dependencias que requiera este bean. -->
	</bean>

	<bean class="example.SimpleMovieCatalog">
		<meta key="format" value="DVD"/>
		<meta key="genre" value="Action"/>
		<!-- Inyecta las dependencias que requiera este bean. -->
	</bean>

	<bean class="example.SimpleMovieCatalog">
		<meta key="format" value="BLURAY"/>
		<meta key="genre" value="Comedy"/>
		<!-- Inyecta las dependencias que requiera este bean. -->
	</bean>

</beans>