Skip to content

类路径扫描与组件管理 (Classpath Scanning and Managed Components)

本章中的大多数示例都使用 XML 来指定在 Spring 容器中生成每个 BeanDefinition 的配置元数据。上一节(基于注解的容器配置)演示了如何通过源代码级注解提供许多配置元数据。然而,即使在那些示例中,“基础”Bean 定义也是在 XML 文件中显式定义的,而注解仅驱动依赖注入。

本节描述了一种通过扫描类路径隐式检测候选组件的方法。候选组件是符合过滤条件并在容器中注册了相应 Bean 定义的类。这消除了使用 XML 执行 Bean 注册的需要。相反,你可以使用注解(例如 @Component)、AspectJ 类型表达式或你自己的自定义过滤条件来选择哪些类在容器中注册 Bean 定义。

注意

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

@Component 和更多构造型注解

@Repository 注解是实现仓库(Repository,也称为数据访问对象或 DAO)角色或 构造型 (stereotype) 的任何类的标记。该标记的用途之一是自动转换异常,如异常转换中所述。

Spring 提供了更多的构造型注解:@Component@Service@Controller@Component 是任何 Spring 管理的组件的通用构造型。@Repository@Service@Controller@Component 针对更具体用例(分别在持久层、服务层和展示层中)的特化。

因此,你可以用 @Component 标注你的组件类,但通过用 @Repository@Service@Controller 标注它们,你的类更适合通过工具进行处理或与切面关联。例如,这些构造型注解是切入点(pointcuts)的理想目标。在 Spring Framework 的未来发行版中,@Repository@Service@Controller 还可能携带额外的语义。因此,如果你在服务层中选择使用 @Component 还是 @Service@Service 显然是更好的选择。同样,如前所述,@Repository 已经作为持久层中自动异常转换的标记得到支持。

使用元注解和组合注解

Spring 提供的许多注解都可以在你自己的代码中用作 元注解 (meta-annotations)。元注解是可以应用于另一个注解的注解。例如,前面提到的 @Service 注解就使用了 @Component 作为元注解,如下例所示:

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

	// ...
}
kotlin
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Component // (1)
annotation class Service {

	// ...
}
  1. @Component 元注解使得 @Service 以与 @Component 相同的方式被处理。

你还可以组合元注解来创建“组合注解 (composed annotations)”。例如,Spring MVC 的 @RestController 注解由 @Controller@ResponseBody 组合而成。

此外,组合注解可以可选地重新声明元注解中的属性,以允许进行自定义。当你只想公开元注解属性的一个子集时,这特别有用。例如,Spring 的 @SessionScope 注解将作用域名称硬编码为 session,但仍允许自定义 proxyMode。以下代码展示了 @SessionScope 注解的定义:

java
@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;

}
kotlin
@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
@Service
@SessionScope
public class SessionScopedService {
	// ...
}
kotlin
@Service
@SessionScope
class SessionScopedService {
	// ...
}

你也可以覆盖 proxyMode 的值,如下例所示:

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

有关更多详细信息,请参阅 Spring 注解编程模型 Wiki 页面。

自动检测类并注册 Bean 定义

Spring 可以自动检测构造型类,并向 ApplicationContext 注册相应的 BeanDefinition 实例。例如,以下两个类符合此类自动检测的条件:

java
@Service
public class SimpleMovieLister {

	private final MovieFinder movieFinder;

	public SimpleMovieLister(MovieFinder movieFinder) {
		this.movieFinder = movieFinder;
	}
}
kotlin
@Service
class SimpleMovieLister(private val movieFinder: MovieFinder)
java
@Repository
public class JpaMovieFinder implements MovieFinder {
	// 省略实现细节
}
kotlin
@Repository
class JpaMovieFinder : MovieFinder {
	// 省略实现细节
}

要自动检测这些类并注册相应的 Bean,你需要在 @Configuration 类中添加 @ComponentScan,其中 basePackages 属性配置为这两个类的共同父包。(或者,你可以指定一个包含每个类的父包的逗号、分号或空格分隔的列表)。

java
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
	// ...
}
kotlin
@Configuration
@ComponentScan(basePackages = ["org.example"])
class AppConfig  {
	// ...
}

提示

为了简洁起见,前面的示例可以使用注解的隐式 value 属性代替:@ComponentScan("org.example")

以下示例使用 XML 配置:

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> 元素。

::: note 注意 类路径包的扫描要求类路径中存在相应的目录条目。当你使用 Ant 构建 JAR 时,请确保不要激活 JAR 任务的 files-only 开关。此外,在某些环境中,可能由于安全策略而无法公开类路径目录 —— 例如 JDK 1.7.0_45 及更高版本上的独立应用程序(这需要在清单文件中进行“Trusted-Library”设置)。

在模块路径(Java 模块系统)上,Spring 的类路径扫描通常按预期工作。但是,请确保你的组件类在 module-info 描述符中已 导出 (exported)。如果你期望 Spring 调用类的非公共成员,请确保它们已 开放 (opened)(即在 module-info 描述符中使用 opens 声明而不是 exports 声明)。 :::

此外,当你使用 <context:component-scan> 元素时,AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 都会被隐式包含。这意味着这两个组件会被自动检测并相互连接 —— 所有这些都无需在 XML 中提供任何 Bean 配置元数据。

::: note 注意 你可以通过在 annotation-config 属性中包含 false 值来禁用 AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 的注册。 :::

属性占位符和 Ant 风格模式

@ComponentScan 中的 basePackagesvalue 属性支持 ${…} 属性占位符(可针对 Environment 进行解析)以及 Ant 风格的包模式,例如 "org.example.**"

此外,可以指定多个包或模式,既可以分别指定,也可以在单个字符串中指定 —— 例如,{"org.example.config", "org.example.service.**"}"org.example.config, org.example.service.**"

以下示例为 @ComponentScan 中的隐式 value 属性指定了 app.scan.packages 属性占位符。

java
@Configuration
@ComponentScan("${app.scan.packages}") // (1)
public class AppConfig {
	// ...
}
kotlin
@Configuration
@ComponentScan(["\${app.scan.packages}"]) // (1)
class AppConfig {
	// ...
}
  1. app.scan.packages 属性占位符将针对 Environment 进行解析。

以下代码片段代表了一个定义 app.scan.packages 属性的属性文件。在前面的示例中,假设此属性文件已向 Environment 注册 —— 例如通过 @PropertySource 或类似机制。

properties
app.scan.packages=org.example.config, org.example.service.**

使用过滤器自定义扫描

默认情况下,标注了 @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) 的注解。
assignableorg.example.SomeClass目标组件可分配给的类(或接口)(继承或实现)。
aspectjorg.example..*Service+目标组件要匹配的 AspectJ 类型表达式。
regexorg\.example\.Default.*目标组件类名要匹配的正则表达式。
customorg.example.MyTypeFilterorg.springframework.core.type.TypeFilter 接口的自定义实现。

以下示例展示了排除所有 @Repository 注解并改为包含“Stub”仓库的 @ComponentScan 配置:

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

以下是等效的 XML:

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>

::: note 注意 你还可以通过在注解上设置 useDefaultFilters=false 或为 <component-scan/> 元素提供 use-default-filters="false" 属性来禁用默认过滤器。这会有效地禁用对标注或元标注了 @Component@Repository@Service@Controller@RestController@Configuration 的类的自动检测。 :::

为自动检测的组件命名

当一个组件在扫描过程中被自动检测到时,其 Bean 名称由该扫描器已知的 BeanNameGenerator 策略生成。

默认情况下,使用 AnnotationBeanNameGenerator。对于 Spring 构造型注解,如果你通过注解的 value 属性提供了一个名称,该名称将被用作对应 Bean 定义中的名称。当使用 @jakarta.inject.Named 注解代替 Spring 构造型注解时,此约定也适用。

从 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
@Service("myMovieLister")
public class SimpleMovieLister {
	// ...
}
kotlin
@Service("myMovieLister")
class SimpleMovieLister {
	// ...
}
java
@Repository
public class MovieFinderImpl implements MovieFinder {
	// ...
}
kotlin
@Repository
class MovieFinderImpl : MovieFinder {
	// ...
}

如果你不想依赖默认的 Bean 命名策略,可以提供一个自定义 Bean 命名策略。首先,实现 BeanNameGenerator 接口,并确保包含一个默认的无参构造函数。然后,在配置扫描器时提供全限定类名,如以下示例注解和 Bean 定义所示。

提示

如果由于多个自动检测到的组件具有相同的非限定类名(即名称相同但在不同包中的类)而导致命名冲突,你可能需要配置一个 BeanNameGenerator,它默认使用全限定类名作为生成的 Bean 名称。可以使用 org.springframework.context.annotation 包中的 FullyQualifiedAnnotationBeanNameGenerator 来达到此目的。

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

作为一般规则,每当其他组件可能对其进行显式引用时,请考虑使用注解指定名称。另一方面,只要容器负责装配,自动生成的名称就足够了。

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

与通常由 Spring 管理的组件一样,自动检测组件的默认且最常见的作用域是 singleton。但是,有时你需要一个不同的作用域,可以通过 @Scope 注解指定。你可以在注解中提供作用域的名称,如下例所示:

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

::: note 注意 @Scope 注解仅在具体的 Bean 类(对于标注的组件)或工厂方法(对于 @Bean 方法)上进行内省。与 XML Bean 定义不同,这里没有 Bean 定义继承的概念,且类级别的继承层次结构对于元数据目的而言是无关紧要的。 :::

有关 Spring 上下文中特定于 Web 的作用域(如 “request” 或 “session”)的详细信息,请参阅请求、会话、应用程序和 WebSocket 作用域。正如这些作用域的预制注解一样,你也可以使用 Spring 的元注解方法组合自己的作用域注解:例如,用 @Scope("prototype") 元标注的自定义注解,可能还可以声明自定义的作用域代理模式(scoped-proxy mode)。

::: note 注意 要提供自定义的作用域解析策略,而不是依赖基于注解的方法,你可以实现 ScopeMetadataResolver 接口。确保包含一个默认的无参构造函数。然后你可以在配置扫描器时提供全限定类名,如下例所示(包含注解和 Bean 定义形式): :::

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

当使用某些非单例作用域时,可能需要为作用域对象生成代理。其原因在作为依赖项的作用域 Bean中描述。为此,component-scan 元素上提供了一个 scoped-proxy 属性。三个可能的值是:nointerfacestargetClass。例如,以下配置将导致标准的 JDK 动态代理:

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

使用注解提供限定符元数据

@Qualifier 注解在使用限定符 (Qualifiers) 精细控制基于注解的自动装配中讨论。该节中的示例演示了如何使用 @Qualifier 注解和自定义限定符注解,在解析自动装配候选者时提供细粒度的控制。因为那些示例基于 XML Bean 定义,所以限定符元数据是通过使用 XML 中 bean 元素的 qualifiermeta 子元素在候选 Bean 定义上提供的。当依靠类路径扫描自动检测组件时,你可以通过在候选类上使用类型级别(Type-level)的注解来提供限定符元数据。以下三个示例演示了此技术:

java
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
	// ...
}
kotlin
@Component
@Qualifier("Action")
class ActionMovieCatalog : MovieCatalog
java
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
	// ...
}
kotlin
@Component
@Genre("Action")
class ActionMovieCatalog : MovieCatalog {
	// ...
}
java
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
	// ...
}
kotlin
@Component
@Offline
class CachingMovieCatalog : MovieCatalog {
	// ...
}

::: note 注意 与大多数基于注解的替代方案一样,请记住,注解元数据绑定到类定义本身,而使用 XML 允许相同类型的多个 Bean 提供其限定符元数据的变化,因为该元数据是针对每个实例而不是针对每个类提供的。 :::

在组件中定义 Bean 元数据

Spring 组件还可以向容器贡献 Bean 定义元数据。你可以使用与在 @Configuration 标注的类中定义 Bean 元数据相同的 @Bean 注解来完成此操作。以下示例展示了如何实现:

java
@Component
public class FactoryMethodComponent {

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

	public void doWork() {
		// 省略组件方法实现
	}
}
kotlin
@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>

如前所述,支持自动装配的字段和方法,并且还额外支持对 @Bean 方法进行自动装配。以下示例详细说明了如何操作:

java
@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);
	}
}
kotlin
@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 表达式语言(SpEL)通过 #{ <expression> } 符号定义属性值。对于 @Value 注解,预配置了一个表达式解析器,在解析表达式文本时会查找 Bean 名称。

从 Spring Framework 4.3 开始,你还可以声明一个 InjectionPoint 类型(或其更具体的子类:DependencyDescriptor)的工厂方法参数,以访问触发当前 Bean 创建的请求注入点。请注意,这仅适用于 Bean 实例的实际创建,而不适用于现有实例的注入。因此,此功能对于原型作用域(prototype scope)的 Bean 最有意义。对于其他作用域,工厂方法只会看到在给定作用域内触发创建新 Bean 实例的注入点(例如,触发创建延迟单例 Bean 的依赖项)。在这种情况下,你可以谨慎地使用提供的注入点元数据。以下示例展示了如何使用 InjectionPoint

java
@Component
public class FactoryMethodComponent {

	@Bean @Scope("prototype")
	public TestBean prototypeInstance(InjectionPoint injectionPoint) {
		return new TestBean("prototypeInstance for " + injectionPoint.getMember());
	}
}
kotlin
@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 处理或其他约束。

::: note 注意 你可以将 @Bean 方法声明为 static,从而允许在不创建其包含配置类实例的情况下调用它们。在定义后置处理器(Post-processor)Bean(例如 BeanFactoryPostProcessorBeanPostProcessor 类型)时,这特别有意义,因为此类 Bean 在容器生命周期的早期就会被初始化,此时应避免触发配置的其他部分。

由于技术限制,对静态 @Bean 方法的调用永远不会被容器拦截,即使在 @Configuration 类中也是如此(如本节早些时候所述):CGLIB 子类化只能重写非静态方法。因此,直接调用另一个 @Bean 方法具有标准 Java 语义,导致直接从工厂方法本身返回一个独立的实例。

Java 语言对 @Bean 方法的可访问性(visibility)不会对 Spring 容器中生成的 Bean 定义产生直接影响。你可以根据自己的需要在非 @Configuration 类中或在任何地方的静态方法中自由声明工厂方法。然而,@Configuration 类中的常规 @Bean 方法需要是可重写的 —— 也就是说,它们不能被声明为 privatefinal

@Bean 方法也会在给定组件或配置类的基类上被发现,以及在组件或配置类实现的接口中声明的 Java 默认方法上被发现。这在组合复杂的配置安排时提供了很大的灵活性,甚至可以通过 Java 默认方法实现多重继承。

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


补充教学 —— 扫描背后的学问

如果把 Spring 容器比作一家工厂,那么“类路径扫描”就像是工厂里的“自动感应雷达”。它让原本需要一个一个手动登记的零部件(Bean),变成了自动入库。

1. 构造型注解(Stereotypes)的深层含义 虽然 @Service@Repository@Controller 本质上都是 @Component,但它们代表了架构中的不同层次:

  • @Repository:不仅代表持久层,它还会触发 Spring 的 PersistenceExceptionTranslationPostProcessor,自动将数据库驱动抛出的原生异常转换为 Spring 的统一数据访问异常。
  • @Controller / @RestController:标记 Web 层,会被 Spring MVC 的 RequestMappingHandlerMapping 识别,将其中的方法映射为 Web 请求的处理程序。

2. 扫描性能与“胖包”问题@ComponentScan("com.example") 虽然方便,但如果你的项目包含上万个类,且大多在同一个根包下,启动时的扫描性能会有所下降。

  • 最佳实践:尽量扫描具体的业务包。或者使用 includeFiltersexcludeFilters 剔除掉那些无关紧要的工具类包。
  • 禁止:千万不要直接扫描 org.springframework 或者 java 包,这会导致不可预知的错误。

3. @Bean 在 @Component vs @Configuration 中的“终极大坑” 这是 Spring 高级面试必考题。文档中提到了 "Full" mode"Lite" mode

  • Full 模式(@Configuration):Spring 会用 CGLIB 修改你的类。如果你在 methodA() 里调用 methodB(),Spring 会拦截这个调用,去容器里找 BeanB。这保证了单例。
  • Lite 模式(@Component):Spring 不会修改你的类。如果你在 methodA() 里调用 methodB(),它就是普通的 Java 方法调用,会直接运行代码并创建一个新的实例,而不是从 Spring 容器里拿。
  • 建议:如果你需要 Bean 之间有复杂的交互组合,请务必使用 @Configuration

4. 解决同名类冲突 当你有 com.a.UserServicecom.b.UserService 时,默认的命名策略会导致 ConflictingBeanDefinitionException

  • 方案:正如提示中所说,配置 FullyQualifiedAnnotationBeanNameGenerator,让 Bean 名字变成 com.a.UserService。或者手动在注解里指定名字:@Service("aUserService")

5. 属性占位符的妙用 你甚至可以动态决定扫描哪个包:

java
@ComponentScan("${my.app.base-package}")

这在做通用框架开发时非常有用,扫描的范围可以根据每个项目的 application.properties 配置来动态决定。

Based on Spring Framework.