ARTICLE · 1101764
技术人怎么读源码:以 Spring 启动流程为例
前几天有个读者私信我,说自己工作两年了,一直想读 Spring 源码,但每次打开 IDE,翻个三五个类就晕了,问我有没有什么窍门。
说实话,我太懂这种感觉了。
我刚开始读源码的时候,也是抱着「从第一个类开始看」的想法,结果 看了三天还在看各种工具类,越看越觉得自己什么都不懂,最后直接放弃。
后来才明白,读源码根本不是读书,不能从第一页翻到最后一页。
今天我就以 Spring 的启动流程为例,把我读源码的完整思路拆给你看。看完你会发现,读源码这件事,真的有方法。
01
别从第一行开始读
这是最多人踩的第一个坑。
打开 Spring 源码,找到 SpringApplication.java,从第一行开始往下看。看到第 50 行,有个 logger,再往下有个 WebApplicationType,再往下一堆成员变量……
二十分钟后,你开始怀疑人生。
正确的做法是,先找入口。
你平时写 Spring Boot 项目,启动类怎么写的?
@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
就这一行 SpringApplication.run(),这就是你的入口。
所有的一切,都从这一个方法开始。你不需要先把 SpringApplication 这个类的所有字段都搞清楚,你只需要 跟着 run 方法走。
记住一句话:读源码是顺着调用链往下读,不是顺着文件从上往下读。
02
先画主线,再填细节
进入 run 方法之后,你会看到它做了很多事。
打印 Banner、准备环境、创建上下文、刷新上下文、发布事件……
这时候第二个坑出现了:每一步都想搞懂,结果哪一步都没搞懂。
我给你的建议是,第一遍读,只抓主干。
Spring Boot 启动的主干是什么?就三步:
1. 构造 SpringApplication 实例
2. 创建并刷新 ApplicationContext
3. 执行 Runner,启动完成
就这三步,其他都是配菜。
Banner 打印?配菜,跳过。环境准备?知道它在加载配置就行了,第一遍不用深究。事件发布?知道有这么个机制就行。
你的目标是先在脑子里画出一条从 main 方法到应用启动完成的主线,这条线上有哪些关键节点、每个节点大概做了什么。
主干清晰了,再回头一个一个抠细节。这时候你会发现,原来那些看不懂的类和方法,只是主线上某个节点的具体实现而已。
03
带问题读,别漫无目的地看
这是我觉得最有效的一条。
漫无目的地翻源码,就像不带地图逛商场——你可能逛了三个小时,最后只记得一楼有个奶茶店。
带问题读就不一样了。每读一行,你都在找答案。
比如读 Spring 启动流程,你可以先列几个问题:
❶ Spring Boot 是怎么判断当前是不是 Web 应用的?
❷ 自动配置的类是什么时候被加载的?
❸ @SpringBootApplication 注解到底做了什么?
❹ Bean 是什么时候被创建的?
有了问题,你的阅读就有了方向。每找到一个答案,你就获得一次正反馈,读下去的动力就更强。
我自己读源码的时候,通常会先写三五个问题在旁边,每解决一个就划掉一个。划完的时候,这一块源码你基本就吃透了。
反过来,如果一个问题在源码里绕了半天找不到答案,别死磕,先记下来,继续往前走。很可能读到后面某个地方,你突然就明白了。
04
实战:Spring 启动的核心路径
说了这么多方法论,我们来实际走一遍。
就从 SpringApplication.run() 开始。
—— 第一步 ——
构造 SpringApplication
你点进去会发现,run 方法其实就干了两件事:new 一个 SpringApplication,然后调用它的 run 方法。
构造函数里做了什么?核心就四件事:
① 把你的主类存起来(primarySources)
② 判断 Web 应用类型(NONE / SERVLET / REACTIVE)
③ 加载所有 ApplicationContextInitializer
④ 加载所有 ApplicationListener
这里面最有意思的是第二步——它怎么知道你是不是 Web 应用?
答案很简单:它去 classpath 里找有没有几个关键的类。比如找得到 DispatcherServlet,就是 SERVLET 类型;找得到 DispatcherHandler,就是 REACTIVE 类型;都没有,那就不是 Web 应用。
很朴素的思路,但很多人就是不知道。
—— 第二步 ——
run 方法的核心流程
SpringApplication 的 run 方法是整个启动流程的骨架。我把它简化一下,核心步骤是这样的:
public ConfigurableApplicationContext run(String... args) { // 1. 计时开始 StopWatch stopWatch = new StopWatch(); stopWatch.start(); // 2. 准备环境 ConfigurableEnvironment environment = prepareEnvironment(); // 3. 打印 Banner Banner printedBanner = printBanner(environment); // 4. 创建上下文 context = createApplicationContext(); // 5. 准备上下文 prepareContext(context, environment, ...); // 6. 刷新上下文 ← 最核心的一步 refreshContext(context); // 7. 执行 Runner callRunners(context, applicationArguments); // 8. 计时结束,发布事件 stopWatch.stop(); listeners.started(context); return context; }
我在第 6 步标了个「最核心」,因为 Spring 真正的魔法都发生在 refresh 里面。
前面那些步骤,说白了都是在做准备工作——准备环境、准备上下文、把该加载的配置加载好。真正把所有 Bean 实例化、把容器搭建起来的,是 refresh。
—— 第三步 ——
refresh:Spring 的灵魂
refresh 方法来自 AbstractApplicationContext,这是 Spring 框架的经典代码,十几年了结构基本没变过。
它里面有 12 个方法调用,一个接一个,像流水线一样。我挑几个最重要的讲:
invokeBeanFactoryPostProcessors
执行 BeanFactory 后置处理器。自动配置的类,就是在这一步被解析和加载的。
registerBeanPostProcessors
注册 Bean 后置处理器。AOP、@Autowired 这些功能,全靠 BeanPostProcessor 实现。
finishBeanFactoryInitialization
初始化所有单例 Bean。你写的 @Service、@Controller,都是在这里被创建的。
finishRefresh
完成刷新,发布事件。如果是 Web 应用,内嵌的 Tomcat 也是在这里启动的。
你看,就这四个方法,基本覆盖了 Spring 最核心的功能。
第一遍读 refresh,你只要知道这 12 步每一步大概是干嘛的,就已经超过 80% 的人了。
至于每一步具体怎么实现的,那是第二遍、第三遍的事。
05
几个让你事半功倍的小技巧
最后分享几个我自己读源码时常用的小技巧,简单但好用。
技巧 1
打断点,别光靠眼睛看
这是我最推荐的方法。在你觉得关键的地方打个断点,然后以 Debug 模式启动,一步一步看调用栈和变量值。
比你干看代码效率高十倍。
技巧 2
用 Call Hierarchy 反查调用者
看到一个方法不知道它什么时候被调用?在 IDEA 里右键方法名,选 Call Hierarchy(快捷键 Ctrl+Alt+H),就能看到所有调用它的地方。
顺着调用链往上爬,你很快就能找到这个方法在整个流程里的位置。
技巧 3
画流程图
读完一个模块,用画图画一下流程。不用好看,自己能看懂就行。
画的过程就是你梳理思路的过程。很多时候你以为自己懂了,一动笔就发现某个地方其实没想清楚。
技巧 4
别纠结每一行代码
Spring 源码里有很多边界处理、兼容处理、异常处理的代码,读起来又烦又没用。
我的原则是,先看正常流程,异常和边界先跳过。等主干搞清楚了,再回头看这些边角料也不迟。
06
最后说几句
很多人把读源码想得太神圣了。
好像只有把整个 Spring 源码全部吃透,才算「精通 Spring」。其实根本不是这样。
读源码的目的,是建立技术直觉。
你遇到一个 bug,大概能猜到是哪一层出了问题;你设计一个方案,大概能想到框架内部会怎么处理;你看到一个新特性,大概能猜到它是基于什么机制实现的。
这就够了。
不需要背下每一行代码,也不需要记住每一个类的名字。框架是工具,不是圣经。你读它,是为了更好地用它,而不是为了成为它的活字典。
从今天开始,打开你 IDE 里那个尘封已久的 Spring 项目,找一个你一直好奇的问题,顺着调用链往里点三层。
就三层。你会发现,源码这件事,没你想的那么难。
— END —