跳到主要内容
版本:7.0.3

类路径扫描和管理组件

Hunyuan 7b 中英对照 Classpath Scanning and Managed Components

本章中的大多数示例都使用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 来标注,这些类就会更适合被工具处理或与切面(aspects)关联。例如,这些模板注解是切点(pointcuts)的理想目标。在 Spring Framework 的未来版本中,@Repository@Service@Controller 也可能携带额外的语义。因此,如果你在选择服务层使用 @Component 还是 @Service 时,@Service 显然是更好的选择。同样地,如前所述,@Repository 已经被支持作为持久层中自动异常转换的标记。

使用元注释和组合注释

Spring提供的许多注解都可以作为元注解(meta-annotations)在你的代码中使用。元注解是一种可以应用到其他注解上的注解。例如,前面提到过@Service注解,实际上是被@Component元注解所标记的,以下示例可以说明这一点:

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

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

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

此外,组合注释(composed annotations)可以选择性地重新声明元注释(meta-annotations)中的属性,以实现定制化。当您只想暴露元注释属性的子集时,这一点尤其有用。例如,Spring的@SessionScope注释将作用域名称硬编码为session,但仍然允许对proxyMode进行定制。以下列表显示了@SessionScope注释的定义:

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

}

然后你可以如下使用 @SessionScope,而无需声明 proxyMode

@Service
@SessionScope
public class SessionScopedService {
// ...
}

您还可以覆盖proxyMode的值,如下例所示:

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

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

自动检测类并注册Bean定义

Spring可以自动检测到模式化的类,并将相应的BeanDefinition实例注册到ApplicationContext中。例如,以下两个类就符合这样的自动检测条件:

@Service
public class SimpleMovieLister {

private final MovieFinder movieFinder;

public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
@Repository
public class JpaMovieFinder implements MovieFinder {
// implementation elided for clarity
}

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

@Configuration
@ComponentScan(basePackages = "org.example")
public 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> 元素。

备注

在类路径(classpath)中扫描包时,需要在类路径中存在相应的目录条目。当你使用Ant构建JAR文件时,请确保没有启用JAR任务的“仅文件”(files-only)选项。此外,在某些环境中,由于安全策略的限制,类路径目录可能无法被访问——例如,在JDK 1.7.0_45及更高版本上的独立应用程序中(这种情况下需要在manifest文件中进行“Trusted-Library”的配置——参见stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。

在模块路径(Java Module System)上,Spring的类路径扫描功能通常能够按预期工作。但是,请确保你的组件类已在module-info描述符中进行了导出。如果你希望Spring能够调用这些类的非公共成员方法,那么请确保这些方法在module-info描述符中使用了opens声明(而不是exports声明)。

此外,当你使用 <context:component-scan> 元素时,AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 会自动被包含进去。这意味着这两个组件会自动被检测到并连接在一起——而这一切都不需要在 XML 中提供任何 bean 配置元数据。

备注

你可以通过添加 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属性占位符。

@Configuration
@ComponentScan("${appscan.packages}") 1
public class AppConfig {
// ...
}
  • app.scan.packages 属性占位符,将根据 Environment 进行解析

以下列表代表一个属性文件,该文件定义了 app_scan.packages 属性。在前面的例子中,假设这个属性文件已经通过 Environment 进行了注册——例如,通过 @PropertySource 或类似的机制。

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一种注释,需要在目标组件的类型级别上存在元存在
assignableorg.example.SomeClass一个类(或接口),目标组件可以赋值给它(继承或实现该类)。
aspectjorg.example..*Service+一种AspectJ类型表达式,目标组件需要与之匹配。
regexorg\.example\.Default.*一种正则表达式,用于匹配目标组件的类名。
customorg.example.MyTypeFilterorg.springframework.core.type.TypeFilter接口的自定义实现。

以下示例展示了@ComponentScan的配置,该配置排除了所有的@Repository注解,而是包含了“Stub”仓库:

@Configuration
@ComponentScan(basePackages = "org.example",
includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
excludeFilters = @Filter(Repository.class))
public 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名称由该扫描器所知的BeanNameGenerator策略生成。

默认情况下,使用的是AnnotationBeanNameGenerator。对于Spring的泛型注解,如果您通过注解的value属性提供了一个名称,那么该名称将作为相应bean定义中的名称使用。当使用@jakarta.inject.Named注解代替Spring的泛型注解时,这一约定也同样适用。

从Spring Framework 6.1开始,用于指定bean名称的注解属性名称不再必须是value。自定义的 stereotype 注解可以声明一个不同的属性名称(例如name),并通过@AliasFor(annotation = Component.class, attribute = "value")来为该属性添加注解。具体示例请参见ControllerAdvice#name()的源代码声明。

注意

从Spring Framework 6.1开始,基于约定的 stereotypes 名称的支持已被弃用,并将在未来的框架版本中移除。因此,自定义 stereotypes 注解必须使用 @AliasFor 来为 @Component 中的 value 属性声明一个明确的别名。请参阅 Repository#value()ControllerAdvice#name() 的源代码声明以了解具体示例。

如果无法从此类注解中推导出显式的bean名称,或者对于任何其他检测到的组件(例如通过自定义过滤器发现的组件),默认的bean名称生成器将返回小写的、未加限定符的类名。例如,如果检测到以下组件类,则其名称分别为myMovieListermovieFinderImpl

@Service("myMovieLister")
public class SimpleMovieLister {
// ...
}
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}

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

提示

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

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

作为一般规则,当其他组件可能会明确引用某个名称时,应考虑在注释中指定该名称。另一方面,当容器负责连接(wiring)工作时,自动生成的名称就足够使用了。

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

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

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

@Scope 注解仅在被注解的组件对应的具体 Bean 类或 @Bean 方法的工厂方法上被解析(introspected)。与 XML Bean 定义不同,不存在 Bean 定义继承的概念,且在元数据用途中,类级别的继承层次结构是无关紧要的。

有关Spring上下文中“request”或“session”等特定于Web的作用域的详细信息,请参阅Request、Session、Application和WebSocket作用域。与这些作用域的预构建注解类似,您也可以使用Spring的元注解方法来自定义自己的作用域注解:例如,一个用@Scope("prototype")进行元注解的自定义注解,可能还会声明一种自定义的作用域代理模式。

备注

为了提供一种自定义的作用域解析策略,而不是依赖于基于注解的方法,你可以实现ScopeMetadataResolver接口。务必包含一个无参数的默认构造函数。然后,在配置扫描器时,你可以提供完全限定的类名,如下例所示,该例同时展示了注解和bean定义:

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

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

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

通过注释提供限定符元数据

@Qualifier 注解在 使用 Qualifier 进行细粒度的自动装配微调 中有介绍。该节中的示例展示了如何使用 @Qualifier 注解和自定义的限定符注解,在解析自动装配候选对象时实现细粒度的控制。由于那些示例是基于 XML 类定义的,因此限定符元数据是通过在 XML 中的 bean 元素的 qualifiermeta 子元素来提供的。当依赖类路径扫描来自动检测组件时,你可以在候选类上使用类型级别的注解来提供限定符元数据。以下三个示例演示了这种技术:

@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
// ...
}
备注

与大多数基于注释的替代方案一样,需要记住的是,注释元数据是绑定到类定义本身上的,而使用XML则允许同一类型的多个bean在其限定符元数据上提供变体,因为这些元数据是按实例提供的,而不是按类提供的。

在组件中定义Bean元数据

Spring组件也可以向容器提供bean定义的元数据。你可以使用与在标注有@Configuration的类中定义bean元数据时相同的@Bean注解来实现这一点。以下示例展示了如何操作:

@Component
public class FactoryMethodComponent {

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

public void doWork() {
// Component method implementation omitted
}
}

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

提示

除了用于组件初始化之外,你还可以在标有@Autowired@Inject的注入点上添加@Lazy注解。在这种情况下,它会导致注入一个延迟解析的代理对象。然而,这种代理方法的使用范围相当有限。对于更复杂的延迟交互,特别是与可选依赖项结合使用时,我们建议使用ObjectProvider<MyTargetBean>

如前所述,支持自动注入字段和方法,同时还增加了对@Bean方法自动注入的支持。以下示例展示了如何实现这一点:

@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);
}
}

该示例将String方法参数country自动绑定到另一个名为privateInstance的bean上的age属性的值。Spring表达式语言(Spring Expression Language)元素通过#{<expression>}的表示法来定义属性的值。对于@Value注解,在解析表达式文本时,会预先配置一个表达式解析器来查找bean名称。

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

@Component
public class FactoryMethodComponent {

@Bean @Scope("prototype")
public TestBean prototypeInstance(InjectionPoint injectionPoint) {
return new TestBean("prototypeInstance for " + injectionPoint.getMember());
}
}

在普通的Spring组件中,@Bean方法的处理方式与在Spring的@Configuration类中的@Bean方法不同。区别在于@Component类并不会通过CGLIB进行增强,以便拦截方法或字段的调用。在@Configuration类中的@Bean方法中调用方法或字段时,会利用CGLIB代理机制来创建对协作对象的bean元数据引用。这类方法的调用并不遵循常规的Java语义,而是需要通过Spring容器来完成,从而实现Spring bean的常规生命周期管理和代理功能,即便这些方法是通过程序式调用@Bean方法来引用其他bean的。相比之下,在普通的@Component类中的@Bean方法中调用方法或字段时,则遵循标准的Java语义,不会应用任何特殊的CGLIB处理或其他限制。

备注

你可以将@Bean方法声明为static,这样就可以在不创建包含它们的配置类的实例的情况下调用这些方法。这在定义后处理器bean(例如BeanFactoryPostProcessorBeanPostProcessor类型的bean)时尤为合理,因为这类bean在容器生命周期的早期就被初始化了,此时应避免触发配置的其他部分。

由于技术限制,对静态@Bean方法的调用永远不会被容器拦截,即使在@Configuration类内部也是如此(如本节前面所描述的)。这是因为CGLIB子类化只能覆盖非静态方法。因此,直接调用另一个@Bean方法将遵循标准的Java语义,从而直接从工厂方法本身返回一个独立的实例。

@Bean方法在Java语言中的可见性并不会直接影响Spring容器中生成的bean定义。你可以在非@Configuration类中自由地声明工厂方法,也可以在任何地方声明静态方法。然而,在@Configuration类中的普通@Bean方法需要是可被覆盖的——也就是说,它们不能被声明为privatefinal

@Bean方法也可以在给定组件或配置类的基类上被发现,以及在组件或配置类实现的接口中声明的Java默认方法上被发现。这使得在构建复杂的配置时具有很大的灵活性,甚至可以通过Java默认方法实现多重继承。

最后,一个类可以包含多个针对同一bean的@Bean方法,这些方法是根据运行时可用的依赖关系来选择的。这与在其他配置场景中选择“最贪婪”的构造函数或工厂方法的算法相同:在构造时会选择满足最多依赖关系的那个版本,类似于容器在多个@Autowired构造函数之间进行选择的方式。