读源码的目标不是读完仓库,而是回答一个具体问题。你会在这篇指南中使用“问题—入口—调用链—实验—结论”五步法, 分析上一节实验工程中的输入处理逻辑。
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 的测试调试后,检查:
- 参数
name的原始值是否为三个空格。 name.isBlank()是否返回true。- 创建的异常类型和消息是否符合测试断言。
- 调用栈是否与刚才画出的调用链一致。
**调用栈(Call Stack)**是当前线程尚未返回的方法帧序列。它反映这一次运行的真实路径,比全局“调用层次”更具体。
第四步:用测试改变一个变量
最小实验每次只改变一个变量。把空格输入改为前后带空格的有效名字,观察程序是否保留业务上需要的内容。
@Test
void trimsValidName() {
var service = new GreetingService();
assertEquals("你好,架构师学习者!", service.greet(" 架构师学习者 "));
}
运行测试:
./mvnw -Dtest=GreetingServiceTest#trimsValidName test
测试证明 trim() 在拼接前删除了首尾空白。它没有证明中文、Emoji 或超长输入都正确;不要扩大结论范围。
第五步:构造最小复现
**最小复现(Minimal Reproduction)**是只保留触发问题所需代码和配置的工程。它帮助你排除业务系统中的日志、网络、 数据库和框架干扰。
一个合格最小复现应满足:
- 一条命令可以触发问题。
- 不需要生产凭证或真实用户数据。
- 删除任一关键步骤后,问题不再出现。
- 输出包含实际结果和预期结果。
- 固定 JDK、依赖和构建工具版本。
当前 java-lab 已经是该问题的最小复现。真实框架问题可以先在原项目确定调用链,再将关键依赖和输入移到独立工程。
记录结论而不是摘抄代码
源码阅读结论必须包含证据和适用范围。下面的记录可以被其他人复查。
问题:空白名字在哪里被拒绝?
结论:GreetingService.greet 在拼接前调用 String.isBlank;结果为 true 时抛出 IllegalArgumentException。
证据:GreetingServiceTest#rejectsBlankName 通过;断点显示输入为三个空格,调用栈只有测试与 greet。
适用范围:当前 java-lab 版本;未验证 null 以外的 Unicode 边界输入。
下一步:增加超长名字和不可见 Unicode 字符测试。
不要用“源码就是这样写的”解释设计理由。代码能证明实现,提交记录、设计文档、Issue 和实验结果才能共同支持原因判断。
下一步
阅读如何写技术实验报告,把问题、环境、实验变量和 结论整理成其他人可以复测的文档。