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。
  • catdogrose 来自被扫描到的子包。
  • 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

✅ 解释:

  • catdog 属于 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 还提供了强大的过滤机制,通过 includeFiltersexcludeFilters 灵活控制扫描结果。

例如,我们想排除 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


原始标题:Spring Component Scanning