1.10 类路径扫描和托管组件

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

本节介绍通过扫描类路径隐式检测候选组件的选项。候选组件是与筛选条件相匹配的类,并在容器中注册了相应的bean定义。这样就不需要使用XML来执行bean注册。相反,你可以使用注解(例如@Component)、AspectJ类型表达式或你自己的自定义筛选条件来选择哪些类具有向容器注册的bean定义。

从Spring3.0开始,Spring JavaConfig项目提供的许多特性都是核心Spring框架的一部分。这允许你使用Java定义bean,而不是使用传统的XML文件。查看@Configuration、@Bean、@Import和@DependsOn注解,了解如何使用这些新功能的示例。

1.10.1 @Component和进一步的构造型注解

@Repository注解是一种类的一个标记,这些类实现了存储库的角色或原型(也称为数据访问对象或DAO)。

Spring提供了更多的原型注解:@Component、@Service和@Controller。@Component是任何Spring管理的组件的通用原型。@Repository、@Service和@Controller是@Component针对更具体的用例(分别在持久性、服务和表示层中)的特殊注解。因此,你可以用@Component注解组件类,但是,通过用@Repository、@Service或@Controller注解它们,你的类将更适合通过工具处理或与aspects关联。

例如,这些构造型注解是切入点的理想目标。@Repository、@Service和@Controller也可以在Spring框架的未来版本中附加语义。因此,如果你选择在服务层中使用@Component或@Service,@Service显然是更好的选择。同样,正如前面所述,@Repository已经被支持作为持久性层中自动异常转换的标记。

1.10.2 使用元注解和组合注解

大多数Spring的注解都可以作为元注解用在你自定的注解中。所谓元注解就是可以用在其他注解中的注解。 像之前提到的@Component就是@Service的元注解。如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @Interface Service {
// ....
}

@Component会导致@Service和@Component一样被对待。

你也可以合并使用元注解。比如:在Spring MVC中的@RestController就是由@Controller和@ResponseBody组成的。 此外,组合注解还可以选择从元注解中重新声明属性以允许自定义。当你只想公开元注解属性的一个子集时,这尤其有用。例如,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 {
// ...
}

1.10.3 自动检测类并注册bean定义

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

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

要自动检测这些类并注册相应的bean,需要将@ComponentScan添加到@Configuration类中,其中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包需要在classpath中存在相应的目录条目。当你使用Ant构建JAR时,请确保你没有激活JAR任务的仅限文件的开关。此外,在某些环境中,根据安全策略,类路径目录可能不会公开-例如,JDK 1.7.0_45及更高版本上的独立应用程序(这需要在清单中设置“可信库”)请参见https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。

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

此外,在使用component-scan元素时,AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor都是隐式被使用的。这意味着这两个组件都是自动检测并连接在一起的-都没有XML中提供任何bean配置元数据。

通过将annotation-config属性的值设置为false,可以禁用AutoWiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor的注册。

1.10.4. 使用Filters来定制化扫描

默认情况下,用@Component、@Repository、@Service、@Controller注解的类或用@Component注解的自定义注解是唯一检测到的候选组件。但是,可以通过应用自定义筛选器来修改和扩展此行为。将它们作为@Component scan注解的includefilters或excludefilters参数添加(或作为component scan元素的include filter或exclude filter子元素添加)。每个筛选器元素都需要类型和表达式属性。下表介绍了筛选选项:

Filter type

表达式例子

描述

annotation(默认)

org.example.SomeAnnotation

type基本的目标组件

assignable

org.example.SomeClass

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

aspectj

org.example..*Service+

匹配目标组件的AspectJ类型

regex

org\.example\.Default.*

匹配目标主键内名的正则表达式

custom

org.example.MyTypeFilter

org.springframework.core.type .TypeFilter的自定义实现

下面例子展示了忽略 @Repository 并且使用“stub” repositories 的注解:

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

你也可以禁用默认的filters,通过设置useDefaultFilters=false在注解中或者设置<component-scan/> 中use-default-filters="false"的属性。 这样就会禁用@Component, @Repository, @Service, @Controller, 和@Configuration 注解.

1.10.5 在组件内部定义Bean元数据

Spring组件还可以为容器提供bean定义元数据。你可以使用用于在@Configuration annotated类中定义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和自定义限定符批注。

除了其用于组件初始化的角色之外,你还可以将@Lazy注解放在标有@Autowired或@Inject的注入点上。在这种情况下,它会导致注入延迟解析代理。

如前所述,支持自动装载的字段和方法,并额外支持@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);
}
}

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

从SpringFramework4.3开始,还可以声明InjectionPoint类型的工厂方法参数(或其更具体的子类:DependencyDescriptor)以访问触发当前bean创建的请求注入点。

注意,这只适用于bean实例的实际创建,而不适用于现有实例的注入。因此,这个特性对于原型范围的bean最有意义。对于其他作用域,factory方法只会看到触发在给定作用域中创建新bean实例的注入点(例如,触发惰性单例bean创建的依赖项)。在这种情况下,你可以使用提供的注入点元数据和语义关注。下面的示例演示如何使用InjectionPoint:

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

常规Spring组件中的@Bean方法的处理方式与[email protected]类中的对应方法不同。不同的是,@Component类没有用cglib来增强以截获方法和字段的调用。cglib代理是调用@Configuration classes中通过@Bean methods内的方法或字段创建对协作对象的bean元数据引用的方法。

即使通过引用对@Bean方法的编程调用来引用其他bean,这也不是普通的Java语义调用,而是通过容器来提供通常的生命周期管理和Spring bean的代理,相反,在普通的@Component类中调用“bean”方法中具有标准Java语义的方法或字段,没有特殊的CGLIB处理或其他约束应用。

你可以将@Bean方法声明为静态方法,允许在不将其包含的配置类创建为实例的情况下调用它们。在定义post-processor bean(例如,BeanFactoryPostProcessor或BeanPostProcessor类型)时,这是特别有意义的,因为这样的bean在容器生命周期的早期就被初始化,应该避免在此时触发配置的其他部分。

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

“@Bean”方法的Java语言可见性不会对Spring容器中的bean定义产生直接影响。你可以自由地声明你认为适合非@Configuration类以及任何静态方法的工厂方法。但是,@Configuration类中的常规@Bean方法必须是可重写的,也就是说,它们不能声明为私有或最终的。

“@Bean”方法也在给定组件或配置类的基类上发现,以及在由组件或配置类实现的接口中声明的Java 8默认方法上。这允许在组成复杂的配置安排方面有很大的灵活性,即使是多个继承也可以通过Java 8默认方法(如Spring 4.2)来实现。

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

1.10.6. 为自动检测组件命名

当一个组件作为扫描过程的一部分被自动检测时,它的bean名称由该扫描仪已知的BeanNameGenerator策略生成。默认情况下,任何包含value的Spring原型注解(@Component、@Repository、@Service和@Controller)都会将该名称提供给相应的bean定义。

如果这样的注解不包含value或任何其他检测到的组件(例如那些由自定义过滤器发现的组件),则默认bean名称生成器将返回小写的非限定类名。例如,如果检测到以下组件类,则名称为myMovieLister和movieFinderImpl:

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

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

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

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

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

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

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

@Scope注解只在具体的bean类(对于带注解的组件)或工厂方法(对于@Bean方法)上进行内省。与XML bean定义不同,没有bean定义继承的概念,并且类级别的继承层次结构与元数据无关。

有关特定于Web的范围(如Spring上下文中的“请求”或“会话”)的详细信息,请参阅请求、会话、应用程序和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>

当使用某些非单例作用域时,可能需要为作用域对象生成代理。为此,组件扫描元素上可以有一个scoped-proxy 属性。三个可能的值是:no、interfaces和targetClass。例如,以下配置将生成标准JDK动态代理:

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

1.10.8 为限定符元数据提供注解

@Qualifier注解将在基于微调注解的带有限定符的自动注入中讨论。该部分中的示例演示了在解析autowire候选时使用@Qualifier注解和自定义限定符注解来提供细粒度控制。因为这些示例是基于XML bean定义的,所以通过使用XML中bean元素的限定符或元子元素,在候选bean定义上提供了限定符元数据。当依赖类路径扫描自动检测组件时,可以在候选类上为限定符元数据提供类型级注解。以下三个示例演示了此技术:

@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可以有多个不同的限定符,因为元数据是按实例而不是按class提供的。

1.10.9 生成候选组件的索引

虽然类路径扫描速度非常快,但是可以通过在编译时创建一个静态候选列表来提高大型应用程序的启动性能。在此模式下,作为组件扫描目标的所有模块都必须使用此机制。

你现有的@ComponentScan或<context:component-scan>指令必须保持原样,以请求上下文扫描某些包中的候选项。当ApplicationContext检测到这样的索引时,它会自动使用它,而不是扫描类路径。

要生成索引,请向每个模块添加一个附加依赖项,该模块包含作为组件扫描指令目标的组件。下面的示例说明如何使用Maven:

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<version>5.1.8.RELEASE</version>
<optional>true</optional>
</dependency>
</dependencies>

在Gradle 4.5或者更早版本,依赖必须配置在compileOnly中,如下:

dependencies {
compileOnly "org.springframework:spring-context-indexer:5.1.8.RELEASE"
}

在Gradle 4.6或者以后版本,依赖必须配置在annotationProcessor中,如下:

dependencies {
annotationProcessor "org.springframework:spring-context-indexer:5.1.8.RELEASE"
}

这个过程生成一个包含在JAR文件中的META-INF/spring.components文件。

在你的IDE中使用此模式时,spring-context-indexer必须注册为注解处理器,以确保在更新候选组件时索引是最新的。

当在类路径上找到META-INF/Spring.components时,索引将自动启用。如果某个索引部分可用于某些库(或用例),但无法为整个应用程序生成,则可以通过将spring.index.ignore设置为true(作为系统属性或类路径根目录下的spring.properties文件)来回滚到常规类路径安排(就像根本没有索引一样)。