环境:Spring Boot3.2.5
1. 简介
在Spring中,@ComponentScan注解是一个强大且常用的工具,但很多人对其功能和使用方法可能只是一知半解。本文将深入探讨@ComponentScan注解的各个方面,揭示其隐藏的功能和最佳实践,帮助读者彻底掌握这一重要注解。
@ComponentScan注解,用于自动检测并注册带有@Component、@Service、@Repository和@Controller等注解的类为Spring Bean。虽然许多开发者都熟悉它的基本用法,但其背后的复杂性和灵活性却往往被忽视。
本篇文章中我将详细的介绍@ComponentScan的各种配置选项,包括如何指定基础包、排除特定组件、自定义过滤器以及处理代理和作用域等配置项的使用。
首先,我们先准备以下要使用到的类
package com.pack.ioc.scan_component
@Component
public class A {}
package com.pack.ioc.scan_component.child
@Component
public class B {}
// 配置类
package com.pack.ioc.scan_component
@Configuration
@ComponentScan
public class AppConfig {
}
接下来,准备一个运行的Main类
public class ComponentScanTest {
public static void main(String[] args) {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationCo
context.registerBean(AppConfig.class) ;
context.refresh() ;
// 打印当前容器中注册的所有bean集合
System.out.println(Arrays.toString(context.getBeanDefinitionNames())) ;
}
}
}
在后续的每一个示例演示中都会基于上面定义的类进行,这过程中我们还会创建更多的类。
2. 实战案例
2.1 基本使用
在上面准备的基本环境中,在AppConfig类是仅仅是添加了@ComponentScan注解,没有配置任何的选项,运行程序输出如下:
[..., com.pack.ioc.scan_component.AppConfig, a, b]
这里将我们自定义的类都打印了(这里省去Spring容器自动注册bean)。根据运行结果看出,虽然A,B不再同一个包中,但是B所在的包是AppConfig所在的包的子包中,所以@ComponentScan会处理当前包及其子包中的类。
2.2 指定basePackages
@ComponentScan(basePackages = {"com.pack.ioc.scan_component.child"})
public class AppConfig {}
这里只是指定了一个子包,运行结果:
[..., com.pack.ioc.scan_component.AppConfig, b]
此时,A类不会包含在内。
2.3 指定basePackageClasses
@ComponentScan(basePackageClasses = {B.class})
public class AppConfig {}
这里可以通过指定Class对象,这样会根据该Class对象所在的包进行扫描,运行结果
[..., com.pack.ioc.scan_component.AppConfig, b]
Spring内部会自动将你配置的所有Class对象所在的包,作为扫描路径。
2.4 指定includeFilters
注:@ComponentScan注解还包括excludeFilters排除过滤属性,这里我们只介绍includeFilters属性。
由于includeFilters中配置的是@Filter,而该注解中定义了type属性,该type属性是FilterType枚举类,其中包括以下值:
public enum FilterType {
ANNOTATION,
ASSIGNABLE_TYPE,
ASPECTJ,
REGEX,
CUSTOM
}
接下来,我们将依次介绍这些枚举值。
- 过滤类型FilterType.ANNOTATION
在目标类上存在指定的注解。
首先,我们自定义一个注解,然后通过该注解标准类。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Pack {
}
// 使用该注解标注下面的类
@Pack
public class C {
}
在默认情况下,Spring容器是不会识别到C这个类的,通过如下配置后就不一样了
@ComponentScan(includeFilters = {
@Filter(type = FilterType.ANNOTATION, classes = {Pack.class})
})
public class AppConfig {}
运行结果
[..., com.pack.ioc.scan_component.AppConfig, a, c, b]
Spring默认的@Component注解及我们自定义的@Pack注解都生效了。
- 过滤类型FilterType.ASPECTJ
与目标类匹配的AspectJ类型表达式。
首先,自定义如下类。
public class PackClass {
}
修改配置类
@ComponentScan(includeFilters = {
@Filter(
type = FilterType.ASPECTJ,
pattern = {"com.pack..Pack*"}
)
})
public class AppConfig {}
com.pack..Pack*:com.pack包及子包下的以Pack开头的类都将匹配。
运行结果
[..., com.pack.ioc.scan_component.AppConfig, a, b, packClass]
注:在本例中@ComponentScan扫描的包还是当前AppConfig所在的包及其子包。
- 过滤类型FilterType.ASSIGNABLE_TYPE
目标类可以是这里给定的类或接口。
首先,自定义DAO接口。
public interface DAO {
}
public class Person implements DAO {
}
修改配置类
@ComponentScan(includeFilters = {
@Filter(
type = FilterType.ASSIGNABLE_TYPE, classes = {DAO.class}
)
})
public class AppConfig {}
只要我们定义的类实现了DAO接口都将匹配。
运行结果
[..., com.pack.ioc.scan_component.AppConfig, a, b, person]
通过classes你可以指定多个接口或者是父类。
- 过滤类型FilterType.REGEX
与目标组件的类名匹配的正则表达式。
@ComponentScan(includeFilters = {
@Filter(
type = FilterType.REGEX,
pattern = {"com\\.pack\\..*.Pack.*"}
)
})
public class AppConfig {}
运行结果
[..., com.pack.ioc.scan_component.AppConfig, a, b, packClass]
注意:在写正则表达式时 "." 需要进行转义。
- 过滤类型FilterType.CUSTOM
org.springframework.core.type.TypeFilter接口的自定义实现。
public class PackTypeFilter implements TypeFilter {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata() ;
return annotationMetadata.hasAnnotation(Pack.class.getName()) ;
}
}
在自定义的类型过滤中,我们判断类上是否存在@Pack。
修改配置类
@ComponentScan(includeFilters = {
@Filter(
type = FilterType.CUSTOM,
classes = {PackTypeFilter.class}
)
})
public class AppConfig {}
这不仅包括了内置的过滤器会生效(过滤@Component注解),同时我们自定义的过滤器也会起到作用。
以上针对每一种过滤类型讲解完了,你是否有点晕,这里指定了type后,接下来是配置pattern还是classes属性呢?大家记住如下组合即可。
类型 | 配置的属性 |
ANNOTATION、ASSIGNABLE_TYPE、CUSTOM | classes |
ASPECTJ、REGEX | pattern |
只有这两种组合没有其它组合。
2.5 指定lazyInit
指定扫描到的类是否进行延迟初始化。
@Component
public class A implements InitializingBean {
@PostConstruct
public void init() {
System.err.println("A init...") ;
}
@Override
public void afterPropertiesSet() throws Exception {
System.err.println("A afterPropertiesSet...") ;
}
}
修改配置类
@ComponentScan(lazyInit = true)
public class AppConfig {}
运行结果
图片
并没有打印在A类中定义的初始化回调。
2.6 指定useDefaultFilters
指示是否应启用对带有@Component、@Repository、@Service或@Controller注解的类的自动检测。
修改配置类
@ComponentScan(
useDefaultFilters = false,
includeFilters = {
@Filter(
type = FilterType.CUSTOM,
classes = {PackTypeFilter.class}
)
})
public class AppConfig {}
运行结果
[..., com.pack.ioc.scan_component.AppConfig, c]
我们定义的A,B两个类都没有打印(这2个类是有@Component注解的)。这说明我们就关闭了默认的过滤器功能。
2.7 指定nameGenerator
通过指定该属性,扫描到的组件将通过该值来生成对应的beanName。
自定义BeanNameGenerator
public class PackBeanNameGenerator extends AnnotationBeanNameGenerator {
@Override
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
String beanName = super.generateBeanName(definition, registry) ;
beanName = beanName.replaceFirst("^.", beanName.substring(0, 1).toUpperCase()) ;
return "pack" + beanName ;
}
}
在这里我们将所有的beanName都添加一个 pack 前缀。
修改配置类
@ComponentScan(nameGenerator = PackBeanNameGenerator.class)
public class AppConfig {}
运行结果
[..., com.pack.ioc.scan_component.AppConfig, packA, packB]
除配置类外,其它beanName都以pack开头。
2.8 指定scopedProxy
指示是否应为检测到的组件生成代理。
首先,修改A类如下:
@Scope
@Component
public class A implements InitializingBean {}
在类A上添加了@Scope作用域注解。注:要使得scopedProxy生效你必须对bean定义作用域(只要有该注解即可,而至于什么作用域无所谓)。
修改配置类
@ComponentScan(scopedProxy = ScopedProxyMode.TARGET_CLASS)
public class AppConfig {}
这里设置为代理目标类。
接下来,我们做如下的测试
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.registerBean(AppConfig.class) ;
context.refresh() ;
System.out.println(Arrays.toString(context.getBeanDefinitionNames())) ;
System.err.println(context.getBean(A.class).getClass().getName()) ;
}
运行结果
图片
Bean A生成的是通过cglib代理后的对象。
2.9 指定scopeResolver
用于解析检测到的组件的作用域。在默认情况下检测的是@Scope定义的作用域。
特别说明:只有当@ComponentScan中的scopedProxy属性设置为ScopedProxyMode.DEFAULT时,该属性才会生效。
接下来,通过实例讲解。
首先,我们自定义作用域注解(为了体现自定义自定义ScopeMetadataResolver的意义)。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PackScope {
@AliasFor("scopeName")
String value() default "";
@AliasFor("value")
String scopeName() default "";
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}
该自定义作用域注解与@Scope中定义的属性一致。
自定义作用域解析器。
public class PackAnnotationScopeMetadataResolver extends AnnotationScopeMetadataResolver {
public PackAnnotationScopeMetadataResolver() {
setScopeAnnotationType(PackScope.class) ;
}
}
解析器中设置我们自定义的作用域注解。
修改的类A。
@PackScope(proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class A implements InitializingBean {}
运行结果
自定义的作用域注解及自定义解析器生效了。