@ComponentScan 是怎么扫描到 @Component 的?扫描过程的原理是什么?
简化版
@ComponentScan 的作用是「告诉 Spring 去哪些包下找带 @Component(及其派生注解 @Service/@Repository/@Controller)的类,把它们注册成 Bean」。它的扫描原理:① 确定扫描的包路径(basePackages,不写就默认扫描 @ComponentScan 所在类的包及子包);② 把包路径转成类路径下的资源模式(如 com/example/**/*.class),用 ResourcePatternResolver 找出所有 .class 文件;③ 用 ASM 读取每个 .class 的字节码(注意:不是用类加载器加载!而是直接读字节码的元数据,避免过早加载/初始化大量类),判断它有没有 @Component 系注解;④ 通过过滤器(includeFilters/excludeFilters)筛选,符合的类解析成 BeanDefinition 注册到容器。关键点:扫描用 ASM 读字节码而非类加载器,是为了性能和避免副作用(几千个 class 全用类加载器加载会很慢、还会触发静态初始化);@SpringBootApplication 里就内置了 @ComponentScan,默认扫描主类所在包,所以「Bean 要放在主类同级或子包下」才能被扫到。
详细版
@ComponentScan 的核心配置:
| 配置 | 作用 |
|---|---|
| basePackages | 扫描哪些包(不写默认扫描当前类所在包及子包) |
| basePackageClasses | 用类来指定包(类型安全,重构不会漏) |
| includeFilters | 额外包含哪些(配合 useDefaultFilters) |
| excludeFilters | 排除哪些(如 Spring Boot 排除 @Configuration) |
| useDefaultFilters | 是否默认扫 @Component 系注解(默认 true) |
| lazyInit | 扫描出的 Bean 是否都懒加载 |
扫描的核心流程:
@ComponentScan("com.example")
↓
① 包路径 → 资源模式:classpath*:com/example/**/*.class
↓
② ResourcePatternResolver 找出所有匹配的 .class 文件资源
↓
③ 对每个 .class:用 ASM(MetadataReader)读字节码元数据
(不用类加载器!只读注解/类信息,不加载不初始化)
↓
④ 判断是否带 @Component 系注解 + 过 includeFilters/excludeFilters
↓
⑤ 符合的 → 生成 ScannedGenericBeanDefinition → 注册到容器
@ComponentScan(
basePackages = "com.example",
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Controller.class)
)
public class AppConfig { }
// @Component 的派生注解本质:@Service/@Repository/@Controller 上都标了 @Component
// 所以扫描时它们都被识别为组件(注解的"元注解"传递)
⚠️ 一个高频坑:Bean 没被扫到,往往是「包路径」问题。
@SpringBootApplication内置的@ComponentScan默认只扫描主启动类所在的包及其子包。如果你把某个@Service放在了主类包的「外面」(如主类在com.example.app,Service 在com.example.other),它就不会被扫到,导致「注入不到 Bean / NoSuchBeanDefinitionException」。解决:要么把 Bean 移到主类包下,要么显式配置@ComponentScan(basePackages = {...})把额外的包加进来。理解「默认扫主类所在包及子包」这条规则能避开大量「Bean 找不到」的问题。
完整版教学
一、@ComponentScan 要解决什么
@ComponentScan 解决的是「怎么自动发现并注册一大批 Bean」:
没有组件扫描的年代(纯 XML):
每个 Bean 都要手写 <bean id="userService" class="..."/>
几百个 Bean = 几百行 XML,繁琐易错
有了组件扫描:
在类上标 @Service,然后一句 @ComponentScan("com.example")
→ Spring 自动扫描这个包,把所有标了 @Component 系注解的类
注册成 Bean
→ 从"逐个手动配置"变成"标注解 + 自动发现"
@ComponentScan 的价值是「自动化 Bean 的发现和注册」——不用逐个手写配置,只需在类上标 @Service/@Component 等注解,扫描器就自动找到并注册。这是「约定优于配置」的体现。理解「@ComponentScan 自动发现并注册带 @Component 系注解的类、从手动配置变自动发现」,就理解了它的定位。
二、@Component 与派生注解的关系
理解扫描之前,先理解「为什么 @Service/@Repository/@Controller 也能被扫到」:
@Service、@Repository、@Controller 的定义上,都标了 @Component:
@Component
public @interface Service { ... }
这叫"元注解"——注解上的注解。
Spring 的扫描器识别时,会检查"这个类的注解(或注解的元注解)里
有没有 @Component" → 有就是组件。
所以:
@Service = @Component + "这是业务层"的语义
@Repository = @Component + "这是数据层"的语义(还加异常转换)
@Controller = @Component + "这是控制层"的语义(MVC 识别)
→ 它们功能上都是组件,只是语义分层,便于阅读和 AOP 切面定位
@Service/@Repository/@Controller 都是 @Component 的「派生注解」——它们的定义上标了 @Component(元注解)。扫描器检查类的注解时,会「递归」看元注解里有没有 @Component,有就当组件。所以这几个注解功能上等价于 @Component,只是语义分层(标明是业务层/数据层/控制层,便于阅读、AOP 切面定位、@Repository 还附带异常转换)。理解「@Service 等是 @Component 的派生注解、靠元注解被识别、功能等价但语义分层」,就理解了为什么它们都能被扫到。
三、从包路径到 .class 资源
扫描的第一步是「把包路径变成能找到的 .class 文件」:
@ComponentScan("com.example") 的第一步转换:
包名 com.example
↓ 转成路径
classpath*:com/example/**/*.class
(classpath*: 表示扫所有 classpath 根,含 jar 里的)
(** 表示任意层子目录,*.class 表示所有类文件)
↓
ResourcePatternResolver.getResources(pattern)
→ 返回所有匹配的 .class 文件资源(Resource[])
这一步靠 Spring 的资源抽象(Resource / ResourcePatternResolver):
它能统一处理 文件系统的 class、jar 包里的 class
→ 不管 class 在文件夹还是 jar 里,都能找出来
第一步是「包路径 → 资源模式 → 找出所有 .class 文件」——com.example 转成 classpath*:com/example/**/*.class,用 ResourcePatternResolver(Spring 的资源加载抽象)找出所有匹配的 .class 文件(包括 jar 包里的)。这一步依赖 Spring 的 Resource 抽象,屏蔽了「class 在文件夹还是 jar 里」的差异。理解「包路径转成 classpath*:xxx/**/*.class 模式、用 ResourcePatternResolver 找出所有 .class 含 jar 里的」,就理解了扫描的第一步——定位候选文件。
四、关键:用 ASM 读字节码而非类加载器
扫描最巧妙的一步是「用 ASM 读字节码,而不是用类加载器加载类」:
判断一个 .class 有没有 @Component,有两种办法:
❌ 办法一:用类加载器 Class.forName 加载它,再反射看注解
问题:① 慢(要完整加载、验证、准备、解析)
② 有副作用(触发静态初始化块、静态字段初始化)
③ 扫几千个 class 全加载 → 启动巨慢、还可能加载了不需要的类
✅ 办法二:用 ASM 直接读 .class 字节码的元数据(Spring 的做法)
- MetadataReader 用 ASM 读字节码,只解析出"类信息、注解信息"
- 不加载类、不初始化、不占用 JVM 的类空间
- 快、无副作用
- 只有"确认是组件、要注册成 Bean"的类,后面才真正被加载
Spring 的 SimpleMetadataReader 就是基于 ASM 实现的
这是扫描的精髓——用 ASM 读字节码元数据判断注解,而非用类加载器加载类。原因:类加载器加载类慢(完整的加载/验证/初始化流程)、有副作用(触发静态初始化)、污染类空间(加载了大量可能用不上的类)。ASM 只读字节码里的「类信息和注解信息」,不加载不初始化,快且无副作用。只有「确认是组件」的类后面才真正被加载。理解「扫描用 ASM 读字节码元数据判断注解、不用类加载器(避免慢/静态初始化副作用/污染类空间)、确认是组件才真正加载」,就理解了扫描高效的关键。
五、过滤器机制:includeFilters 与 excludeFilters
扫描不是「一刀切全收」,而是有「过滤器」精细控制:
两类过滤器:
includeFilters:额外"包含"哪些(除了默认的 @Component 系)
excludeFilters:从结果里"排除"哪些
useDefaultFilters(默认 true):
是否默认扫描 @Component/@Service/@Repository/@Controller
设为 false,就只按你自己的 includeFilters 扫
过滤类型(FilterType):
ANNOTATION:按注解(如包含所有带 @MyAnno 的)
ASSIGNABLE_TYPE:按类型(某接口的实现)
ASPECTJ / REGEX:按表达式匹配类名
CUSTOM:自定义 TypeFilter
例:Spring Boot 的 @SpringBootApplication 用 excludeFilters
排除了 @Configuration 类(避免重复处理自动配置类)
过滤器让扫描「可精细控制包含/排除范围」——includeFilters 额外包含(配合 useDefaultFilters=false 可完全自定义扫描规则)、excludeFilters 排除。过滤类型有按注解、按类型、按正则、自定义等。典型应用:Spring Boot 的 @SpringBootApplication 用 excludeFilters 排除某些 @Configuration。理解「includeFilters/excludeFilters 精细控制扫描范围、useDefaultFilters 控制是否默认扫 @Component 系、过滤类型有注解/类型/正则/自定义」,就理解了扫描的过滤机制。
六、扫到之后:注册成 BeanDefinition
扫描的最后一步是「把符合条件的类变成 BeanDefinition 注册」:
一个类确认是组件后:
1. 创建 ScannedGenericBeanDefinition(一种 BeanDefinition)
2. 解析类上的注解,填充 BeanDefinition:
- @Scope → scope(singleton/prototype)
- @Lazy → lazyInit
- @Primary → primary
- @DependsOn → dependsOn
3. 用 BeanNameGenerator 生成 Bean 名字
(默认:类名首字母小写,如 UserService → userService)
(@Service("myName") 可自定义名字)
4. registerBeanDefinition 注册到容器
至此,扫描完成——容器里多了一批 BeanDefinition
后续按 BeanDefinition 创建实例(见"BeanDefinition"相关题)
扫描的终点是「生成 BeanDefinition 并注册」——把类信息和注解(@Scope/@Lazy/@Primary 等)解析成 ScannedGenericBeanDefinition,用 BeanNameGenerator 生成 Bean 名(默认类名首字母小写),注册到容器。之后就走「BeanDefinition → 实例」的流程了。所以组件扫描本质是「配置来源」的一种——它和 XML、@Bean 一样,最终都产出 BeanDefinition。理解「扫到的类解析成 ScannedGenericBeanDefinition(含 @Scope/@Lazy 等)、生成 Bean 名注册、扫描是产出 BeanDefinition 的一种配置来源」,就把组件扫描接回了容器的整体流程。
记忆钩子:「@ComponentScan 扫描原理:① 包路径转 classpath*:xxx//*.class ② ResourcePatternResolver 找出所有 .class(含 jar 里)③ 用 ASM 读字节码元数据判断注解(不用类加载器!避免慢/静态初始化副作用/污染类空间)④ includeFilters/excludeFilters 过滤 ⑤ 符合的生成 ScannedGenericBeanDefinition 注册;@Service/@Repository/@Controller 是 @Component 派生注解(元注解识别,功能等价语义分层);坑:@SpringBootApplication 默认只扫主类所在包及子包,Bean 放外面就扫不到」**。
七、常见误区与追问
- 误区:@ComponentScan 用类加载器把包里的类都加载一遍。 不是——扫描用 ASM 直接读 .class 字节码的元数据判断注解,不加载类、不触发静态初始化;这样快、无副作用,只有确认是组件的类后面才真正被类加载器加载。
- 误区:@Service 和 @Component 是不同的机制。 @Service/@Repository/@Controller 都是 @Component 的派生注解(定义上标了 @Component 作为元注解),扫描器靠识别元注解发现它们;功能等价,只是语义分层(业务/数据/控制层)。
- 误区:@ComponentScan 会扫描整个 classpath。 只扫指定的 basePackages;不写 basePackages 时默认扫「@ComponentScan 所在类的包及子包」——这也是「Bean 放错包扫不到」的根源。
- 误区:Spring Boot 不用配 @ComponentScan。 @SpringBootApplication 内置了 @ComponentScan(默认扫主启动类所在包及子包);所以 Bean 要放在主类同级或子包,否则要显式扩大 basePackages。
- 追问:为什么扫描要用 ASM 而不是反射? 反射需要先用类加载器加载类(慢、触发静态初始化、污染类空间),扫几千个类会让启动很慢且有副作用;ASM 直接读字节码元数据,只解析类信息和注解,不加载不初始化,高效无副作用。
- 追问:Bean 注入不到、报 NoSuchBeanDefinitionException,先查什么? 先查包路径——Bean 的类是否在 @ComponentScan(或 @SpringBootApplication 主类)所在包及子包下;不在就扫不到。其次查是否漏标 @Component 系注解、是否被 excludeFilters 排除、是否有条件注解(@Conditional)没满足。
- 追问:默认的 Bean 名字是怎么生成的? 默认用 AnnotationBeanNameGenerator:取类的简单名首字母小写(UserService → userService);如果注解里指定了名字(@Service(“myUserService”))则用指定的;特殊情况(如前两个字母都大写 URLParser)保持原样不小写首字母。
八、加强记忆
@ComponentScan 告诉 Spring「去哪些包找带 @Component 系注解的类注册成 Bean」。扫描原理五步:① 包路径转资源模式(com.example → classpath*:com/example/**/*.class);② 用 ResourcePatternResolver 找出所有 .class 文件(含 jar 里的,靠 Spring 的 Resource 抽象屏蔽差异);③ 用 ASM 读字节码元数据判断注解——不用类加载器(避免慢、避免触发静态初始化的副作用、避免污染类空间),只有确认是组件的类后面才真正加载;④ 用 includeFilters/excludeFilters 过滤(useDefaultFilters 控制是否默认扫 @Component 系,过滤类型有注解/类型/正则/自定义);⑤ 符合的生成 ScannedGenericBeanDefinition(解析 @Scope/@Lazy/@Primary 等)、用 BeanNameGenerator 生成 Bean 名(默认类名首字母小写)注册。@Service/@Repository/@Controller 是 @Component 的派生注解(定义上标 @Component 作元注解,功能等价、语义分层)。高频坑:@SpringBootApplication 内置的 @ComponentScan 默认只扫主启动类所在包及子包,Bean 放在主类包外就扫不到(NoSuchBeanDefinitionException),要么移进去、要么显式配 basePackages。一句话「@ComponentScan:包路径→找 .class→ASM 读字节码判断注解(不用类加载器省性能避副作用)→过滤→注册成 BeanDefinition;@Service 等是 @Component 派生注解;默认只扫主类所在包及子包」。