類路徑掃描與託管元件

本章中的大多數示例都使用 XML 來指定在 Spring 容器中生成每個 BeanDefinition 的配置元資料。上一節(基於註解的容器配置)演示瞭如何透過原始碼級註解提供許多配置元資料。然而,即使在這些示例中,“基礎”bean 定義也是在 XML 檔案中顯式定義的,而註解僅用於驅動依賴注入。本節描述了一種透過掃描類路徑隱式檢測候選元件的選項。候選元件是符合過濾條件並已在容器中註冊相應 bean 定義的類。這消除了使用 XML 執行 bean 註冊的需要。相反,您可以使用註解(例如,@Component)、AspectJ 型別表示式或您自己的自定義過濾條件來選擇哪些類需要在容器中註冊 bean 定義。

您可以使用 Java 而不是 XML 檔案來定義 bean。請檢視 @Configuration@Bean@Import@DependsOn 註解,瞭解如何使用這些功能的示例。

@Component 及其他型別註解(Stereotype Annotations)

@Repository 註解是任何扮演倉庫(Repository)角色或型別(stereotype)(也稱為資料訪問物件或 DAO)的類的標記。該標記的用途之一是異常的自動轉換,如異常轉換中所述。

Spring 提供了其他型別註解:@Component@Service@Controller@Component 是任何 Spring 託管元件的通用型別。@Repository@Service@Controller@Component 的特殊化,用於更具體的用例(分別在持久層、服務層和表現層)。因此,您可以使用 @Component 為您的元件類添加註解,但透過使用 @Repository@Service@Controller 代替,您的類更適合由工具處理或與切面關聯。例如,這些型別註解是切入點(pointcuts)的理想目標。在 Spring Framework 的未來版本中,@Repository@Service@Controller 也可能帶有額外的語義。因此,如果您在為服務層選擇使用 @Component@Service,顯然 @Service 是更好的選擇。同樣,如前所述,@Repository 已被支援作為持久層中自動異常轉換的標記。

使用元註解(Meta-annotations)和組合註解(Composed Annotations)

Spring 提供的許多註解可以在您自己的程式碼中用作元註解。元註解是可以應用於其他註解的註解。例如,前面提到的 @Service 註解透過 @Component 進行了元註解,如下例所示

  • 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 相同的方式處理。

您還可以組合元註解來建立“組合註解”。例如,Spring MVC 中的 @RestController 註解由 @Controller@ResponseBody 組合而成。

此外,組合註解可以選擇性地重新宣告元註解的屬性以允許定製。當您只想暴露元註解屬性的一個子集時,這會特別有用。例如,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 可以自動檢測型別(stereotyped)類並將相應的 BeanDefinition 例項註冊到 ApplicationContext 中。例如,以下兩個類符合自動檢測條件

  • 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,您需要在您的 @Configuration 類中新增 @ComponentScan,其中 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 任務的 files-only 開關。此外,在某些環境下,類路徑目錄可能不會因安全策略而暴露——例如,JDK 1.7.0_45 及更高版本上的獨立應用程式(這需要在清單中進行 'Trusted-Library' 設定——參見 stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。

在模組路徑(Java 模組系統)上,Spring 的類路徑掃描通常按預期工作。但是,請確保您的元件類已在 module-info 描述符中匯出。如果您期望 Spring 呼叫您的類的非公共成員,請確保它們已“開放”(即在您的 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

在目標元件的型別級別存在元存在的註解。

assignable

org.example.SomeClass

目標元件可賦值給的類(或介面)(即繼承或實現)。

aspectj

org.example..*Service+

與目標元件匹配的 AspectJ 型別表示式。

regex

org\.example\.Default.*

與目標元件類名匹配的正則表示式。

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 或在 <component-scan/> 元素中提供屬性 use-default-filters="false" 來停用預設過濾器。這會有效地停用對使用 @Component@Repository@Service@Controller@RestController@Configuration 註解或元註解的類的自動檢測。

在元件中定義 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 Language 元素透過符號 #{ <expression> } 定義屬性的值。對於 @Value 註解,預配置了一個表示式解析器,用於在解析表示式文字時查詢 bean 名稱。

從Spring Framework 4.3開始,你也可以宣告一個型別為 InjectionPoint (或其更具體的子類 DependencyDescriptor) 的工廠方法引數,以訪問觸發當前bean建立的請求注入點。請注意,這僅適用於bean例項的實際建立,不適用於現有例項的注入。因此,此功能對於原型(prototype)作用域的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語義,會直接從工廠方法本身返回一個獨立的例項。

@Bean 方法的Java語言可見性對Spring容器中生成的bean定義沒有直接影響。在非 @Configuration 類中,你可以根據需要自由地宣告你的工廠方法,靜態方法在任何地方都可以這樣宣告。然而,@Configuration 類中的常規 @Bean 方法需要是可覆蓋的 — 也就是說,它們不能被宣告為 privatefinal

在給定元件或配置類的基類上,以及在由元件或配置類實現的介面中宣告的Java 8預設方法上,也會發現 @Bean 方法。這使得組合複雜的配置安排具有很大的靈活性,從Spring 4.2開始,透過Java 8預設方法甚至可以實現多重繼承。

最後,單個類可以為同一個bean包含多個 @Bean 方法,作為根據執行時可用依賴項選擇使用的多個工廠方法的安排。這與在其他配置場景中選擇“最貪婪”的建構函式或工廠方法的演算法相同:在構造時選擇具有最多可滿足依賴項的變體,這類似於容器如何在多個 @Autowired 建構函式之間進行選擇。

命名自動檢測到的元件

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

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

從Spring Framework 6.1開始,用於指定bean名稱的註解屬性名稱不再強制要求是 value。自定義stereotype註解可以宣告一個具有不同名稱(例如 name)的屬性,並使用 @AliasFor(annotation = Component.class, attribute = "value") 註解該屬性。具體示例請參見 ControllerAdvice#name() 的原始碼宣告。

從Spring Framework 6.1開始,對基於約定的stereotype名稱的支援已被棄用,並將在未來的框架版本中移除。因此,自定義stereotype註解必須使用 @AliasFor 來為 @Component 中的 value 屬性宣告一個顯式別名。具體示例請參見 Repository#value()ControllerAdvice#name() 的原始碼宣告。

如果無法從此類註解或任何其他檢測到的元件(例如透過自定義過濾器發現的元件)派生出顯式bean名稱,則預設bean名稱生成器返回類名的非限定(non-qualified)且首字母小寫的名稱。例如,如果檢測到以下元件類,名稱將是 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 介面,並確保包含一個預設的無參建構函式。然後,在配置掃描器時提供完全限定類名(fully qualified class name),如下面的示例註解和bean定義所示。

如果由於多個自動檢測到的元件具有相同的非限定類名(即,名稱相同但位於不同包中的類)而遇到命名衝突,你可能需要配置一個預設使用完全限定類名作為生成的bean名稱的 BeanNameGenerator。位於 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>

一般來說,當其他元件可能顯式引用該bean時,請考慮透過註解指定其名稱。另一方面,當容器負責注入(wiring)時,自動生成的名稱就足夠了。

為自動檢測到的元件提供作用域

與一般的Spring管理元件一樣,自動檢測到的元件的預設和最常見作用域是 singleton。然而,有時你需要不同的作用域,這可以透過 @Scope 註解來指定。你可以在註解中提供作用域的名稱,如下面的示例所示

  • Java

  • Kotlin

@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
	// ...
}
@Scope("prototype")
@Repository
class MovieFinderImpl : MovieFinder {
	// ...
}
@Scope 註解僅在具體的bean類(對於有註解的元件)或工廠方法(對於 @Bean 方法)上進行自省(introspected)。與XML bean定義不同,沒有bean定義繼承的概念,類級別的繼承層次結構與元資料目的無關。

有關Spring上下文中“request”或“session”等Web特定作用域的詳細資訊,請參見 Request, Session, Application, and WebSocket Scopes。與那些作用域的預置註解一樣,你也可以使用Spring的元註解方法來組合自己的作用域註解:例如,一個使用 @Scope("prototype") 作為元註解的自定義註解,可能還會宣告自定義的作用域代理模式。

為了提供自定義的作用域解析策略而不是依賴基於註解的方法,你可以實現 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>

當使用某些非單例作用域時,可能需要為帶作用域的物件生成代理。其原因在 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 註解在 Fine-tuning Annotation-based Autowiring with Qualifiers 中進行了討論。該節中的示例展示瞭如何使用 @Qualifier 註解和自定義限定符註解,以便在解析自動注入(autowire)候選者時提供細粒度控制。由於這些示例基於XML bean定義,限定符元資料是透過在XML中使用 bean 元素的 qualifiermeta 子元素提供給候選bean定義的。當依賴於classpath掃描進行元件的自動檢測時,你可以透過在候選類上使用型別級註解來提供限定符元資料。以下三個示例演示了此技術

  • 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提供不同的限定符元資料,因為該元資料是按例項(per-instance)而不是按類(per-class)提供的。