Classpath Scanning and Managed Components

本章中的大多数示例都使用 XML 来指定在 Spring 容器中产生每个 BeanDefinition 的配置元数据。前面的部分 (Annotation-based Container Configuration) 演示了如何通过源级注释提供许多配置元数据。但是,即使在那些示例中,“基础”Bean 定义仍然在 XML 文件中显式定义,而注释仅驱动依赖注入。本部分描述了一种通过扫描类路径来隐式检测候选组件的选项。候选组件是与筛选标准匹配并在容器中注册了相应 Bean 定义的类。这消除了使用 XML 来执行 Bean 注册的需要。相反,你可以使用注释(例如,@Component)、AspectJ 类型表达式或你自己的自定义筛选标准来选择向容器注册 Bean 定义的类。

可以使用 Java 而不是 XML 文件来定义 bean。请查看 @Configuration@Bean@Import@DependsOn 注释,了解如何使用这些功能的示例。

@Component and Further Stereotype Annotations

"@49" 注解是任何实现存储库 (也称为数据访问对象或 DAO) 的角色或 "@50" 的类的标记。这个标记的用法之一是自动转换异常,如 "@51" 中所述。

Spring 提供了更多陈规定型注释:@Component@Service@Controller@Component 是任何 Spring 管理组件的通用陈规定型。@Repository@Service@Controller@Component 在更具体的用例(分别在持久化、服务和表示层)中的专业化。因此,可以使用 @Component 为组件类加上注释,但通过使用 @Repository@Service@Controller 为它们加上注释,这些类更适合由工具处理或与切面关联。例如,这些陈规定型注释是切入点的理想目标。@Repository@Service@Controller 在 Spring Framework 的未来版本中也可能包含其他语义。因此,如果你在为服务层选择使用 @Component@Service 之间进行选择,@Service 明显是更好的选择。与此类似,如前所述,@Repository 已被支持为持久层中自动异常翻译的标记。

Using Meta-annotations and Composed Annotations

Spring 提供的许多注解都可以作为你自己的代码中的元注解来使用。元注解是可以应用于另一个注解的注解。例如,"@52" 注解在 "@54" 中被提及,它被 "@53" 元注解,如下面的示例所示:

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

	// ...
}
1 @Component 使得 @Service@Component 中被以相同的方式处理。
Kotlin
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Component (1)
annotation class Service {

	// ...
}
2 @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 注释编程模型wiki 页面。

Automatically Detecting Classes and Registering Bean Definitions

Spring 可以自动检测陈规定型类,并将相应的 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,需要将 @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 及更高版本上的独立应用程序(这需要在清单中设置“受信任库”——请参阅 https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。 在 JDK 9 的模块路径(拼图)上,Spring 的类路径扫描通常按预期工作。但是,请确保在 module-info 描述符中导出了组件类。如果你希望 Spring 调用类的非公共成员,请确保它们已“打开”(即,它们在 module-info 描述符中使用 opens 声明而不是 exports 声明)。

此外,当你使用 component-scan 元素时,AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 都隐含包含。这意味着这两个组件会被自动检测,并连接起来,而无需在 XML 中提供任何 bean 配置元数据。

你可以通过包括 annotation-config 属性将 AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 的注册禁用,属性值设为 false

Using Filters to Customize Scanning

默认情况下,只有用 @Component@Repository@Service@Controller@Configuration 或自定义注解(其中注解本身用 @Component 标记)标记的类才是唯一可以检测到的候选组件。但是,你可以通过应用自定义过滤器来修改和扩展此行为。将它们添加为 @ComponentScan 注解的 includeFiltersexcludeFilters 属性(或作为 <context:component-scan> 元素在 XML 配置中的 <context:include-filter /><context:exclude-filter /> 子元素)。每个过滤器元素都需要 typeexpression 属性。下表描述了筛选选项:

Table 1. Filter Types
Filter Type Example Expression Description

annotation (default)

org.example.SomeAnnotation

要在目标组件中 presentmeta-present 的类型级别的注释。

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 或将 use-default-filters="false" 提供为 <component-scan/> 元素的属性来禁用默认过滤器。这有效地禁用了带 @Component@Repository@Service@Controller@RestController@Configuration 进行注释或元注释的类的自动检测。

Defining Bean Metadata within Components

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 会在容器生命周期的早期初始化,并且应该避免在此阶段触发配置的其他部分。 对 static @Bean 方法的调用永远不会被容器拦截,即使在 @Configuration 类中也是如此(如本节前面所述),这是由于技术限制:CGLIB 子类化只能覆盖非 static 方法。因此,对另一个 @Bean 方法的直接调用具有标准 Java 语义,导致从工厂方法本身直接返回一个独立的实例。 @Bean 方法的 Java 语言可见性不会对 Spring 容器中生成的 bean 定义产生直接影响。你可以在 @Configuration 类和其他任何位置自由地以你认为合适的方式声明你的工厂方法。但是,@Configuration 类中的常规 @Bean 方法需要可重写,即它们不能声明为 privatefinal@Bean 方法也出现在给定组件或配置类的基类以及组件或配置类实现的接口中声明的 Java 8 默认方法中。这允许在组合复杂配置方案方面有很大的灵活性,甚至可以通过 Spring 4.2 中的 Java 8 默认方法进行多重继承。 最后,一个类可以为同一个 bean 保存多个 @Bean 方法,作为根据运行时可用的依赖项而使用的多个工厂方法的排列。这与在其他配置方案中选择“贪婪”构造函数或工厂方法的算法相同:在构造时选择具有最大数量的可满足依赖项的变体,类似于容器在多个 @Autowired 构造函数之间进行选择的方式。

Naming Autodetected Components

当一个组件作为扫描过程的一部分被自动检测到时,其 bean 名称由该扫描器所知的 BeanNameGenerator 策略生成。

默认情况下,使用 "@55"。对于 Spring "@61",如果你通过注解的 "@56" 属性提供了一个名称,则该名称将用作相应 Bean 定义中的名称。当使用以下 JSR-250 和 JSR-330 注解代替 Spring 固定用法注解时,也会适用此约定:"@57"、"@58"、"@59" 和 "@60"。

从 Spring Framework 6.1 开始,不再需要用于指定 bean 名称的注解属性的名称为“value”。自定义 stereotype annotations 可以声明一个名称不同的属性(例如,“name”),并使用 @AliasFor(annotation = Component.class, attribute = "value") 注释该属性。请参阅 ControllerAdvice#name() 的源代码声明以获取具体示例。

从 Spring Framework 6.1 开始,对基于约定的 stereotype 名称的支持已弃用,并且将在框架的未来版本中移除。因此,自定义 stereotype annotations 必须使用 @AliasFor@Component 中的 value 属性声明一个显式别名。请参阅 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,该 BeanNameGenerator 默认为生成的 bean 名称的完全限定类名称。从 Spring Framework 5.2.3 开始,位于包 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>

作为一个通用规则,只要有其他组件可能对它进行显式引用,就考虑用注解来指定该名称。另一方面,只要容器负责连接,自动生成的名称就足够了。

Providing a Scope for Autodetected Components

与 Spring 管理的组件一样,自动检测组件的默认范围和最常见范围是 singleton。但是,有时需要不同的范围,可由 @Scope 注解指定。您可以按照以下示例所示提供注解中的范围名称:

  • Java

  • Kotlin

@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
	// ...
}
@Scope("prototype")
@Repository
class MovieFinderImpl : MovieFinder {
	// ...
}

@Scope 注释仅内省具体 Bean 类(对于带注释的组件)或工厂方法(对于 @Bean 方法)。与 XML Bean 定义相反,没有 bean 定义继承的概念,并且类级别的继承层次结构与元数据目的无关。

有关 Spring 上下文中特定于 Web 的范围(例如 "@62" 或 "@63")的详细信息,请参阅 "@65"。与用于这些范围的预置注解一样,你也可以使用 Spring 的元注解方法来组合你自己的范围注解:例如,用 "@64" 进行元注解的自定义注解,也有可能声明自定义作用域代理模式。

要提供一个自定义的范围解析策略,而不是依赖于基于注释的方法,您可以实现https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/ScopeMetadataResolver.html[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>

在使用某些非单例范围时,可能需要为范围对象生成代理。推理在 "@69" 中描述。为此,组件扫描元素中提供了 scoped-proxy 属性。三个可能的值是:"@66"、"@67" 和 "@68"。例如,以下配置将生成标准 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>

Providing Qualifier Metadata with Annotations

Fine-tuning Annotation-based Autowiring with Qualifiers 中讨论了 @Qualifier 注释。该部分中的示例演示了如何使用 @Qualifier 注释和自定义限定符注释在解析自动装配候选对象时提供细粒度控制。由于这些示例基于 XML Bean 定义,因此限定符元数据是使用 XML 中 bean 元素的 qualifiermeta 子元素在候选 Bean 定义上提供的。在依靠类路径扫描自动检测组件时,可以使用候选类上的类型级注释提供限定符元数据。以下三个示例演示了此技术:

  • 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 在其品质元数据中提供变体,因为该元数据是按实例提供的,而不是按类提供的。