学了一些新的东西,总得留下点什么。
前言
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都是基于其之上去继续扩展出更多功能的。如果掌握了容器刷新这部分的内容,想必之后在阅读这两个源码的时候会更加的游刃有余。
后续我也会继续就这一部分的内容,展开更详细的说明。
流程图
上下文创建流程
容器刷新流程
总结
只是学习是痛苦的,遇到困难容易让人退缩。没有及时输出,获得正向的反馈,就无法提高学习的兴趣,持续保持学习。学了一些新的东西,总得留下点什么。留下点什么,写点什么,激励自己,继续学习,深度学习。