学了一些新的东西,总得留下点什么。
前言
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进去看一下方法的实现(核心方法)
这里做的就是把我们的配置类ComponentScanConfiguration的bean定义信息注册到beanFactory的beanDefinitionMap中,名称就是首字母小写的类名。
继续按下F8,回到方法的入口处
可以看到,执行完这个步骤,我们的配置类的定义信息就已经注册到beanFactory当中了。
refresh()方法
到这里为止,完成了beanFactory的创建、注解处理器以及配置类的定义信息注册,还没有任何创建好的bean实例往beanFactory中存放。以上步骤只是为应用上下文的创建做了一些基础的准备操作,真正最重要的还是最后的这个容器刷新步骤。
断点进入refresh()
这个方法里,完成了实例对象的创建、对象属性的填充、自动注入等等操作,可谓是Spring里最精华的部分。
Spring MVC、SpringBoot都是基于其之上去继续扩展出更多功能的。如果掌握了容器刷新这部分的内容,想必之后在阅读这两个源码的时候会更加的游刃有余。
后续我也会继续就这一部分的内容,展开更详细的说明。
流程图
上下文创建流程

容器刷新流程

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