SpringBoot默认组件扫描

HeJin大约 5 分钟源码解析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。点进去看看:

image-20221230145752924
image-20221230145752924

这里我们发现了包扫描注解。说明是会扫包的。所以接下来看看是怎么做的。

关键代码还是在main方法第一行,执行完这行之后,bean就已经被注册到Spring容器中。

注意:这次我们探究的是默认包扫描,所以其他的不需要关注。如果全部关注的话,会影响我们的探究。因为SpringBoot和Spring源码太复杂了。只关注当前关心的。

image-20221230145907761
image-20221230145907761

什么时候可以停止探究呢?只要发现容器中有了自定义的bean,说明已经扫包了。重点看扫包的过程。

1.进入SpringApplicationrefresh()方法。

image-20221230150145852
image-20221230150145852

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

image-20221230150832763
image-20221230150832763

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

image-20221230150934970
image-20221230150934970

进入invokeBeanFactoryPostProcessors()一探究竟。

进入AbstractApplicationContextinvokeBeanFactoryPostProcessors()方法:

image-20221230151106523
image-20221230151106523

经过调试(查看beanName),我们发现关键代码就是第一行。

3.进入PostProcessorRegistrationDelegateinvokeBeanFactoryPostProcessors()方法。

image-20221230151638955
image-20221230151638955

进入invokeBeanDefinitionRegistryPostProcessors()方法:

image-20221230151829290
image-20221230151829290

4.进入ConfigurationClassPostProcessorpostProcessBeanDefinitionRegistry()方法。

image-20221230151938110
image-20221230151938110

进入processConfigBeanDefinitions()方法:

image-20221230152208536
image-20221230152208536

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

image-20221230152428016
image-20221230152428016

我们用了@Configuration,注意看。

image-20221230152738416
image-20221230152738416

myConfig就是我们的配置类,里面我们注册了一个bean。说明SpringBoot扫描到了这个类。关键代码。

5.进入ConfigurationClassParser中的parse()方法。

image-20221230153031588
image-20221230153031588
image-20221230153259486
image-20221230153259486

进入parse()方法:

image-20221230153349096
image-20221230153349096

进入processConfigurationClass()方法:

image-20221230153627558
image-20221230153627558

进入doProcessConfigurationClass()方法:

image-20221230153816265
image-20221230153816265

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

image-20221230154141937
image-20221230154141937

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

image-20221230154507406
image-20221230154507406

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

image-20221230154735360
image-20221230154735360

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

image-20221230155436292
image-20221230155436292

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

image-20221230155717578
image-20221230155717578

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

image-20221230155827568
image-20221230155827568

到这里,我们就可以确定并验证:SpringBoot会扫描主启动类所在的包。接下来看看会不会递归扫描子包。

7.进入ClassPathBeanDefinitionScanner中的doScan()方法。

这个方法的主要功能:在指定的基本包中执行扫描,返回已注册的bean定义(BeanDefinition)。 此方法不注册注释配置处理器,而是将其留给调用者。

image-20221230160307927
image-20221230160307927

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

image-20221230160426075
image-20221230160426075

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

image-20221230160653413
image-20221230160653413

进入scanCandidateComponents()方法:

image-20221230160958281
image-20221230160958281

为了测试是否会扫描子包,我们在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()方法:

image-20221230162352119
image-20221230162352119

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

image-20221230162735732
image-20221230162735732

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

9.返回ClassPathBeanDefinitionScanner中的doScan()方法。

image-20221230163415159
image-20221230163415159

这里我们发现,会把扫描的的bean定义信息(BeanDefinition)注册到BeanDefinitionRegistry。通过BeanDefinition就可以创建bean。

总结

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