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了。