Skip to content

使用限定符 (Qualifiers) 精细控制基于注解的自动装配

当可以确定一个首选 (Primary) 或非回退 (Non-fallback) 候选者时,@Primary@Fallback 是通过类型自动装配多个实例的有效方法。

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

java
public class MovieRecommender {

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

	// ...
}
kotlin
class MovieRecommender {

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

	// ...
}

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

java
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;
	}

	// ...
}
kotlin
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
<?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) -->

		<!-- 注入此 Bean 所需的任何依赖 -->
	</bean>

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

		<!-- 注入此 Bean 所需的任何依赖 -->
	</bean>

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

</beans>
  1. 具有 main 限定符值的 Bean 将与使用相同值限定的构造函数参数进行装配。
  2. 具有 action 限定符值的 Bean 将与使用相同值限定的构造函数参数进行装配。

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

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

提示

在类型匹配的候选者中,让限定符值根据目标 Bean 名称进行选择,并不一定需要在注入点使用 @Qualifier 注解。如果没有其他解析指示符(如限定符或 Primary 标记),对于非唯一依赖的情况,Spring 会将注入点名称(即字段名或参数名)与目标 Bean 名称进行匹配,并选择同名的候选者(按 Bean 名称或关联别名)。

自 6.1 版本起,这需要存在 -parameters Java 编译器标志。从 6.2 版本开始,容器会对 Bean 名称匹配应用快速简写解析(fast shortcut resolution),当参数名与 Bean 名称匹配且没有类型、限定符或 Primary 条件覆盖该匹配时,会绕过完整的类型匹配算法。因此建议你的参数名称与目标 Bean 名称保持一致

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

对于本身定义为集合、Map 或数组类型的 Bean,@Resource 是一个很好的解决方案,它可以通过唯一名称引用特定的集合或数组 Bean。也就是说,你也可以通过 Spring 的 @Autowired 类型匹配算法来匹配集合、Map 和数组类型,只要元素类型信息保留在 @Bean 返回类型签名或集合继承层次结构中。在这种情况下,你可以使用限定符值在相同类型的集合中进行选择,如前段所述。

@Autowired 也会考虑自我引用。详情请参阅自我注入

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

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

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

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

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

java
public class MovieRecommender {

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

	private MovieCatalog comedyCatalog;

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

	// ...
}
kotlin
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 定义提供信息。你可以将 <qualifier/> 标签添加为 <bean/> 标签的子元素,然后指定 typevalue 以匹配你的自定义限定符注解。type 与注解的全限定类名相匹配。或者,如果没有名称冲突的风险,为了方便起见,你可以使用简写的类名。以下示例演示了这两种方法:

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

	<bean class="example.SimpleMovieCatalog">
		<qualifier type="Genre" value="Action"/>
		<!-- 注入此 Bean 所需的任何依赖 -->
	</bean>

	<bean class="example.SimpleMovieCatalog">
		<qualifier type="example.Genre" value="Comedy"/>
		<!-- 注入此 Bean 所需的任何依赖 -->
	</bean>

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

</beans>

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

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

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

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

java
public class MovieRecommender {

	@Autowired
	@Offline // (1)
	private MovieCatalog offlineCatalog;

	// ...
}
kotlin
class MovieRecommender {

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

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

现在,Bean 定义只需要一个限定符 type,如下例所示:

xml
<bean class="example.SimpleMovieCatalog">
	<qualifier type="Offline"/> <!-- (1) -->
	<!-- 注入此 Bean 所需的任何依赖 -->
</bean>
  1. 该元素指定限定符。

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

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

	String genre();

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

在这种情况下,Format 是一个枚举,定义如下:

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

要自动装配的字段被标注了自定义限定符,并包含了两个属性的值:genreformat,如下例所示:

java
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;

	// ...
}
kotlin
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 元数据属性 (Bean meta attributes) 而不是 <qualifier/> 元素。如果存在 <qualifier/> 元素及其属性,它们将获得优先级;但如果没有此类限定符,则自动装配机制会回退到 <meta/> 标签中提供的值,如以下示例中的最后两个 Bean 定义所示:

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

	<bean class="example.SimpleMovieCatalog">
		<qualifier type="MovieQualifier">
			<attribute key="format" value="VHS"/>
			<attribute key="genre" value="Action"/>
		</qualifier>
		<!-- 注入此 Bean 所需的任何依赖 -->
	</bean>

	<bean class="example.SimpleMovieCatalog">
		<qualifier type="MovieQualifier">
			<attribute key="format" value="VHS"/>
			<attribute key="genre" value="Comedy"/>
		</qualifier>
		<!-- 注入此 Bean 所需 the 任何依赖 -->
	</bean>

	<bean class="example.SimpleMovieCatalog">
		<meta key="format" value="DVD"/>
		<meta key="genre" value="Action"/>
		<!-- 注入此 Bean 所需的任何依赖 -->
	</bean>

	<bean class="example.SimpleMovieCatalog">
		<meta key="format" value="BLURAY"/>
		<meta key="genre" value="Comedy"/>
		<!-- 注入此 Bean 所需的任何依赖 -->
	</bean>

</beans>

补充教学 —— 限定符的高阶玩法

1. @Qualifier 的“软引用”特质 初学者经常把 @Qualifier("beanId") 当成按 ID 找 Bean。 真相:它是按“标签”找。只不过如果没有显式标签,Bean 的 ID 会被当成一个默认标签。

  • 建议:给 Bean 打上语义化的标签(如 main, backup, retry),而不是死磕 ID。这样当你重命名 Bean ID 时,注入点不需要改动。

2. 消失的参数名匹配 (Spring 6.1+) 在以前,即使不加 @Qualifier,只要字段名或参数名写对,Spring 也能注进去。 变化:从 Spring 6.1 开始,Spring 不再尝试“猜测”被擦除后的 Java 参数名,除非你开启了 -parameters 编译 flag。

  • 最佳实践:为了代码的健壮性和可读性,手动加上 @Qualifier 永远是最安全的做法

3. 面对强迫症:自定义注解限定符 如果你讨厌在代码里写字符串魔术值(魔鬼字符串):

java
@Autowired @Qualifier("action") // 这种写法容易写错且不好重构

那就用文档里推荐的 自定义注解

java
@Autowired @Genre("Action") // 强类型,有代码提示,重构方便

4. @Qualifier 处理集合注入 当你注入 List<Service> 时,可以配合 @Qualifier 做二次过滤。 比如容器里有 10 个 Service,你只想注入标了 @Qualifier("important") 的那三个,Spring 也能完美支持。

5. @Resource vs @Autowired + @Qualifier 如果你只需要“按名字找唯一的那个 Bean”,用 @Resource(name="xxx") 更直接。 如果你需要的是“按类型找,并且通过一些业务标签来过滤”,那么 @Autowired + @Qualifier 是唯一选择。 另外,@Resource 不支持构造函数注入,这是它相对于 @Autowired 的一个劣势。

Based on Spring Framework.