Spring启动流程源码解析

学了一些新的东西,总得留下点什么。

前言

Spring源码我最早是从2022年3月份开始读的,断断续续看了几个月,期间也有因为看不懂放弃过,不过最终还是跟着源码断点了一遍。从完全不懂到知道整体脉络,感觉还是颇有成就感的。

出于从来不做笔记、写文档等原因,过了一段时间以后,没有再次去深入阅读、理解一遍,所以很快就遗忘了很多。这一次决定重新开始,从头再来梳理一遍,过程中输出自己的学习文档,以此来加深自己的理解、记忆。鉴于Spring源码太过庞大,一口吃不成胖子,一篇文章写不下,所以我会按着我的学习进度,每次输出一点,最后整理成一个系列的文档。

Spring的源码如何获取、如何搭建本地环境,网上已经有很多人写过了,随便一搜就有,这个我就略过了,不是学习的重点。

我会以使用注解的方式,从创建一个上下文开始,一行一行的断点,先捋清楚脉络,再不断深入细节。

前置知识:Spring注解编程的发展过程

如果想要对Spring注解编程的发展过程有个大致了解的话,可以参考上面这篇文章。

准备

这个例子会展示Spring如何将我们定义好的实体类加载到容器中,并且为我们自动注入相应的bean对象。

配置类

先定义一个配置类,告诉Spring需要扫描的包路径,待加载的bean信息就是放在这个包下的。这里我是用注解的方式定义配置类

@Configuration注解用来表示当前类是一个配置类,在Spring 3.0版本以前,我们一般都是通过编写xml的方式来定义相关信息。通过注解的方式,我们就无需编写xml了。

@ComponentScan注解的作用就是用来告诉Spring容器启动时需要扫描的包路径,如果不指定value的话,默认就是扫描当前配置类所在的包及其子包。这个注解用来替代Spring 3.1版本以前的<context:component-scan .../>标签

java 复制代码
@Configuration
@ComponentScan(value = "com.xiaojiesen.debug.annotation")
public class ComponentScanConfiguration {
}

实体类

定义一个Person类、Phone类,待注册到容器中。

@Component注解可以使我们不用在xml文件中去定义bean,只要配置了包扫描路径,Spring就会帮我们加载当前bean到容器中。

Person

java 复制代码
@Component
public class Person {

    @Autowired
    private Phone phone;

    private String name;

    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Phone

java 复制代码
@Component
public class Phone {

    private String brand;

    private int price;

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}

启动类

创建一个测试启动类,定义main方法,程序的入口。

这里把我们在上面定义好的配置类ComponentScanConfiguration传入到上下文的构造方法里,待上下文创建好后,我们就能从中获取到想要的bean对象了。

java 复制代码
public class Test {

    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ComponentScanConfiguration.class);
        Object person = applicationContext.getBean("person");
        System.out.println(person);
    }
}

到此,我们就已经准备好了调试Spring源码的一些基本要素,我们现在只要在main方法上打上断点,以Debug模式运行main方法,就可以一步一步跟着源码走了。

Spring应用上下文的创建

以上面这个例子来说,我们只写了短短的几行代码,Spring就能帮我们完成从bean加载、到实例化,再到我们可以从容器中获取想要的对象,这些Spring都是如何帮我们实现的呢?接下来,就一起来断点看一下。

应用上下文创建的流程

main方法的第一行上打一个断点 按下F7,进入AnnotationConfigApplicationContext的构造方法 可以看到,这里调用的是AnnotationConfigApplicationContext类的带配置类的构造方法。这个构造方法的作用就是创建一个新的上下文,将会对参数传入的配置类进行一些处理,然后获取到bean的定义信息,并且会自动刷新上下文。

这里一共有3个步骤,一是调用当前类的无参构造方法,二是把我们编写的配置类ComponentScanConfiguration添加到bean定义信息集合中,三是容器的刷新(Spring最最最重要的部分)。

接下来,一起来看一下这3部分的内容。

this()方法

从刚才的步骤那里,再次按一下F7,进入this()方法(AnnotationConfigApplicationContext的无参构造方法) 注意一下,这里是比较重要的。首先这个构造方法是无参的,所以会先调用父类的无参构造方法,我们在上面这个状态再按F7,进入到父类的构造方法 可以看到,在这里创建了一个默认的beanFactory,也就是DefaultListableBeanFactory(如果通过xml的方式来启动Spring,会发现beanFactory创建的时机跟这里的不一样)。

继续,断点回到AnnotationConfigApplicationContext的无参构造方法中,第一行的代码创建了一个解析注解类型的bean定义信息的解析器AnnotatedBeanDefinitionReader。这里面最主要的内容就是注册一些默认的注解配置处理器。在这里,我们先看下当前bean工厂中beanDefinitionMap的状态,可以看到当前是一个空的map。 我们跳过一些重载的方法,直接进入到这个方法核心的地方。 这里一共往beanDefinitionMap里注册了5个对应的类定义信息。这些类分别有不同的作用。

回到原来的方法中,看看创建完AnnotatedBeanDefinitionReader对象后的结果。可以看到如上面所说的,beanDefinitionMap里多了5个类定义信息。 然后接下来就是为当前上下文创建一个类路径bean定义扫描器。

到此,this()方法就已经执行完了。

在这一个步骤里面,主要是帮我们创建了一个默认的beanFactory,以及注册了一些处理注解时必要的注解处理器。

register(componentClasses)

执行完上一个步骤以后,代码执行来到了register(componentClasses)的入口处,也就是回到了AnnotationConfigApplicationContext的构造方法里。 按下F7,进入register(componentClasses)的实现 这里的reader,实际上就是我们在步骤一里this()方法代码的第一行所创建的那个解析器。这里调用了这个解析器的注册方法,F7进去看一下方法的实现(核心方法) 这里做的就是把我们的配置类ComponentScanConfigurationbean定义信息注册到beanFactorybeanDefinitionMap中,名称就是首字母小写的类名。

继续按下F8,回到方法的入口处 可以看到,执行完这个步骤,我们的配置类的定义信息就已经注册到beanFactory当中了。

refresh()方法

到这里为止,完成了beanFactory的创建、注解处理器以及配置类的定义信息注册,还没有任何创建好的bean实例往beanFactory中存放。以上步骤只是为应用上下文的创建做了一些基础的准备操作,真正最重要的还是最后的这个容器刷新步骤。

断点进入refresh() 这个方法里,完成了实例对象的创建、对象属性的填充、自动注入等等操作,可谓是Spring里最精华的部分。

Spring MVC、SpringBoot都是基于其之上去继续扩展出更多功能的。如果掌握了容器刷新这部分的内容,想必之后在阅读这两个源码的时候会更加的游刃有余。

后续我也会继续就这一部分的内容,展开更详细的说明。

流程图

上下文创建流程

容器刷新流程

总结

只是学习是痛苦的,遇到困难容易让人退缩。没有及时输出,获得正向的反馈,就无法提高学习的兴趣,持续保持学习。学了一些新的东西,总得留下点什么。留下点什么,写点什么,激励自己,继续学习,深度学习。

相关推荐
初晴~5 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
雷神乐乐7 小时前
Spring学习(一)——Sping-XML
java·学习·spring
小林coding7 小时前
阿里云 Java 后端一面,什么难度?
java·后端·mysql·spring·阿里云
文大。8 小时前
2024年广西职工职业技能大赛-Spring
java·spring·网络安全
小马爱打代码9 小时前
Spring Boot 中 Map 的最佳实践
java·spring boot·spring
智慧老师14 小时前
Spring基础分析13-Spring Security框架
java·后端·spring
hanbarger17 小时前
mybatis框架——缓存,分页
java·spring·mybatis
龙少954319 小时前
【深入理解@EnableCaching】
java·后端·spring
啦啦右一1 天前
Spring Boot | (一)Spring开发环境构建
spring boot·后端·spring
荆州克莱1 天前
mysql中局部变量_MySQL中变量的总结
spring boot·spring·spring cloud·css3·技术