类路径扫描和托管组件

本章中的大多数示例使用XML来指定配置元数据,以在Spring容器中生成每个BeanDefinition。前一节(基于注解的容器配置)演示了如何通过源级别注解提供大量配置元数据。然而,即使在这些示例中,“基本” bean 定义也是在XML文件中明确定义的,而注解仅驱动依赖注入。本节描述了一种通过扫描类路径隐式检测候选组件的选项。候选组件是与过滤条件匹配并在容器中注册相应bean定义的类。这消除了使用XML执行bean注册的需要。相反,您可以使用注解(例如@Component)、AspectJ 类型表达式或您自己的自定义过滤条件来选择哪些类具有在容器中注册bean定义。

您可以使用Java定义bean,而不是使用XML文件。查看@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已经支持作为持久性层中自动异常转换的标记。

使用元注解和组合注解

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注解编程模型维基页面。

自动检测类并注册Bean定义

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,您需要在您的@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任务的仅文件开关。此外,基于某些环境中的安全策略,类路径目录可能不会被公开,例如,独立应用程序在JDK 1.7.0_45及更高版本上(这需要在清单中设置'Trusted-Library'配置,参见stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。

在JDK 9的模块路径(Jigsaw)上,Spring的类路径扫描通常按预期工作。但是,请确保您的组件类在您的module-info描述符中被导出。如果您希望Spring调用您的类的非公共成员,请确保它们是“opened”(即,在您的module-info描述符中使用opens声明而不是exports声明)。

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

您可以通过包含值为falseannotation-config属性来禁用AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor的注册。

使用过滤器自定义扫描

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

表1. 过滤器类型
过滤器类型 示例表达式 描述

注解(默认)

org.example.SomeAnnotation

要求目标组件在类型级别上存在或元级别上存在的注解。

可分配

org.example.SomeClass

目标组件可分配给的类(或接口)(扩展或实现)。

aspectj

org.example..*Service+

要求目标组件匹配的AspectJ类型表达式。

正则表达式

org\.example\.Default.*

要求目标组件的类名匹配的正则表达式。

自定义

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注解或元注解的类。

在组件中定义Bean元数据

Spring组件也可以向容器贡献bean定义元数据。您可以使用相同的@Bean注解来定义@Configuration注解类中的bean元数据。以下示例展示了如何实现:

  • Java

  • Kotlin

@Component
public class FactoryMethodComponent {

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

	public void doWork() {
		// 组件方法实现已省略
	}
}
@Component
class FactoryMethodComponent {

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

	fun doWork() {
		// 组件方法实现已省略
	}
}

上述类是一个Spring组件,其doWork()方法中包含特定于应用程序的代码。但是,它还贡献了一个bean定义,其中包含引用publicInstance()方法的工厂方法。@Bean注解标识了工厂方法和其他bean定义属性,例如通过@Qualifier注解指定的限定符值。可以指定的其他方法级注解包括@Scope@Lazy和自定义限定符注解。

除了用于组件初始化的作用外,您还可以在标记为@Autowired@Inject的注入点上放置@Lazy注解。在这种情况下,它会导致注入延迟解析代理。但是,这种代理方法相对有限。对于复杂的延迟交互,特别是与可选依赖项结合使用时,我们建议改用ObjectProvider<MyTargetBean>

支持Autowired字段和方法,如前所述,还支持对@Bean方法进行自动装配。以下示例展示了如何实现:

  • Java

  • Kotlin

@Component
public class FactoryMethodComponent {

	private static int i;

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

	// 使用自定义限定符和方法参数的自动装配
	@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")

	// 使用自定义限定符和方法参数的自动装配
	@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语义,直接从工厂方法本身返回独立实例。

@Bean方法的Java语言可见性对Spring容器中生成的bean定义没有直接影响。您可以根据需要自由地在非@Configuration类中声明工厂方法,也可以在任何地方声明静态方法。但是,在@Configuration类中,常规的@Bean方法需要是可重写的,即不能声明为privatefinal

@Bean方法还会在给定组件或配置类的基类上被发现,以及在由组件或配置类实现的接口中声明的Java 8默认方法上。这允许以非常灵活的方式组合复杂的配置安排,甚至通过Java 8默认方法实现多重继承,从Spring 4.2开始。

最后,单个类可以包含多个相同bean的@Bean方法,作为根据运行时可用依赖项选择多个工厂方法的安排。这与在其他配置方案中选择“最贪婪”构造函数或工厂方法的算法相同:在构建时选择具有最大可满足依赖项数量的变体,类似于容器在多个@Autowired构造函数之间进行选择的方式。

命名自动检测到的组件

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

默认情况下,使用AnnotationBeanNameGenerator。对于Spring构造型注解,如果通过注解的value属性提供名称,该名称将用作相应bean定义中的名称。当使用以下JSR-250和JSR-330注解而不是Spring构造型注解时,此约定也适用:@jakarta.annotation.ManagedBean@javax.annotation.ManagedBean@jakarta.inject.Named@javax.inject.Named

从Spring Framework 6.1开始,用于指定bean名称的注解属性的名称不再需要是value。自定义构造型注解可以声明具有不同名称(例如name)的属性,并使用@AliasFor(annotation = Component.class, attribute = "value")注解该属性。查看ControllerAdvice#name()的源代码声明以获取具体示例。

从Spring Framework 6.1开始,基于约定的构造型名称支持已被弃用,并将在将来的框架版本中移除。因此,自定义构造型注解必须使用@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,该生成器默认为生成的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>

一般来说,当其他组件可能会明确引用时,请考虑在注解中指定名称。另一方面,当容器负责装配时,自动生成的名称是足够的。

为自动检测的组件提供作用域

与一般的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上下文中使用诸如“request”或“session”等特定于Web的作用域的详细信息,请参阅请求、会话、应用程序和WebSocket作用域。与这些作用域的预构建注解一样,您也可以通过使用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>

当使用某些非单例作用域时,可能需要为作用域对象生成代理。这种做法的原因在作为依赖项的作用域bean中有所描述。为此,组件扫描元素上提供了一个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注解和自定义限定符注解在解析自动装配候选项时提供精细控制。因为这些示例是基于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提供限定符元数据的变化,因为该元数据是针对每个实例而不是每个类提供的。