1. 概述
本文深入讲解 Spring 中的组件扫描(Component Scanning)机制。在使用 Spring 时,我们通常通过注解(如 @Component
、@Service
等)将普通类标记为 Spring Bean。而 Spring 需要知道去哪里“找”这些被注解的类 —— 这就是组件扫描的核心作用。
✅ 默认情况下,Spring 会从配置类所在的包开始,递归扫描其所有子包。
⚠️ 但并不是所有加了注解的类都必须被加载为 Bean,我们可以通过配置精准控制扫描范围,避免加载无用类,提升启动性能。
接下来,我们将从默认行为讲起,逐步介绍如何自定义扫描路径、排除特定类,以及一些容易踩坑的注意事项。
2. 无参的 @ComponentScan
2.1 在 Spring 应用中使用 @ComponentScan
在标准 Spring 项目中,我们通常将 @ComponentScan
与 @Configuration
一起使用,用来指定 Spring 容器应扫描哪些包下的组件。
当 @ComponentScan
不带任何参数时,Spring 会自动扫描该配置类所在包及其所有子包。
假设我们的配置类位于 com.example.componentscan.springapp
包下:
@Configuration
@ComponentScan
public class SpringComponentScanApp {
private static ApplicationContext applicationContext;
@Bean
public ExampleBean exampleBean() {
return new ExampleBean();
}
public static void main(String[] args) {
applicationContext =
new AnnotationConfigApplicationContext(SpringComponentScanApp.class);
for (String beanName : applicationContext.getBeanDefinitionNames()) {
System.out.println(beanName);
}
}
}
同时,我们在子包中定义了两个组件:
package com.example.componentscan.springapp.animals;
// ...
@Component
public class Cat {}
package com.example.componentscan.springapp.animals;
// ...
@Component
public class Dog {}
以及一个花类组件:
package com.example.componentscan.springapp.flowers;
// ...
@Component
public class Rose {}
运行 main()
方法后,输出如下:
springComponentScanApp
cat
dog
rose
exampleBean
✅ 解释:
springComponentScanApp
是配置类本身,由于@Configuration
本质也是@Component
,所以它也被注册为 Bean。cat
、dog
、rose
来自被扫描到的子包。exampleBean
是通过@Bean
手动注册的。
⚠️ 关键点:组件扫描的起点是 @Configuration
类所在的包,而不是主启动类的位置。即使主类和配置类分离,扫描依然从配置类包开始。
此外,@ComponentScan
无参时等价于:
@ComponentScan(basePackages = "com.example.componentscan.springapp")
其中 basePackages
指定一个或多个要扫描的根包。
2.2 在 Spring Boot 应用中使用 @ComponentScan
Spring Boot 的便利之处在于很多配置是隐式生效的。核心注解 @SpringBootApplication
实际上是三个注解的组合:
@Configuration
@EnableAutoConfiguration
@ComponentScan
这意味着,只要使用 @SpringBootApplication
,组件扫描就已经默认开启。
我们创建一个类似的结构,主类位于 com.example.componentscan.springbootapp
:
package com.example.componentscan.springbootapp;
// ...
@SpringBootApplication
public class SpringBootComponentScanApp {
private static ApplicationContext applicationContext;
@Bean
public ExampleBean exampleBean() {
return new ExampleBean();
}
public static void main(String[] args) {
applicationContext = SpringApplication.run(SpringBootComponentScanApp.class, args);
checkBeansPresence(
"cat", "dog", "rose", "exampleBean", "springBootComponentScanApp");
}
private static void checkBeansPresence(String... beans) {
for (String beanName : beans) {
System.out.println("Is " + beanName + " in ApplicationContext: " +
applicationContext.containsBean(beanName));
}
}
}
其他组件类结构保持一致。
输出结果:
Is cat in ApplicationContext: true
Is dog in ApplicationContext: true
Is rose in ApplicationContext: true
Is exampleBean in ApplicationContext: true
Is springBootComponentScanApp in ApplicationContext: true
⚠️ 注意:这里我们没有打印所有 Bean,因为 Spring Boot 的 @EnableAutoConfiguration
会根据 classpath 自动注册大量基础设施 Bean(比如数据源、Web MVC 相关等),导致输出过长。我们只验证关键 Bean 是否存在即可。
3. 带参数的 @ComponentScan
有时候默认扫描范围太宽,我们需要更精细地控制。例如:排除某个包,或只扫描特定几个包。
3.1 指定具体扫描包
如果我们想只扫描动物相关的组件,排除 Rose
,可以显式指定 basePackages
:
@ComponentScan(basePackages = "com.example.componentscan.springapp.animals")
@Configuration
public class SpringComponentScanApp {
// ...
}
此时输出为:
springComponentScanApp
cat
dog
exampleBean
✅ 解释:
cat
和dog
属于animals
包,被成功扫描。rose
所在的flowers
包未被包含,因此不会注册。
该方式同样适用于 Spring Boot,只需在 @SpringBootApplication
旁加上自定义 @ComponentScan
:
@SpringBootApplication
@ComponentScan(basePackages = "com.example.componentscan.springbootapp.animals")
⚠️ 注意:@SpringBootApplication
已经自带 @ComponentScan
,如果重复添加,会覆盖默认行为。因此一旦自定义,就必须明确写出所有需要扫描的包。
3.2 扫描多个包
Spring 支持同时扫描多个包,方式如下:
使用字符串数组(推荐):
@ComponentScan(basePackages = {
"com.example.componentscan.springapp.animals",
"com.example.componentscan.springapp.flowers"
})
从 Spring 4.1.1 起,也支持用分隔符写成单个字符串:
@ComponentScan(basePackages = "com.example.componentscan.springapp.animals;com.example.componentscan.springapp.flowers")
@ComponentScan(basePackages = "com.example.componentscan.springapp.animals,com.example.componentscan.springapp.flowers")
@ComponentScan(basePackages = "com.example.componentscan.springapp.animals com.example.componentscan.springapp.flowers")
✅ 分号、逗号、空格均可作为分隔符,效果一致。但建议使用数组形式,更清晰且 IDE 支持更好。
3.3 使用过滤器排除特定类
除了按包路径控制,Spring 还提供了强大的过滤机制,通过 includeFilters
和 excludeFilters
灵活控制扫描结果。
例如,我们想排除 flowers
包下的所有类,可以使用正则表达式:
@ComponentScan(excludeFilters =
@ComponentScan.Filter(type = FilterType.REGEX,
pattern = "com\\.example\\.componentscan\\.springapp\\.flowers\\..*"))
或者更精准地排除某个具体类:
@ComponentScan(excludeFilters =
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = Rose.class))
✅ FilterType
支持多种类型:
ANNOTATION
:根据注解类型过滤ASSIGNABLE_TYPE
:根据类是否继承/实现某类型ASPECTJ
:使用 AspectJ 表达式REGEX
:正则匹配类名CUSTOM
:自定义过滤逻辑
⚠️ 过滤器功能强大,但过度使用会让配置变得晦涩,建议优先通过包结构设计来解耦,而非依赖复杂过滤。
4. 避免使用默认包(default package)
不要将 @Configuration
类放在默认包下(即不写 package
声明)。
❌ 错误示例:
// 没有 package 声明
@Configuration
@ComponentScan
public class AppConfig { }
⚠️ 问题:Spring 会尝试扫描 classpath 下所有 JAR 包中的类,导致:
- 启动极慢
- 可能加载冲突类
- 应用无法正常启动
✅ 正确做法:始终为类指定明确的包名,如 com.example.config
。
这在 Spring Boot 官方文档中也有明确建议,属于典型的“看似省事,实则踩坑”操作。
5. 总结
本文系统梳理了 Spring 组件扫描的核心机制:
✅ 默认行为:从 @Configuration
类所在包开始,递归扫描所有子包。
✅ 自定义方式:通过 basePackages
指定扫描路径,支持多个包。
✅ 精细控制:利用 excludeFilters
/ includeFilters
结合多种 FilterType
实现高级过滤。
❌ 避坑提醒:切勿将配置类放在默认包中。
合理使用组件扫描,不仅能提升应用启动效率,还能增强模块间解耦。建议结合清晰的包结构设计,让扫描逻辑更直观、可维护。
示例代码已整理至 GitHub:https://github.com/example/spring-component-scan-demo