乐于分享
好东西不私藏

怎么读源码或是别人的代码呢?

怎么读源码或是别人的代码呢?

接手一个陌生的代码库,大多数人的第一反应是打开项目根目录,从 src/main/java 开始往下翻。类太多,关系不清楚,每个方法都跳进去看,越看越乱,一个小时后对整体还是没概念。

直接从入口开始。

先摸清楚项目的骨架

打开项目不要先看代码,先看构建文件。pom.xml 里的 <modules> 告诉你项目分了几个模块,<dependencies> 告诉你它依赖了什么——用了 MyBatis 还是 JPA、有没有 Redis、有没有 MQ、用了哪个版本的 Spring Boot。三分钟看完 pom,脑子里就有了技术栈的轮廓。

多模块项目看一下模块之间的依赖关系。IDEA 里 File → Project Structure → Modules,或者直接看每个子模块的 pom 互相引用了谁。通常是 api 定义接口、service 实现业务、dal 访问数据库、web 暴露 HTTP 接口。依赖方向从 web → service → dal,反过来的依赖说明模块划分有问题,但至少你知道了代码在哪里。

数据库 schema 是另一个经常被忽略的入口。SHOW TABLES 看一眼表名,userorderpaymentorder_item——表名直接告诉你业务域是什么。SHOW CREATE TABLE orders 看字段和索引,status 字段有哪些值、user_id 上有没有索引、有没有 create_time 和 update_time。表结构是业务模型的物理映射,比看 Entity 类更直接,因为 Entity 上可能有一堆注解和继承关系干扰视线。

从入口往下追,不要从实现往上猜

代码库的入口很具体:HTTP 接口对应的 Controller,消息队列的 Consumer,定时任务的 @Scheduled 方法。从这些起点顺着调用链往下走,你看到的是代码真实的执行路径。

在 IDEA 里找入口最快的方式:Ctrl+Shift+F 搜 @RestController 或 @Controller,所有 HTTP 接口入口全出来了。搜 @KafkaListener 或 @RabbitListener 找消息消费入口,搜 @Scheduled 找定时任务。

不要从 Service 实现类开始读。一个 OrderService 可能有 30 个方法,脱离调用上下文你不知道哪些是核心流程、哪些是边角功能。从 Controller 的某个具体接口开始,比如 POST /api/orders,顺着 controller → service → mapper 这条链走一遍,这个接口的完整逻辑就清楚了。

git log --stat 能帮你找到代码库里真正活跃的部分:

git log --oneline --stat -30

最近 30 次提交里改动最频繁的文件,大概率就是当前业务的核心代码。一个半年没人动过的 Service,大概率不是你现在需要关注的。

git log --oneline --since="2026-03-01" -- src/main/java/com/example/order/

限定目录和时间范围,快速看某个模块最近在改什么。提交信息里的关键词(”修复超卖”、”加分布式锁”、”优化查询”)直接告诉你这个模块最近的痛点在哪里。

先跑起来再读

代码一定要跑起来,才算真的在读。

先读再跑,脑子里的模型是静态的,碰到条件分支只能靠猜。先跑再读,加断点,看运行时数据,立刻知道实际走的是哪条路、变量的真实值是什么。

断点命中之后,调试面板里最有价值的不是变量值,是 Call Stack(调用栈)。调用栈从上到下展示了整条调用链——从 Controller 到 Filter 到 Interceptor 到 AOP 到 Service,哪些中间层参与了这次调用,一目了然。Spring 项目里一个方法调用实际经过的层数,经常比你在代码里看到的多得多(代理、拦截器、事务管理器),Call Stack 直接把这些隐藏层暴露出来。

如果项目本地跑不起来(依赖外部服务、配置文件缺失),退而求其次用 Arthas 连测试环境:

java -jar arthas-boot.jar
# 连上测试环境的进程
trace com.example.controller.OrderController createOrder

trace 命令输出完整的调用树和每层耗时,不需要加断点,不需要本地跑起来,直接看运行中的调用链。

用 git 还原代码的演进过程

面对一段复杂的代码,直接读当前版本往往是最低效的——它可能经过了几十次修改,加了各种 edge case 处理,已经面目全非。

Git Blame(IDEA 里左侧行号右键 → Annotate with Git Blame)能看到每一行是谁在什么时候写的。碰到一段看不懂的条件判断,Blame 找到对应的提交,看提交信息——”fix: 修复并发下重复扣款”,立刻知道这段代码是为了解决什么问题。

更进一步,git log -p 看某个文件的完整修改历史:

git log -p --follow -- src/main/java/com/example/service/OrderService.java

--follow 跟踪文件重命名,-p 显示每次提交的 diff。从最早的版本开始看,第一版可能只有 50 行,逻辑很清楚;后面每次加了什么、为什么加,一步步还原出来,比直接读当前 500 行的最终版本容易理解得多。

IDEA 里这几个功能值得专门学一下

Find Usages(Alt+F7)——选中一个方法名,看它在哪里被调用。一个方法被七八个地方调用,这些调用场景是理解这个方法最快的途径。

Call Hierarchy(Ctrl+Alt+H)——看调用链。选中一个方法,往上追到顶层入口(谁在调它),往下看所有被调的方法(它调了谁)。面对一个陌生的 Service 方法,Ctrl+Alt+H 直接看到整条链路。

Dependency MatrixAnalyze → Dependency Matrix)——模块级别的依赖关系矩阵。20 个模块互相依赖的大项目,这个视图能快速看清楚哪些模块是核心(被依赖最多的)、哪些模块是边缘(只被一两个地方用到)。

Structure 面板(Alt+7)——展示当前类的所有字段和方法。几十个方法的大类,先扫一眼结构,把方法名大致归一下类(查询类、修改类、事件处理类),再针对性地看。

Diagrams → Show Diagram——选中一个包或一组类,生成类图。复杂的继承和组合关系,文字描述不如图直观。不需要画得很精确,看清楚核心类之间的关系就行。

带问题读,不要无目的泛读

“把这个项目读懂”这个目标太大,没有边界,很容易变成低效的漫游。

带具体问题去读:这个接口的参数在哪里校验的、这张表的数据从哪个入口写进来的、这个定时任务失败了会怎么处理、这个分布式锁是怎么释放的。有了具体问题,Find Usages 找到相关代码,断点跑起来验证,问题确认了这一段就算读完了。

读代码最忌讳的是”我觉得我懂了”。懂了的标准只有一个:能改它、改完之后不会意外破坏其他地方。不确定的就写个测试验证,加断点跑一下,不要靠脑子里的推演。

测试代码是被严重低估的阅读入口。UserServiceTest.testCreateUser() 在三十秒内告诉你创建用户的主流程是什么、边界条件在哪里。测试方法的命名(testCreateOrder_whenStockInsufficient_shouldThrowException)直接就是业务规则的描述。

画图是为了记忆,不是为了理解

读代码过程中确实需要画图,但代码跑起来比图清楚得多。画图是为了记录已经理解的东西,防止之后忘了再读一遍。

一张简单的调用关系图(A调B调C,哪里读数据库哪里读缓存哪里调外部接口),手画在纸上或者用任何工具记一下。后面需要改东西的时候能快速定位,不用每次都重新从 Controller 追一遍。

读别人的代码还有一个经常被低估的方法:直接问原作者。”这里为什么这么写?当时遇到了什么约束?”。五分钟的对话能省掉两小时的推断,而且往往能知道代码之外的背景——某个奇怪的判断是为了规避一个数据库锁问题,某段看起来冗余的代码是为了兼容一个旧格式的外部数据。这些上下文不在代码里,只在人的记忆里。