Spring IoC容器:类路径扫描和管理组件(Classpath Scanning and Managed Components)

https://docs.spring.io/spring-framework/reference/core/beans/classpath-scanning.html

这一节描述了一种通过扫描类路径隐式检测候选组件的选项。候选组件是匹配过滤条件的类,并且在容器中注册了相应的bean定义。这样就无需使用XML来执行bean注册。相反,你可以使用注解(例如,@Component)、AspectJ类型表达式或自定义的过滤条件来确定哪些类的bean定义应该注册到容器中。

你可以使用Java而不是XML文件来定义beans。

Component 和其它类型注解(Stereotype Annotations)

@Repository 注解是任何满足仓库角色或类型(也称为数据访问对象或DAO)的类的标记。这个标记的使用包括自动异常转换。

Spring 提供了进一步的类型注解:@Component@Service@Controller@Component 是任何 Spring 管理组件的通用类型。@Repository@Service@Controller 分别是 @Component 在持久层、服务层和表现层中的特定用例的专门化。因此,你可以使用 @Component 来注解你的组件类,但是,通过使用 @Repository@Service@Controller 来替代,你的类更适合于工具处理或与切面关联。例如,这些类型注解是切入点的理想目标。在未来的 Spring 框架版本中,@Repository@Service@Controller 可能还会携带额外的语义。因此,如果你在选择使用 @Component 还是 @Service 来注解你的服务层,@Service 显然是更好的选择。同样地,如前所述,@Repository 已经作为自动异常转换的标记在你的持久层中得到支持。

使用元注解和组合注解

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

@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 {
	// ...
}

自动检测类并注册 Bean 定义

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

@Service
public class SimpleMovieLister {

	private MovieFinder movieFinder;

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

要自动检测这些类并注册相应的 beans,你需要在你的 @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>元素。

扫描类路径包需要类路径中存在相应的目录条目。当你使用Ant构建JAR文件时,确保你没有激活JAR任务的files-only开关。此外,基于某些环境的安全策略,类路径目录可能不会被公开——例如,在JDK 1.7.0_45及更高版本上的独立应用程序(这需要在你的manifest中设置’Trusted-Library’)。

在JDK 9的模块路径(Jigsaw)上,Spring的类路径扫描通常能够如预期般工作。然而,确保你的组件类在module-info描述符中被导出。如果你期望Spring调用你类的非公共成员,确保它们是’opened(即,它们在module-info描述符中使用opens声明而不是exports声明)。

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

你可以通过将annotation-config属性的值设置为false来禁用AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor的注册。

使用过滤器定制扫描

默认情况下,只有带有@Component@Repository@Service@Controller@Configuration或自定义注解(本身带有@Component)的类会被检测为候选组件。然而,你可以通过应用自定义过滤器来修改和扩展这种行为。将它们作为includeFiltersexcludeFilters属性添加到@ComponentScan注解中(或者作为XML配置中的<context:component-scan>元素的<context:include-filter /><context:exclude-filter />子元素)。每个过滤器元素都需要typeexpression属性。下表描述了过滤选项:
在这里插入图片描述
在这里插入图片描述

以下示例展示了配置,它忽略了所有@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或作为<component-scan/>元素的属性提供use-default-filters="false"来禁用默认过滤器。这实际上禁用了自动检测带有@Component@Repository@Service@Controller@RestController@Configuration注解或元注解的类。

在组件内定义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定义,该定义有一个指向publicInstance()方法的工厂方法。@Bean注解标识了工厂方法和其它bean定义属性,如通过@Qualifier注解指定的限定符值。可以指定的其它方法级别的注解包括@Scope@Lazy和自定义限定符注解。

除了在组件初始化中的作用外,你还可以将@Lazy注解放在用@Autowired@Inject标记的注入点上。在这种情况下,它会导致注入一个惰性解析代理。然而,这种代理方法相当有限。对于复杂的惰性交互,特别是与可选依赖项结合使用时,推荐使用ObjectProvider<MyTargetBean>

如前所述,支持@Autowired字段和方法,同时还支持@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表达式语言元素通过#{ <expression> }表示法定义了属性的值。对于@Value注解,预配置了一个表达式解析器,在解析表达式文本时查找bean名称。

自Spring框架4.3起,你还可以将工厂方法参数声明为InjectionPoint类型(或其更具体的子类:DependencyDescriptor),以访问触发当前bean创建的请求注入点。请注意,这只适用于bean实例的实际创建,而不适用于现有实例的注入。因此,对于原型作用域的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类中的方法处理方式不同。区别在于,@Component类没有通过CGLIB增强来拦截方法和字段的调用。CGLIB代理是@Configuration类中@Bean方法内调用方法或字段创建bean元数据引用到协作对象的手段。这些方法不是用常规Java语义调用的,而是通过容器进行,以提供通常的Spring bean生命周期管理和代理,即使是通过编程调用@Bean方法引用其它bean。相比之下,在普通的@Component类中的@Bean方法里调用方法或字段具有标准Java语义,没有特殊的CGLIB处理或其它约束适用。

你可以将@Bean方法声明为静态的,这样在不创建它们包含的配置类实例的情况下也可以调用它们。这在定义后处理器(post-processor)bean(例如,类型为BeanFactoryPostProcessorBeanPostProcessor)时特别有意义,因为这些bean在容器生命周期早期初始化,并且应该避免在那时触发配置的其它部分。

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

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

@Bean方法也会在给定组件或配置类的基类上发现,以及在由组件或配置类实现的接口中声明的Java 8默认方法上发现。这允许在组合复杂配置安排时具有很大的灵活性,甚至通过Java 8默认方法在Spring 4.2及更高版本中实现多重继承。

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

命名自动检测到的组件

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

默认情况下,使用AnnotationBeanNameGenerator。对于Spring的stereotype注解,如果你通过注解的value属性提供了一个名称,那么该名称将被用作相应bean定义中的名称。当使用以下JSR-250和JSR-330注解代替Spring stereotype注解时,也适用此约定:@jakarta.annotation.ManagedBean, @javax.annotation.ManagedBean, @jakarta.inject.Named, 和 @javax.inject.Named

自Spring框架6.1起,用于指定bean名称的注解属性名称不再要求是value。自定义stereotype注解可以声明一个不同名称的属性(如name),并用@AliasFor(annotation = Component.class, attribute = "value")注解该属性。

自Spring框架6.1起,基于约定的stereotype名称支持已被弃用,并将在未来版本的框架中移除。因此,自定义stereotype注解必须使用@AliasFor@Component中的value属性声明一个显式别名。

如果无法从这样的注解中派生出显式的bean名称,或者对于任何其它检测到的组件(如通过自定义过滤器发现的),默认的bean名称生成器将返回未大写的非限定类名。例如,如果检测到以下组件类,名称将是myMovieListermovieFinderImpl

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

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

如果你遇到由于多个自动检测到的组件具有相同的非限定类名(即,名称相同但位于不同包中的类)而引起的命名冲突,你可能需要配置一个默认使用完全限定类名生成bean名称的BeanNameGenerator。自Spring框架5.2.3起,位于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>

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

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

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

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

@Scope注解仅在具体bean类(对于带注解的组件)或工厂方法(对于@Bean方法)上进行自我检查。与XML bean定义相比,没有bean定义继承的概念,类级别的继承层次结构对于元数据目的是不相关的。

要提供自定义的作用域解析策略,而不是依赖于基于注解的方法,你可以实现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>

当使用某些非singleton作用域时,可能需要为作用域内的对象生成代理。为此,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注解和自定义限定符注解,在解析自动装配候选者时提供细粒度的控制。由于那些示例基于XML bean定义,所以限定符元数据是通过在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提供其限定符元数据的变化,因为该元数据是按实例提供的,而不是按类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值