|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
类路径扫描与托管组件
本章中的大多数示例使用 XML 来指定配置元数据,以在 Spring 容器中生成每个 BeanDefinition。The previous section
(基于注解的容器配置) demonstrates how to provide a lot of the configuration
元数据通过源级注解。然而,即使在这些示例中,“基础”bean 的定义也是在 XML 文件中显式声明的,而注解仅用于驱动依赖注入。本节介绍了一种通过扫描类路径来隐式检测候选组件的选项。候选组件是指符合过滤条件,并且已在容器中注册了相应 Bean 定义的类。这消除了使用 XML 进行 bean 注册的需要。而不是这样,
您可以使用注解(例如,@Component),AspectJ 类型表达式,或您自己的
自定义筛选条件来选择哪些类具有由容器注册的 Bean 定义。
|
你可以使用 Java 来定义 Bean,而不必使用 XML 文件。请查看 |
@Component以及更多的构造型注解
@Repository 注解是用于标记任何扮演仓库(也称为数据访问对象或 DAO)角色或构造型的类。该标记的用途之一是自动进行异常转换,如异常转换一节所述。
Spring 提供了更多的构造型注解:@Component、@Service 和
@Controller。@Component 是任何由 Spring 管理的组件的通用刻板印象。@Repository, @Service, 和 @Controller 是 @Component 在更多具体用例中的特化(分别在持久层、服务层和表现层)。因此,您可以使用 @Component 注解您的组件类,但改用 @Repository、@Service 或 @Controller 进行注解,会使您的类更适合由工具处理或与切面关联。例如,这些构造型注解是切点(pointcuts)的理想目标。@Repository, @Service, 和 @Controller 也可以在未来的 Spring 框架版本中携带额外的语义。因此,如果你在选择用于服务层的版本时,在使用@Component或@Service之间进行选择,@Service显然是更好的选择。同样地,如之前所述,@Repository 已经被支持作为自动异常转换在持久层中的标记。
使用元注解和组合注解
Spring 提供的许多注解都可以在你自己的代码中用作元注解。元注解是指可以应用到另一个注解上的注解。
例如,前面提到的 #beans-stereotype-annotations 注解就通过元注解的方式标注了 @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,你需要在你的 @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:annotation-config> 时,
无需再包含 <context:component-scan> 元素。 |
|
扫描类路径(classpath)中的包需要类路径中存在相应的目录条目。当你使用 Ant 构建 JAR 文件时,请确保不要启用 JAR 任务的“仅文件”(files-only)开关。此外,在某些环境中,基于安全策略,类路径目录可能无法被暴露出来——例如,在 JDK 1.7.0_45 及更高版本上运行的独立应用程序(这需要在你的清单文件中设置 'Trusted-Library'——参见 stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。 在 JDK 9 的模块路径(Jigsaw)上,Spring 的类路径扫描通常能按预期正常工作。
但是,请确保您的组件类已在 |
此外,当你使用 AutowiredAnnotationBeanPostProcessor 元素时,CommonAnnotationBeanPostProcessor 和 2 都会被隐式包含。这意味着这两个组件会被自动检测并装配在一起——完全无需在 XML 中提供任何 Bean 配置元数据。
你可以通过将 AutowiredAnnotationBeanPostProcessor 属性的值设为 CommonAnnotationBeanPostProcessor,来禁用 annotation-config 和 false 的注册。 |
使用过滤器自定义扫描
默认情况下,只有使用 @Component、@Repository、@Service、@Controller、
@Configuration 注解,或使用本身被 @Component 注解的自定义注解标注的类,才会被检测为候选组件。但是,您可以通过应用自定义过滤器来修改和扩展此行为。将它们作为 @ComponentScan 注解的 includeFilters 或 excludeFilters 属性添加(或在 XML 配置中作为 <context:component-scan> 元素的 <context:include-filter /> 或
<context:exclude-filter /> 子元素添加)。每个过滤器元素都需要 type 和 expression 属性。
下表描述了过滤选项:
| 过滤器类型 | 示例表达式 | <description> </description> |
|---|---|---|
注解(默认) |
|
一个在目标组件的类型级别上存在或元存在的注解。 |
可赋值的 |
|
目标组件可赋值(继承或实现)的类(或接口)。 |
aspectj |
|
一个 AspectJ 类型表达式,用于匹配目标组件。 |
正则表达式 |
|
一个正则表达式,用于匹配目标组件的类名。 |
自定义 |
|
|
以下示例展示了忽略所有 @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 注解的类中定义 Bean 元数据时相同的 @Configuration 注解来实现这一点。以下示例展示了如何进行此操作:
-
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 自动装配为另一个名为 age 的 bean 上 privateInstance 属性的值。Spring 表达式语言(SpEL)元素通过 #{ <expression> } 语法定义该属性的值。对于 @Value 注解,表达式解析器已预先配置为在解析表达式文本时查找 bean 名称。
从 Spring Framework 4.3 起,您还可以声明一个类型为 InjectionPoint(或其更具体的子类:DependencyDescriptor)的工厂方法参数,以访问触发当前 Bean 创建的请求注入点。
请注意,这仅适用于 Bean 实例的实际创建过程,而不适用于现有实例的注入。因此,此功能对原型(prototype)作用域的 Bean 最有意义。对于其他作用域,工厂方法只会看到在给定作用域中触发新 Bean 实例创建的那个注入点
(例如,触发延迟初始化的单例(lazy singleton)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 代理是这样一种机制:在 @Bean 类的 @Configuration 方法内部调用方法或字段时,会创建指向协作对象的 bean 元数据引用。这些方法并非以普通的 Java 语义被调用,而是通过容器进行调用,从而提供 Spring bean 常规的生命周期管理和代理功能,即使通过编程方式调用 @Bean 方法来引用其他 bean 也是如此。相比之下,在普通的 @Bean 类中调用 @Component 方法内的方法或字段时,则遵循标准的 Java 语义,不会应用特殊的 CGLIB 处理或其他约束。
|
您可以将 对静态
最后,单个类可以包含多个用于同一 bean 的 |
命名自动检测的组件
当一个组件在扫描过程中被自动检测到时,其 Bean 名称由该扫描器所知的 BeanNameGenerator 策略生成。默认情况下,任何包含名称 @Component 的 Spring 刻板印象注解(@Repository、@Service、@Controller 和 value)都会将该名称提供给相应的 Bean 定义。
如果此类注解未包含名称 value,或者对于任何其他被检测到的组件(例如通过自定义过滤器发现的组件),默认的 bean 名称生成器将返回非限定类名的小写形式。例如,如果检测到以下组件类,则其名称将分别为 myMovieLister 和 movieFinderImpl:
-
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 起,可以使用位于 FullyQualifiedAnnotationBeanNameGenerator 包中的 org.springframework.context.annotation 来实现此目的。 |
-
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>
通常情况下,当其他组件可能需要显式引用该名称时,请考虑使用注解来指定名称。另一方面,当容器负责自动装配(wiring)时,自动生成的名称就已足够。
为自动检测的组件提供作用域
与一般的 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 的作用域(例如“request”或“session”)的详细信息,请参阅请求、会话、应用和 WebSocket 作用域。与这些作用域的预置注解类似,您也可以使用 Spring 的元注解(meta-annotation)方式组合自己的作用域注解:例如,一个使用 @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一节中有详细说明。
为此,no 元素提供了一个 interfaces 属性。该属性有三个可能的取值:targetClass、5 和 6。例如,以下配置将生成标准的 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 中 qualifier 元素的 meta 或 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 在其限定符元数据上有所差异,因为该元数据是按实例(per-instance)提供的,而不是按类(per-class)提供的。 |
生成候选组件索引
虽然类路径扫描速度非常快,但通过在编译时创建一个静态的候选组件列表,可以进一步提升大型应用的启动性能。在此模式下,所有作为组件扫描目标的模块都必须使用此机制。
您现有的 @ComponentScan 或 <context:component-scan/> 指令必须保持不变,以请求上下文在特定包中扫描候选组件。当 ApplicationContext 检测到此类索引时,它会自动使用该索引,而不是扫描类路径。 |
要生成索引,请为每个包含作为组件扫描指令目标的组件的模块添加一个额外的依赖项。以下示例展示了如何在 Maven 中实现这一点:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<version>6.0.23</version>
<optional>true</optional>
</dependency>
</dependencies>
对于 Gradle 4.5 及更早版本,应将依赖项声明在 compileOnly 配置中,如下例所示:
dependencies {
compileOnly "org.springframework:spring-context-indexer:6.0.23"
}
对于 Gradle 4.6 及更高版本,应将依赖项声明在 annotationProcessor 配置中,如下例所示:
dependencies {
annotationProcessor "org.springframework:spring-context-indexer:6.0.23"
}
spring-context-indexer 工件会生成一个 META-INF/spring.components 文件,并将其包含在 jar 文件中。
在 IDE 中使用此模式时,必须将 spring-context-indexer 注册为注解处理器,以确保在候选组件更新时索引保持最新。 |
当在类路径中找到 META-INF/spring.components 文件时,索引会自动启用。如果某些库(或用例)的索引部分可用,但无法为整个应用程序构建索引,您可以通过将 spring.index.ignore 设置为 true 回退到常规的类路径安排(就像完全不存在索引一样),既可以作为 JVM 系统属性设置,也可以通过 SpringProperties 机制进行设置。 |