类路径扫描与托管组件

本章中的大多数示例都使用 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)提供的。