跳到正文
Elaine Blog
返回

如何读源码:调用链、断点、测试与最小复现

学习路线

读源码的目标不是读完仓库,而是回答一个具体问题。你会在这篇指南中使用“问题—入口—调用链—实验—结论”五步法, 分析上一节实验工程中的输入处理逻辑。

Table of contents

Open Table of contents

先写出可验证问题

一个好问题必须能被代码或测试证伪。“Spring 很强”无法验证;“空字符串为什么会被拒绝”可以通过断点和测试回答。

这次使用下面的问题:

GreetingService.greet 收到只包含空格的名字时,程序在哪一行拒绝输入,调用者最终看到什么异常?

问题包含输入、行为和观察结果。后续每一步都为回答它服务。

第一步:找到稳定入口

入口是你能够重复触发行为的位置。HTTP 请求、命令行主方法、公开 API 和测试方法都可以成为入口。

本例使用一个指定测试作为入口:

./mvnw -Dtest=GreetingServiceTest#rejectsBlankName test

如果测试没有被发现,确认文件位于 src/test/java,类名以 Test 结尾,并检查 Surefire 版本是否为 3.0.0 或更高。

第二步:画出最短调用链

**调用链(Call Chain)**是方法从入口到目标行为的调用顺序。先只记录与问题有关的主路径。

GreetingServiceTest.rejectsBlankName
└── GreetingService.greet
    ├── String.isBlank
    └── IllegalArgumentException.<init>

你可以使用 IDE 的“查找用法”和“调用层次”生成候选路径,但仍要通过运行确认真实路径。静态搜索会包含当前场景没有执行的 分支。

在终端中,先用 rg 找到定义和调用:

rg -n "class GreetingService|\.greet\(" src

第三步:设置少量断点

断点应验证假设,不应铺满每一行。先在 GreetingService.greet 的条件判断和抛出异常处设置两个断点。

启动 IDE 的测试调试后,检查:

**调用栈(Call Stack)**是当前线程尚未返回的方法帧序列。它反映这一次运行的真实路径,比全局“调用层次”更具体。

第四步:用测试改变一个变量

最小实验每次只改变一个变量。把空格输入改为前后带空格的有效名字,观察程序是否保留业务上需要的内容。

@Test
void trimsValidName() {
    var service = new GreetingService();

    assertEquals("你好,架构师学习者!", service.greet("  架构师学习者  "));
}

运行测试:

./mvnw -Dtest=GreetingServiceTest#trimsValidName test

测试证明 trim() 在拼接前删除了首尾空白。它没有证明中文、Emoji 或超长输入都正确;不要扩大结论范围。

第五步:构造最小复现

**最小复现(Minimal Reproduction)**是只保留触发问题所需代码和配置的工程。它帮助你排除业务系统中的日志、网络、 数据库和框架干扰。

一个合格最小复现应满足:

当前 java-lab 已经是该问题的最小复现。真实框架问题可以先在原项目确定调用链,再将关键依赖和输入移到独立工程。

记录结论而不是摘抄代码

源码阅读结论必须包含证据和适用范围。下面的记录可以被其他人复查。

问题:空白名字在哪里被拒绝?
结论:GreetingService.greet 在拼接前调用 String.isBlank;结果为 true 时抛出 IllegalArgumentException。
证据:GreetingServiceTest#rejectsBlankName 通过;断点显示输入为三个空格,调用栈只有测试与 greet。
适用范围:当前 java-lab 版本;未验证 null 以外的 Unicode 边界输入。
下一步:增加超长名字和不可见 Unicode 字符测试。

不要用“源码就是这样写的”解释设计理由。代码能证明实现,提交记录、设计文档、Issue 和实验结果才能共同支持原因判断。

下一步

阅读如何写技术实验报告,把问题、环境、实验变量和 结论整理成其他人可以复测的文档。


分享这篇文章:

上一篇
搭建可复现的 Java 实验室
下一篇
如何写技术实验报告