使用 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>