使用 Qualifiers 微调基于注解的自动装配

@Primary@Fallback 是在使用类型进行自动装配时,存在多个候选项且能够确定一个主选(或非回退)候选 bean 的有效方式。

当你需要对选择过程进行更多控制时,可以使用 Spring 的 @Qualifier 注解。你可以将限定符值与特定参数关联,从而缩小类型匹配的范围,以便为每个参数选择特定的 bean。在最简单的情况下,这可以是一个纯粹的描述性值,如下例所示

  • Java

  • Kotlin

public class MovieRecommender {

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

	// ...
}
class MovieRecommender {

	@Autowired
	@Qualifier("main")
	private lateinit var movieCatalog: MovieCatalog

	// ...
}

你也可以在单独的构造函数参数或方法参数上指定 @Qualifier 注解,如下例所示

  • Java

  • Kotlin

public class MovieRecommender {

	private final MovieCatalog movieCatalog;

	private final CustomerPreferenceDao customerPreferenceDao;

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

	// ...
}
class MovieRecommender {

	private lateinit var movieCatalog: MovieCatalog

	private lateinit var customerPreferenceDao: CustomerPreferenceDao

	@Autowired
	fun prepare(@Qualifier("main") movieCatalog: MovieCatalog,
				customerPreferenceDao: CustomerPreferenceDao) {
		this.movieCatalog = movieCatalog
		this.customerPreferenceDao = customerPreferenceDao
	}

	// ...
}

下例展示了相应的 bean 定义。

<?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"/> (1)

		<!-- inject any dependencies required by this bean -->
	</bean>

	<bean class="example.SimpleMovieCatalog">
		<qualifier value="action"/> (2)

		<!-- inject any dependencies required by this bean -->
	</bean>

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

</beans>
1 带有 main 限定符值的 bean 将与带有相同限定符值的构造函数参数进行自动装配。
2 带有 action 限定符值的 bean 将与带有相同限定符值的构造函数参数进行自动装配。

对于回退匹配,bean 名称被视为默认的限定符值。因此,你可以定义一个 idmain 的 bean,而不是使用嵌套的 qualifier 元素,这会产生相同的匹配结果。然而,尽管你可以使用这种约定按名称引用特定的 bean,但 @Autowired 本质上是关于类型驱动的注入,并带有可选的语义限定符。这意味着限定符值,即使是 bean 名称回退,也始终在类型匹配的集合内具有缩小范围的语义。它们在语义上不表示对唯一 bean id 的引用。好的限定符值如 mainEMEApersistent,它们表达了特定组件的特性,这些特性独立于 bean id,而 bean id 在匿名 bean 定义(如上例所示)的情况下可能是自动生成的。

限定符也适用于带类型的集合,如前所述——例如,对于 Set<MovieCatalog>。在这种情况下,根据声明的限定符匹配到的所有 bean 都将作为一个集合注入。这意味着限定符不必是唯一的。相反,它们构成过滤条件。例如,你可以定义多个 MovieCatalog bean,它们都具有相同的限定符值“action”,所有这些 bean 都将被注入到用 @Qualifier("action") 注解的 Set<MovieCatalog> 中。

让限定符值在类型匹配的候选项中根据目标 bean 名称进行选择,这在注入点不需要 @Qualifier 注解。如果没有其他的解析指示符(例如限定符或 primary 标记),对于非唯一依赖的情况,Spring 会将注入点名称(即字段名或参数名)与目标 bean 名称进行匹配,并选择名称相同的候选项(如果存在)(无论是通过 bean 名称还是关联的别名)。

从 6.1 版本开始,这需要存在 -parameters Java 编译器标志。从 6.2 版本开始,当参数名与 bean 名称匹配且没有类型、限定符或 primary 条件覆盖匹配时,容器会应用 bean 名称匹配的快速 shortcut 解析,绕过完整的类型匹配算法。因此,建议你的参数名与目标 bean 名称匹配。

作为按名称注入的一种替代方案,可以考虑 JSR-250 的 @Resource 注解,其语义定义是通过其唯一名称来标识特定的目标组件,而声明的类型与匹配过程无关。@Autowired 的语义则大不相同:在按类型选择候选 bean 后,指定的 String 限定符值仅在这些按类型选择的候选项中考虑(例如,将 account 限定符与标记有相同限定符标签的 bean 进行匹配)。

对于自身被定义为集合、Map 或数组类型的 bean,@Resource 是一个不错的解决方案,它通过唯一名称引用特定的集合或数组 bean。话虽如此,你也可以通过 Spring 的 @Autowired 类型匹配算法来匹配集合、Map 和数组类型,只要元素类型信息在 @Bean 返回类型签名或集合继承层级中得到保留。在这种情况下,你可以使用限定符值来选择同类型的集合,如前一段所述。

@Autowired 也考虑自身引用进行注入(即,引用回当前被注入的 bean)。有关详细信息,请参阅自身注入

@Autowired 适用于字段、构造函数和多参数方法,允许通过在参数级别使用限定符注解来缩小范围。相比之下,@Resource 仅支持字段和带单参数的 bean 属性 setter 方法。因此,如果你的注入目标是构造函数或多参数方法,你应该坚持使用限定符。

你可以创建自己的自定义限定符注解。为此,定义一个注解并在你的定义中提供 @Qualifier 注解,如下例所示

  • Java

  • Kotlin

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

	String value();
}
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class Genre(val value: String)

然后你可以在自动装配的字段和参数上提供自定义限定符,如下例所示

  • Java

  • Kotlin

public class MovieRecommender {

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

	private MovieCatalog comedyCatalog;

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

	// ...
}
class MovieRecommender {

	@Autowired
	@Genre("Action")
	private lateinit var actionCatalog: MovieCatalog

	private lateinit var comedyCatalog: MovieCatalog

	@Autowired
	fun setComedyCatalog(@Genre("Comedy") comedyCatalog: MovieCatalog) {
		this.comedyCatalog = comedyCatalog
	}

	// ...
}

接下来,你可以为候选 bean 定义提供信息。你可以在 <bean/> 标签下添加 <qualifier/> 标签作为子元素,然后指定 typevalue 来匹配你的自定义限定符注解。类型与注解的完全限定类名匹配。或者,为了方便,如果不存在名称冲突的风险,你可以使用短类名。下例展示了这两种方法

<?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"/>
		<!-- inject any dependencies required by this bean -->
	</bean>

	<bean class="example.SimpleMovieCatalog">
		<qualifier type="example.Genre" value="Comedy"/>
		<!-- inject any dependencies required by this bean -->
	</bean>

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

</beans>

类路径扫描和托管组件中,你可以看到一种基于注解的替代方案,用于在 XML 中提供限定符元数据。具体来说,请参阅使用注解提供限定符元数据

在某些情况下,使用不带值的注解可能就足够了。当注解具有更通用的用途,并且可以应用于几种不同类型的依赖项时,这会很有用。例如,你可能提供一个离线目录,在没有互联网连接时可以搜索它。首先,定义简单注解,如下例所示

  • Java

  • Kotlin

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {
}
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class Offline

然后将注解添加到需要自动装配的字段或属性上,如下例所示

  • Java

  • Kotlin

public class MovieRecommender {

	@Autowired
	@Offline (1)
	private MovieCatalog offlineCatalog;

	// ...
}
1 此行添加了 @Offline 注解。
class MovieRecommender {

	@Autowired
	@Offline (1)
	private lateinit var offlineCatalog: MovieCatalog

	// ...
}
1 此行添加了 @Offline 注解。

现在 bean 定义只需要一个 qualifier type,如下例所示

<bean class="example.SimpleMovieCatalog">
	<qualifier type="Offline"/> (1)
	<!-- inject any dependencies required by this bean -->
</bean>
1 此元素指定了限定符。

你还可以定义接受命名属性(除了或代替简单的 value 属性)的自定义限定符注解。如果需要在自动装配的字段或参数上指定多个属性值,则 bean 定义必须匹配所有这些属性值才能被视为自动装配的候选 bean。例如,考虑以下注解定义

  • Java

  • Kotlin

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

	String genre();

	Format format();
}
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class MovieQualifier(val genre: String, val format: Format)

在本例中,Format 是一个枚举类型,定义如下

  • Java

  • Kotlin

public enum Format {
	VHS, DVD, BLURAY
}
enum class Format {
	VHS, DVD, BLURAY
}

需要自动装配的字段使用自定义限定符进行注解,并包含两个属性的值:genreformat,如下例所示

  • Java

  • Kotlin

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;

	// ...
}
class MovieRecommender {

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

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

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

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

	// ...
}

最后,bean 定义应该包含匹配的限定符值。此示例还演示了你可以使用 bean 元属性(meta attributes)代替 <qualifier/> 元素。如果存在 <qualifier/> 元素及其属性,则它们优先,但如果没有这样的限定符,自动装配机制将回退到 <meta/> 标签中提供的值,如下例中的最后两个 bean 定义所示

<?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>
		<!-- inject any dependencies required by this bean -->
	</bean>

	<bean class="example.SimpleMovieCatalog">
		<qualifier type="MovieQualifier">
			<attribute key="format" value="VHS"/>
			<attribute key="genre" value="Comedy"/>
		</qualifier>
		<!-- inject any dependencies required by this bean -->
	</bean>

	<bean class="example.SimpleMovieCatalog">
		<meta key="format" value="DVD"/>
		<meta key="genre" value="Action"/>
		<!-- inject any dependencies required by this bean -->
	</bean>

	<bean class="example.SimpleMovieCatalog">
		<meta key="format" value="BLURAY"/>
		<meta key="genre" value="Comedy"/>
		<!-- inject any dependencies required by this bean -->
	</bean>

</beans>