近期接手了一个陌生的 Java 项目,在阅读源码的过程中,一路踩坑,一路总结。本文是这一系列的第三篇。在这篇文章里,进一步确认了"断点回溯法"的便捷之处,并尝试提出源码阅读的一般回溯流程,最后针对一些特定场景,指出了该方法的不足之处。
在接手一个项目的过程中,第一件事情,一般是 Run 起来,并尝试调用 API 输入数据,观察输出数据,然后通过阅读代码,了解从输入到输出的整个处理流程。
一个直觉的代码阅读方法,是从输入开始,一步步跟踪代码执行过程,直至获得输出为止。当我们在学校期间,为了知晓一个算法的执行过程,可能采用的都是这种办法。
但是,工作之后,面对真实的项目,其代码量是庞大的,其组件是复杂的,从入口开始一步步跟着看,是很容易迷路的,看着看着大概率就晕掉了。
为了应对这个问题,在之前的一篇文章中,我提出了一个阅读源码的方法,称之为"断点回溯法"。这个方法的最大特点,不是从入口开始看,而是直接跳到一个关键节点,通过这个关键节点往回溯,一次性了解整个关键链路的执行过程。
而关键节点,一般都是从日志中来找的。大部分的项目,都会在关键节点处打日志。如果没看到,考虑把日志级别调成DEBUG甚至TRACE看看,大概率是可以看到不少日志的。
以我最近看的项目为例,在调用 "/admin/list" 接口时,看到了以下日志:
js
2024-04-09 15:36:12.997 INFO 98838 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-04-09 15:36:12.997 INFO 98838 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2024-04-09 15:36:13.002 INFO 98838 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 5 ms
2024-04-09 15:36:13.031 DEBUG 98838 --- [nio-8080-exec-1] c.x.t.s.c.DynamicSecurityFilter : Authorized filter invocation [GET /admin/list?pageNum=1&pageSize=5] with attributes [25:UserManager]
2024-04-09 15:36:13.044 DEBUG 98838 --- [nio-8080-exec-1] c.x.t.s.c.DynamicSecurityFilter : Authorized public object filter invocation [GET /error?pageNum=1&pageSize=5]
很明显,DynamicSecurityFilter 是一个关键类,其打了两行相似的日志,暗示可能有两个关键的执行点。这时,我也无需去猜测其到底是干嘛的,直接代码搜索"Authorized.*with attributes" (这里有个小细节,如果全文搜索不到时,使用关键字的正则表达式),定位到代码。
然后设置断点,触发执行,回溯堆栈,一气呵成。 在回溯堆栈的过程中,我们可以看到整个堆栈的执行函数以及相关参数。通过这些函数和参数,我们可以轻易知道,整个链路是从哪里来的,其大致执行过程是咋样的。
但这里也引出了一些问题,我们可以直接看到参数的内容,却不能直接看出来参数是谁生成的。 比如上面的例子,我们看到了 AnonymousAuthenticationToken,这明显是一个身份信息。但谁生成的呢?
我们尝试回溯一下堆栈,浏览一下各个函数的源码,会发现一些线索。在这个例子中,至少有两个明显的线索。 第一个是 AnonymousAuthenticationFilter,其有可能会设置 Authentication。
第二个是 JwtAuthenticationTokenFilter,其也有可能会设置 Authentication
到这里,就很简单了,在这两个地方设置断点,再触发一次,就可以知晓结果了。
当然,我们如果知晓 Spring Security 的原理,知晓 Authentication 是通过下面这个方法设置的:
则把这个地方当作"关键节点",运用一次"断点回溯法",就立即知晓谁生成的这个 Authentication了。
通过这个回溯可以发现,Spring 内部集成了一个 AnonymousAuthenticationFilter,这个Filter在当前上下文没发现有效的Authentication时,会生成一个匿名的Authentication。
很显然,"断点回溯法"是非常香的,它极大地降低了认知负担,只需要知晓少量"关键节点"这样的信息,通过回溯,就可以快速了解代码的运行过程。
在大部分情况下,找寻这样的"关键节点",并不困难。程序自身日志,架构设计文档,网上源码解读文章,都包含了大量的这样的关键信息。
然而,在实践中,"断点回溯法"也不是完美的,至少存在以下问题:
- 到达"关键节点"的路径不止一条。这就意味着,在这个过程中,需要去比较不同的链路。而比较,就意味着大脑需要记住之前的链路,而"断点回溯法"一次只能展示一条链路,在这种情况下,会对大脑提出较高的认知负担。
- 多线程和异步。遇到这种情况,回溯一次是不够的。往往需要把多条链路拼接起来,才能完整理解整个过程。同上,也会对大脑提出较高的认知负担。
- 到了代码的深入处,我们可能无法事先知道"关键节点"。这个时候,无法运用"断点回溯点",只能退回"正向搜索法",一行一行挨个找。
理解一个真实的代码项目,总体来说是一个比较复杂的过程。运用适当的方法,可以降低我们的认知负担。只是当前阶段,还没有一个非常完美的方法。我会持续探索和实践,不断优化,敬请期待下篇分享。