SpringBoot默认组件扫描
测试代码
我们在主启动类同一目录建一个包config
。里面用配置类注册一个bean:
@Configuration
public class MyConfig {
@Bean
public User user(){
return new User(1473587915, "李四", 18);
}
}
主启动类直接获取bean:
@SpringBootApplication
public class SpringBootAnalysisApp {
private static final Logger logger = LoggerFactory.getLogger(SpringBootAnalysisApp.class);
public static void main(String[] args) {
ApplicationContext applicationContext = SpringApplication.run(SpringBootAnalysisApp.class, args);
User user = (User) applicationContext.getBean("user");
logger.info("user: {}", user);
}
}
结果:
com.sanfen.SpringBootAnalysisApp : user: User{id=1473587915, name='李四', age=18}
没问题。这里就有个问题了,我们没有配置包扫描,SpringBoot是如何找到MyConfig
类中的bean进行注册的。合理猜测,SpringBoot有一个默认的扫包规则,会扫描主启动类所在包及其子包下的加了相应注解的bean
。
探究目标
SpringBoot项目没有配置包扫描,为什么会默认扫描启动类所在的包?
SpringBoot默认包扫描解析
主启动类有一个注解@SpringBootApplication
。点进去看看:

这里我们发现了包扫描注解。说明是会扫包的。所以接下来看看是怎么做的。
关键代码还是在main方法第一行,执行完这行之后,bean就已经被注册到Spring容器中。
注意:这次我们探究的是默认包扫描,所以其他的不需要关注。如果全部关注的话,会影响我们的探究。因为SpringBoot和Spring源码太复杂了。只关注当前关心的。

什么时候可以停止探究呢?只要发现容器中有了自定义的bean,说明已经扫包了。重点看扫包的过程。
1.进入
SpringApplication
的refresh()
方法。

2.最后我们在
AbstractApplicationContext
中的refresh()
中发现了关键代码。

执行完这行之后,beanName就有了。说明被扫描到了。

进入invokeBeanFactoryPostProcessors()
一探究竟。
进入AbstractApplicationContext
的invokeBeanFactoryPostProcessors()
方法:

经过调试(查看beanName),我们发现关键代码就是第一行。
3.进入
PostProcessorRegistrationDelegate
的invokeBeanFactoryPostProcessors()
方法。

进入invokeBeanDefinitionRegistryPostProcessors()
方法:

4.进入
ConfigurationClassPostProcessor
的postProcessBeanDefinitionRegistry()
方法。

进入processConfigBeanDefinitions()
方法:

往下走,看什么时候我们定义的bean出现了。

我们用了@Configuration
,注意看。

myConfig
就是我们的配置类,里面我们注册了一个bean。说明SpringBoot扫描到了这个类。关键代码。
5.进入
ConfigurationClassParser
中的parse()
方法。


进入parse()
方法:

进入processConfigurationClass()
方法:

进入doProcessConfigurationClass()
方法:

然后会执行到这里,处理包扫描注解:

我们没有写@ComponentScan
,为什么会走到这里呢?其实我们写了,主启动类上有个@SpringBootApplication
这个注解里面就包含了包扫描。

6.进入
ComponentScanAnnotationParser
(组件扫描注解解析器)的parse()
方法。

因为我们没有配置包扫描,所以是SpringBoot的默认配置。SpringBoot没有配置扫描的路径。所以解析注解@ComponentScan
得到的basePackages是空的。

这时候会把主启动类所在的包(declaringClass
是传入的参数,主启动类所在的包)添加到basePackages中。

接下来扫描包,进行处理,扫描的是主启动类所在的包:

到这里,我们就可以确定并验证:SpringBoot会扫描主启动类所在的包
。接下来看看会不会递归扫描子包。
7.进入
ClassPathBeanDefinitionScanner
中的doScan()
方法。
这个方法的主要功能:在指定的基本包中执行扫描,返回已注册的bean定义(BeanDefinition)
。 此方法不注册注释配置处理器,而是将其留给调用者。

看这行代码,扫描到了MyConfig配置类,并且解析成了BeanDefinition对象:

8.进入
ClassPathScanningCandidateComponentProvider
中的findCandidateComponents()
方法

进入scanCandidateComponents()
方法:

为了测试是否会扫描子包,我们在config包下新建一个user包,里面添加一个配置类MyConfig2
:
package com.sanfen.config.user;
import org.springframework.context.annotation.Configuration;
/**
* @author HeJin
* @date 2022/12/30 14:47
*/
@Configuration
public class MyConfig2 {
}
接下来重新debug到上面那个地方,ClassPathScanningCandidateComponentProvider
中的scanCandidateComponents()
方法:

这个方法最后返回的是两个自定义配置类:

发现,SpringBoot默认确实会扫描主启动类及其子包下加了相应注解的bean。这里获取到的是class文件。我们可以返回去看看,这个class文件会变成什么。
9.返回
ClassPathBeanDefinitionScanner
中的doScan()
方法。

这里我们发现,会把扫描的的bean定义信息(BeanDefinition)注册到BeanDefinitionRegistry
。通过BeanDefinition
就可以创建bean。
总结
- SpringBoot默认会扫描主启动类所在的包及其子包下的加了相应注解的类,主启动类加了
@SpringBootApplication
,包扫描就是在这个注解中。 - SpringBoot的包注解没有配置basePackages,所以在解析注解的时候
basePackages集合
是空的。但是SpringBoot会将主启动类所在的包加入到basePackages集合
。为什么能获取到主启动类所在的包?因为程序的入口就是在这里,我们是从这里开始执行的。当然可以获取到主启动类所在的包了。 - SpringBoot的包扫描首先扫描获取到的是bean编译后的class文件,然后通过反射把class文件解析成
BeanDefinition
对象,并把BeanDefinition
进行注册。 - 有了扫描到并解析之后的
BeanDefinition
对象,就可以创建bean了。