类路径扫描与管理组件
本章中的大多数示例使用 XML 来指定生成每个 BeanDefinition
的配置元数据,这些 BeanDefinition
存在于 Spring 容器中。前一节(基于注解的容器配置)演示了如何通过源级注解提供大量的配置元数据。即使在那些示例中,“基础” bean 定义仍然在 XML 文件中显式定义,而注解仅驱动依赖注入。本节描述了一种通过扫描类路径隐式检测候选组件的选项。候选组件是与过滤标准匹配的类,并且在容器中注册了相应的 bean 定义。这消除了使用 XML 进行 bean 注册的需要。相反,您可以使用注解(例如,@Component
)、AspectJ 类型表达式或您自己的自定义过滤标准来选择哪些类在容器中注册了 bean 定义。
您可以使用 Java 定义 beans,而不是使用 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 {
// ...
}
@Component
使得@Service
被视为与@Component
相同的处理方式。
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Component 1
annotation class Service {
// ...
}
@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
)
您可以在不声明 proxyMode
的情况下使用 @SessionScope
,如下所示:
- 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 可以自动检测被标记的类,并在 ApplicationContext
中注册相应的 BeanDefinition
实例。例如,以下两个类符合这种自动检测的条件:
- 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)。
在模块路径(Java 模块系统)上,Spring 的类路径扫描通常按预期工作。然而,请确保你的组件类在 module-info
描述符中被导出。如果你希望 Spring 调用你类的非公共成员,请确保它们是 'opened'(也就是说,它们在你的 module-info
描述符中使用 opens
声明而不是 exports
声明)。
此外,当您使用 component-scan 元素时,AutowiredAnnotationBeanPostProcessor
和 CommonAnnotationBeanPostProcessor
都会被隐式包含。这意味着这两个组件会被自动检测并连接在一起 — 这一切都不需要在 XML 中提供任何 bean 配置元数据。
您可以通过包含 annotation-config
属性并将其值设置为 false
来禁用 AutowiredAnnotationBeanPostProcessor
和 CommonAnnotationBeanPostProcessor
的注册。
使用过滤器自定义扫描
默认情况下,带有 @Component
、@Repository
、@Service
、@Controller
、@Configuration
注解的类,或者自定义注解(该注解本身带有 @Component
)是唯一被检测到的候选组件。然而,您可以通过应用自定义过滤器来修改和扩展此行为。将它们作为 @ComponentScan
注解的 includeFilters
或 excludeFilters
属性添加(或作为 XML 配置中 <context:component-scan>
元素的 <context:include-filter />
或 <context:exclude-filter />
子元素)。每个过滤器元素需要 type
和 expression
属性。下表描述了过滤选项:
表 1. 过滤器类型
过滤器类型 | 示例表达式 | 描述 |
---|---|---|
annotation (默认) | org.example.SomeAnnotation | 一个注解,必须在目标组件的类型级别上 存在 或 元存在。 |
assignable | org.example.SomeClass | 目标组件可赋值的类(或接口)(扩展或实现)。 |
aspectj | org.example..*Service+ | 一个 AspectJ 类型表达式,目标组件需要匹配该表达式。 |
regex | org\.example\.Default.* | 一个正则表达式,目标组件的类名需要匹配该表达式。 |
custom | org.example.MyTypeFilter | org.springframework.core.type.TypeFilter 接口的自定义实现。 |
以下示例显示了忽略所有 @Repository
注解并使用“存根”仓库的配置:
- 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 定义元数据。您可以使用与在 @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(例如,类型为 BeanFactoryPostProcessor
或 BeanPostProcessor
的 bean)时特别有意义,因为这些 bean 在容器生命周期的早期被初始化,并且此时应该避免触发配置的其他部分。
对静态 @Bean
方法的调用永远不会被容器拦截,即使在 @Configuration
类中也是如此(如本节前面所述),这是由于技术限制:CGLIB 子类化只能覆盖非静态方法。因此,直接调用另一个 @Bean
方法具有标准 Java 语义,导致从工厂方法本身直接返回一个独立的实例。
@Bean
方法的 Java 语言可见性对 Spring 容器中生成的 bean 定义没有直接影响。您可以在非 @Configuration
类中自由声明您的工厂方法,也可以在任何地方声明静态方法。然而,@Configuration
类中的常规 @Bean
方法需要是可重写的 — 也就是说,它们不能被声明为 private
或 final
。
@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 名称生成器将返回未大写的非限定类名。例如,如果检测到以下组件类,则名称将是 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 名称的完全限定类名。位于包 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 上下文中“请求”或“会话”等 Web 特定作用域的详细信息,请参见 Request, Session, Application, and WebSocket Scopes。与这些作用域的预构建注解一样,您还可以通过使用 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 属性。三个可能的值是:no
、interfaces
和 targetClass
。例如,以下配置将产生标准的 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
注解在 Fine-tuning Annotation-based Autowiring with Qualifiers 中进行了讨论。该部分中的示例演示了如何使用 @Qualifier
注解和自定义限定符注解,以便在解析自动装配候选者时提供细粒度的控制。由于这些示例基于 XML bean 定义,因此限定符元数据是通过使用 XML 中 bean
元素的 qualifier
或 meta
子元素提供的。当依赖于类路径扫描进行组件的自动检测时,可以通过在候选类上使用类型级注解来提供限定符元数据。以下三个示例演示了这种技术:
- 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 提供其限定符元数据的变体,因为该元数据是按实例提供的,而不是按类提供的。