類別路徑掃描與受管理組件

本章的大多數範例使用 XML 來指定組態中繼資料,這些中繼資料會在 Spring 容器中產生每個 BeanDefinition。前一節(基於註解的容器組態)示範如何透過原始碼層級註解提供許多組態中繼資料。然而,即使在這些範例中,「基本」bean 定義仍然在 XML 檔案中明確定義,而註解僅驅動依賴注入。本節描述一個選項,透過掃描類別路徑來隱含地偵測候選組件。候選組件是符合篩選條件的類別,並且在容器中註冊了對應的 bean 定義。這消除了使用 XML 執行 bean 註冊的需要。相反地,您可以使用註解 (例如,@Component)、AspectJ 類型運算式或您自己的自訂篩選條件來選擇哪些類別要註冊 bean 定義到容器中。

您可以使用 Java 而非 XML 檔案來定義 bean。請查看 @Configuration@Bean@Import@DependsOn 註解,以了解如何使用這些功能的範例。

@Component 和其他刻板印象註解

@Repository 註解是任何類別的標記,這些類別履行儲存庫的職責或刻板印象(也稱為資料存取物件或 DAO)。此標記的用途之一是自動翻譯例外,如例外翻譯中所述。

Spring 提供更多刻板印象註解:@Component@Service@Controller@Component 是任何 Spring 管理組件的通用刻板印象。@Repository@Service@Controller@Component 的特殊化,用於更特定的用例(分別在持久層、服務層和表示層)。因此,您可以使用 @Component 註解您的組件類別,但是,透過使用 @Repository@Service@Controller 註解它們,您的類別更適合由工具處理或與切面關聯。例如,這些刻板印象註解是切入點的理想目標。@Repository@Service@Controller 也可能在 Spring Framework 未來的版本中攜帶額外的語意。因此,如果您在為您的服務層選擇使用 @Component@Service 之間猶豫,@Service 顯然是更好的選擇。同樣地,如先前所述,@Repository 已被支援作為持久層中自動例外翻譯的標記。

使用 Meta-annotation 和組合註解

Spring 提供的許多註解可以用作您自己程式碼中的 meta-annotation。Meta-annotation 是可以應用於另一個註解的註解。例如,前面提到@Service 註解使用 @Component 進行 meta-annotation,如下例所示

  • Java

  • Kotlin

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

	// ...
}
1 @Component 導致 @Service 以與 @Component 相同的方式處理。
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Component (1)
annotation class Service {

	// ...
}
1 @Component 導致 @Service 以與 @Component 相同的方式處理。

您還可以組合 meta-annotation 來建立「組合註解」。例如,Spring MVC 中的 @RestController 註解由 @Controller@ResponseBody 組成。

此外,組合註解可以選擇性地重新宣告 meta-annotation 中的屬性以允許自訂。當您只想公開 meta-annotation 屬性的子集時,這可能特別有用。例如,Spring 的 @SessionScope 註解將作用域名稱硬編碼為 session,但仍然允許自訂 proxyMode。以下列表顯示了 SessionScope 註解的定義

  • Java

  • Kotlin

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

	/**
	 * Alias for {@link Scope#proxyMode}.
	 * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
	 */
	@AliasFor(annotation = Scope.class)
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}
@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Scope(WebApplicationContext.SCOPE_SESSION)
annotation class SessionScope(
		@get:AliasFor(annotation = Scope::class)
		val proxyMode: ScopedProxyMode = ScopedProxyMode.TARGET_CLASS
)

然後,您可以像以下範例所示,使用 @SessionScope 而無需宣告 proxyMode

  • Java

  • Kotlin

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

您也可以覆寫 proxyMode 的值,如下例所示

  • Java

  • Kotlin

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

有關更多詳細資訊,請參閱 Spring Annotation Programming Model wiki 頁面。

自動偵測類別並註冊 Bean 定義

Spring 可以自動偵測刻板印象類別,並在 ApplicationContext 中註冊對應的 BeanDefinition 實例。例如,以下兩個類別符合自動偵測的條件

  • Java

  • Kotlin

@Service
public class SimpleMovieLister {

	private MovieFinder movieFinder;

	public SimpleMovieLister(MovieFinder movieFinder) {
		this.movieFinder = movieFinder;
	}
}
@Service
class SimpleMovieLister(private val movieFinder: MovieFinder)
  • Java

  • Kotlin

@Repository
public class JpaMovieFinder implements MovieFinder {
	// implementation elided for clarity
}
@Repository
class JpaMovieFinder : MovieFinder {
	// implementation elided for clarity
}

若要自動偵測這些類別並註冊對應的 bean,您需要將 @ComponentScan 新增到您的 @Configuration 類別,其中 basePackages 屬性是這兩個類別的通用父套件。(或者,您可以指定逗號、分號或空格分隔的列表,其中包含每個類別的父套件。)

  • Java

  • Kotlin

@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
	// ...
}
@Configuration
@ComponentScan(basePackages = ["org.example"])
class AppConfig  {
	// ...
}
為了簡潔起見,先前的範例可以使用註解的 value 屬性(即 @ComponentScan("org.example"))。

以下替代方案使用 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>
使用 <context:component-scan> 隱含地啟用了 <context:annotation-config> 的功能。使用 <context:component-scan> 時,通常不需要包含 <context:annotation-config> 元素。

類別路徑套件的掃描需要在類別路徑中存在相應的目錄條目。當您使用 Ant 建置 JAR 時,請確保您未啟用 JAR 任務的僅限檔案開關。此外,類別路徑目錄可能無法根據某些環境中的安全策略公開 — 例如,JDK 1.7.0_45 及更高版本上的獨立應用程式(這需要在您的 manifest 中進行 'Trusted-Library' 設定 — 請參閱 stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。

在模組路徑(Java 模組系統)上,Spring 的類別路徑掃描通常按預期工作。但是,請確保您的組件類別在您的 module-info 描述符中已匯出。如果您希望 Spring 調用您類別的非公開成員,請確保它們是「opened」(即,它們在您的 module-info 描述符中使用 opens 宣告而不是 exports 宣告)。

此外,當您使用 component-scan 元素時,AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 都會隱含地包含在內。這表示這兩個組件會自動偵測並連接在一起 — 全部都無需在 XML 中提供任何 bean 組態中繼資料。

您可以透過包含 annotation-config 屬性並將值設為 false 來停用 AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 的註冊。

使用篩選器自訂掃描

預設情況下,使用 @Component@Repository@Service@Controller@Configuration 或本身使用 @Component 註解的自訂註解註解的類別是唯一偵測到的候選組件。但是,您可以透過應用自訂篩選器來修改和擴充此行為。將它們新增為 @ComponentScan 註解的 includeFiltersexcludeFilters 屬性(或 XML 組態中 <context:component-scan> 元素的 <context:include-filter /><context:exclude-filter /> 子元素)。每個篩選器元素都需要 typeexpression 屬性。下表描述了篩選選項

表 1. 篩選器類型
篩選器類型 範例運算式 描述

annotation (預設)

org.example.SomeAnnotation

要在目標組件的類型層級存在meta-present 的註解。

assignable

org.example.SomeClass

目標組件可指派給的類別(或介面)(擴充或實作)。

aspectj

org.example..*Service+

要由目標組件匹配的 AspectJ 類型運算式。

regex

org\.example\.Default.*

要由目標組件的類別名稱匹配的 regex 運算式。

custom

org.example.MyTypeFilter

org.springframework.core.type.TypeFilter 介面的自訂實作。

以下範例顯示了組態忽略所有 @Repository 註解並改用「stub」儲存庫

  • Java

  • Kotlin

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

以下列表顯示了等效的 XML

<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>
您也可以透過在註解上設定 useDefaultFilters=false 或提供 use-default-filters="false" 作為 <component-scan/> 元素的屬性來停用預設篩選器。這有效地停用了自動偵測使用 @Component@Repository@Service@Controller@RestController@Configuration 註解或 meta-annotation 的類別。

在組件中定義 Bean 中繼資料

Spring 組件也可以將 bean 定義中繼資料貢獻給容器。您可以使用與在 @Configuration 註解類別中定義 bean 中繼資料相同的 @Bean 註解來執行此操作。以下範例顯示了如何執行此操作

  • Java

  • Kotlin

@Component
public class FactoryMethodComponent {

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

	public void doWork() {
		// Component method implementation omitted
	}
}
@Component
class FactoryMethodComponent {

	@Bean
	@Qualifier("public")
	fun publicInstance() = TestBean("publicInstance")

	fun doWork() {
		// Component method implementation omitted
	}
}

先前的類別是一個 Spring 組件,在其 doWork() 方法中具有應用程式特定的程式碼。但是,它也貢獻了一個 bean 定義,該定義具有一個工廠方法,該方法引用了 publicInstance() 方法。@Bean 註解識別工廠方法和其他 bean 定義屬性,例如透過 @Qualifier 註解的限定詞值。可以指定的其他方法層級註解是 @Scope@Lazy 和自訂限定詞註解。

除了組件初始化的作用外,您還可以將 @Lazy 註解放在以 @Autowired@Inject 標記的注入點上。在這種情況下,它會導致注入延遲解析代理。但是,這種代理方法相當有限。對於複雜的延遲互動,特別是與可選相依性結合使用時,我們建議改用 ObjectProvider<MyTargetBean>

如先前討論,支援自動裝配欄位和方法,並額外支援自動裝配 @Bean 方法。以下範例顯示了如何執行此操作

  • Java

  • Kotlin

@Component
public class FactoryMethodComponent {

	private static int i;

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

	// use of a custom qualifier and autowiring of method parameters
	@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);
	}
}
@Component
class FactoryMethodComponent {

	companion object {
		private var i: Int = 0
	}

	@Bean
	@Qualifier("public")
	fun publicInstance() = TestBean("publicInstance")

	// use of a custom qualifier and autowiring of method parameters
	@Bean
	protected fun protectedInstance(
			@Qualifier("public") spouse: TestBean,
			@Value("#{privateInstance.age}") country: String) = TestBean("protectedInstance", 1).apply {
		this.spouse = spouse
		this.country = country
	}

	@Bean
	private fun privateInstance() = TestBean("privateInstance", i++)

	@Bean
	@RequestScope
	fun requestScopedInstance() = TestBean("requestScopedInstance", 3)
}

該範例將 String 方法參數 country 自動裝配到另一個名為 privateInstance 的 bean 上 age 屬性的值。Spring 運算式語言元素透過 #{ <expression> } 表示法定義屬性的值。對於 @Value 註解,運算式解析器已預先組態為在解析運算式文字時尋找 bean 名稱。

從 Spring Framework 4.3 開始,您也可以宣告 InjectionPoint 類型(或其更特定的子類別:DependencyDescriptor)的工廠方法參數,以存取觸發目前 bean 建立的要求注入點。請注意,這僅適用於 bean 實例的實際建立,而不適用於現有實例的注入。因此,此功能對於原型作用域的 bean 最有意義。對於其他作用域,工廠方法只會看到在給定作用域中觸發新 bean 實例建立的注入點(例如,觸發延遲單例 bean 建立的相依性)。在這種情況下,您可以謹慎地使用提供的注入點中繼資料。以下範例顯示了如何使用 InjectionPoint

  • Java

  • Kotlin

@Component
public class FactoryMethodComponent {

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

	@Bean
	@Scope("prototype")
	fun prototypeInstance(injectionPoint: InjectionPoint) =
			TestBean("prototypeInstance for ${injectionPoint.member}")
}

常規 Spring 組件中的 @Bean 方法的處理方式與 Spring @Configuration 類別中的對應方法不同。不同之處在於,@Component 類別不會使用 CGLIB 增強來攔截方法和欄位的調用。CGLIB 代理是透過這種方式,在 @Configuration 類別中的 @Bean 方法中調用方法或欄位會建立對協作物件的 bean 中繼資料參考。這些方法不是使用正常的 Java 語意調用,而是通過容器進行,以便提供 Spring bean 的通常生命週期管理和代理,即使透過程式化呼叫 @Bean 方法來引用其他 bean 也是如此。相反地,在普通 @Component 類別中的 @Bean 方法中調用方法或欄位具有標準 Java 語意,沒有特殊的 CGLIB 處理或其他約束適用。

您可以將 @Bean 方法宣告為 static,允許在不建立包含它們的組態類別作為實例的情況下調用它們。當定義後處理器 bean(例如,BeanFactoryPostProcessorBeanPostProcessor 類型)時,這尤其有意義,因為這些 bean 在容器生命週期的早期初始化,並且應避免在該點觸發組態的其他部分。

由於技術上的限制,呼叫靜態 @Bean 方法永遠不會被容器攔截,即使在 @Configuration 類別中也是如此(如本節稍早所述):CGLIB 子類化只能覆寫非靜態方法。因此,直接呼叫另一個 @Bean 方法具有標準 Java 語義,導致直接從 factory 方法本身傳回獨立的實例。

@Bean 方法的 Java 語言可見性對於 Spring 容器中產生的 bean 定義沒有直接影響。您可以自由地在非 @Configuration 類別中,以及在任何地方的靜態方法中,宣告您的 factory 方法,只要您認為合適即可。然而,@Configuration 類別中的一般 @Bean 方法需要是可覆寫的,也就是說,它們不能宣告為 privatefinal

在給定組件或配置類別的基底類別,以及組件或配置類別實作的介面中宣告的 Java 8 預設方法上,也會發現 @Bean 方法。這允許在組合複雜的配置安排方面具有很大的彈性,甚至可以透過 Spring 4.2 的 Java 8 預設方法實現多重繼承。

最後,單一類別可以為同一個 bean 保有多個 @Bean 方法,作為根據執行時期可用的依賴項來使用多個 factory 方法的安排。這與在其他配置情境中選擇「最貪婪」的建構子或 factory 方法的演算法相同:具有最多可滿足依賴項的變體會在建構時被選中,這類似於容器如何在多個 @Autowired 建構子之間進行選擇。

命名自動偵測到的組件

當組件作為掃描過程的一部分被自動偵測到時,其 bean 名稱由該掃描器已知的 BeanNameGenerator 策略產生。

預設情況下,使用 AnnotationBeanNameGenerator。對於 Spring stereotype annotations,如果您透過註解的 value 屬性提供名稱,則該名稱將用作相應 bean 定義中的名稱。當使用以下 JSR-250 和 JSR-330 註解而不是 Spring stereotype annotations 時,此慣例也適用:@jakarta.annotation.ManagedBean@javax.annotation.ManagedBean@jakarta.inject.Named@javax.inject.Named

從 Spring Framework 6.1 開始,不再需要使用名為 value 的註解屬性來指定 bean 名稱。自訂 stereotype annotations 可以宣告具有不同名稱(例如 name)的屬性,並使用 @AliasFor(annotation = Component.class, attribute = "value") 註解該屬性。有關具體範例,請參閱 ControllerAdvice#name() 的原始碼宣告。

從 Spring Framework 6.1 開始,對基於慣例的 stereotype 名稱的支援已被棄用,並將在框架的未來版本中移除。因此,自訂 stereotype annotations 必須使用 @AliasFor 來宣告 @Componentvalue 屬性的明確別名。有關具體範例,請參閱 Repository#value()ControllerAdvice#name() 的原始碼宣告。

如果無法從此類註解或任何其他偵測到的組件(例如透過自訂過濾器發現的組件)衍生出明確的 bean 名稱,則預設的 bean 名稱產生器會傳回未大寫的首字母且非限定的類別名稱。例如,如果偵測到以下組件類別,則名稱將為 myMovieListermovieFinderImpl

  • Java

  • Kotlin

@Service("myMovieLister")
public class SimpleMovieLister {
	// ...
}
@Service("myMovieLister")
class SimpleMovieLister {
	// ...
}
  • Java

  • Kotlin

@Repository
public class MovieFinderImpl implements MovieFinder {
	// ...
}
@Repository
class MovieFinderImpl : MovieFinder {
	// ...
}

如果您不想依賴預設的 bean 命名策略,您可以提供自訂的 bean 命名策略。首先,實作 BeanNameGenerator 介面,並確保包含預設的無參數建構子。然後,在配置掃描器時提供完全限定的類別名稱,如下面的註解和 bean 定義範例所示。

如果您由於多個自動偵測到的組件具有相同的非限定類別名稱(即,名稱相同但位於不同套件中的類別)而遇到命名衝突,您可能需要配置一個 BeanNameGenerator,該產生器預設使用完全限定的類別名稱作為產生的 bean 名稱。位於套件 org.springframework.context.annotation 中的 FullyQualifiedAnnotationBeanNameGenerator 可用於此類用途。
  • Java

  • Kotlin

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

作為一般規則,當其他組件可能正在明確參考它時,請考慮使用註解指定名稱。另一方面,當容器負責組裝時,自動產生的名稱就足夠了。

為自動偵測到的組件提供 Scope

與 Spring 管理的組件通常一樣,自動偵測到的組件的預設和最常見的 scope 是 singleton。但是,有時您需要不同的 scope,可以透過 @Scope 註解來指定。您可以在註解中提供 scope 的名稱,如下面的範例所示

  • Java

  • Kotlin

@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
	// ...
}
@Scope("prototype")
@Repository
class MovieFinderImpl : MovieFinder {
	// ...
}
@Scope 註解僅在具體的 bean 類別(對於註解組件)或 factory 方法(對於 @Bean 方法)上進行內省。與 XML bean 定義相反,沒有 bean 定義繼承的概念,並且類別層次的繼承階層與元數據目的無關。

有關 Spring context 中特定於 web 的 scope 的詳細資訊,例如「request」或「session」,請參閱 Request、Session、Application 和 WebSocket Scopes。與這些 scope 的預建註解一樣,您也可以透過使用 Spring 的元註解方法來組合您自己的 scope 註解:例如,使用 @Scope("prototype") 進行元註解的自訂註解,可能還宣告自訂的 scoped-proxy 模式。

要提供自訂的 scope 解析策略,而不是依賴基於註解的方法,您可以實作 ScopeMetadataResolver 介面。請務必包含預設的無參數建構子。然後,您可以在配置掃描器時提供完全限定的類別名稱,如下面的註解和 bean 定義範例所示
  • Java

  • Kotlin

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

當使用某些非 singleton scope 時,可能需要為 scoped 物件產生代理。原因在 Scoped Beans as Dependencies 中進行了描述。為此,component-scan 元素上提供了一個 scoped-proxy 屬性。三個可能的值是:nointerfacestargetClass。例如,以下配置會產生標準的 JDK 動態代理

  • Java

  • Kotlin

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

使用註解提供 Qualifier 元數據

@Qualifier 註解在 使用 Qualifiers 微調基於註解的自動裝配 中進行了討論。該節中的範例示範了 @Qualifier 註解和自訂 qualifier 註解的使用,以便在解析自動裝配候選者時提供細緻的控制。由於這些範例基於 XML bean 定義,因此 qualifier 元數據是透過使用 XML 中 bean 元素的 qualifiermeta 子元素在候選 bean 定義上提供的。當依靠類別路徑掃描來自動偵測組件時,您可以使用候選類別上的類型層級註解來提供 qualifier 元數據。以下三個範例示範了此技術

  • Java

  • Kotlin

@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
	// ...
}
@Component
@Qualifier("Action")
class ActionMovieCatalog : MovieCatalog
  • Java

  • Kotlin

@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
	// ...
}
@Component
@Genre("Action")
class ActionMovieCatalog : MovieCatalog {
	// ...
}
  • Java

  • Kotlin

@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
	// ...
}
@Component
@Offline
class CachingMovieCatalog : MovieCatalog {
	// ...
}
與大多數基於註解的替代方案一樣,請記住,註解元數據綁定到類別定義本身,而 XML 的使用允許同一類型的多個 bean 提供其 qualifier 元數據的變體,因為該元數據是按實例而不是按類別提供的。