陌生Java项目历险记 2 ——搜索与回溯

近期接手了一个陌生的 Java 项目,在阅读源码的过程中,一路踩坑,一路总结。本文是这一系列的第二篇,并尝试提出两种基本的源码阅读方法。前者具有普适性,后者则更高效。

在跌跌撞撞一周之后,对 Spring Security 的核心流程,有了以下基本认识:

  • JwtAuthenticationTokenFilter 负责生成认证的 AuthenticationToken(包含用户基本信息)
  • FilterSecurityInterceptor 负责权限校验(判断用户的 Role 以及相关权限)
  • ExceptionTranslationFilter 负捕获 Security 异常,并进行处理

然后在异常处理的过程中,发现以下 DEBUG 日志:

vbnet 复制代码
2024-04-03 14:53:42.724 DEBUG 66265 --- [nio-8080-exec-1] c.x.c.DynamicSecurityFilter        : Authorized public object filter invocation [GET /error?pageNum=1&pageSize=5]

于是,自然产生一个问题,为啥请求会转发到 /error,谁转发的?

如果把代码的执行过程,比作一棵树的深度优先遍历过程,则现在我们知道了起点,位于 ExceptionTranslationFilter 的 try-catch 处,需要找到一条关键执行路径,以推演得到 /error 的结果。

如果顺着代码执行过程,深度优先去阅读,理论上最终肯定可以找到结果,但那会陷入无穷的细节中,很容易迷路。

一个合理的办法是,利用广度优先的办法,找到关键行,然后再深入这一行,继续广度优先执行,直至找到最终代码为止。

这个过程可以描述如下:

  1. dedug 到起始点
  2. "step over" 执行每一行,观察日志或者变量,以确定该行是否是关键行
  3. 找到关键行之后,"step in" 该行的函数,并以该函数为新的起始点,重复执行过程 1 和 2
  4. 搜集所有的关键行,就是一条核心代码执行路径

相比于深度优先,广度优先需要重复多次 Debug,但可以快速的找到关键代码行和关键执行路径。

我实践了一下 "广度优先" 的办法,大约花了半个小时,终于定位到了关键路径:

  • org.apache.catalina.core.StandardHostValve.invoke(request, response)
  • 如果 response 有异常,则调用 StandardHostValve.status(request, response) 方法, 并最终调用 ApplicationDispatcher.forward(request, response) 转发请求,然后跟普通请求一样,重新执行一次 filters 和 servlets
scss 复制代码
if (response.isErrorReportRequired()) {
    // If an error has occurred that prevents further I/O, don't waste time
    // producing an error report that will never be read
    AtomicBoolean result = new AtomicBoolean(false);
    response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
    if (result.get()) {
        if (t != null) {
            throwable(request, response, t);
        } else {
            status(request, response);
        }
  }
}

也就是说,/error 请求是 tomcat 转发出来的,这是 tomcat 的一个异常处理机制,允许用户定制化异常的输出内容。而 Spring 默认也是利用这个机制来进行异常处理,所以才会在 ExceptionTranslationFilter 的异常处理之后,输出一个转发到 "/error" 的日志。

得益于"广度优先搜索"的代码阅读方式,"/error" 转发的疑问顺利解决了。这个方法具有较强的普适性,几乎所有问题都可以采用类似的方法,在一个合理的时间内,找到关键代码。

这个合理的时间,通常是半个小时或1个小时!

还是太长了,有没有更好的办法呢?

必须有的!

前面说过,代码执行本质是一个遍历树的过程。

树有一个典型的特征,从起点搜索终点是一个发散的过程,层次越深越发散,最终极易迷路。但如果我们知道了某个终点,回溯却是简单明确的!

因此,我们接下来的办法是,找到一个代码执行会经过的点,debug 到那里,然后回溯堆栈。

日志就是最好的回溯点,根据之前的日志,搜索"Authorized public",可以定位到代码:

打上断点,然后使用 Idea 的回溯功能:

通过这个回溯,迅速可以迅速知晓整个链路的执行情况。

在"预期终点"打上断点,然后进行回溯的办法,我称之为"断点回溯法",而前面那个从起点开始搜索的办法,我们姑且称之为"正向搜索法"。

断点回溯法非常香,大大加速了我的代码阅读进程。但这个办法也不是完美的,在使用的过程中,依然踩了不少的坑。更多的经验,敬请期待下一篇分享。

相关推荐
脚本自动化开发12 小时前
存储涨价后,磐镭WO5迷你主机值得买吗
后端
一只爱撸猫的程序猿14 小时前
我看着一个 AI 引擎在 14 天里,从"记忆"走进了"搜索"
人工智能·后端·架构
陈随易17 小时前
纯 Go 写的 JS 引擎,跑分超过了 C 语言的 QuickJS
前端·后端·程序员
卷无止境18 小时前
RAGFlow 项目全景报告:从文档解析引擎到 Agent 原生数据库
后端·python
小Z先生18 小时前
我用 Go + 系统 WebView 写了个纯本地 Postman:不装 Electron、不连云端
后端·github
IT_陈寒19 小时前
Redis连接池泄漏那次,我差点把生产环境搞崩了
前端·人工智能·后端
灿飞屋19 小时前
Kotlin与Java虚拟机三:控制流
android·java·开发语言·后端·python·kotlin·安卓
石小石Orz19 小时前
AI 正在蚕食我们的编码能力
前端·后端·算法
陈随易20 小时前
专注飞机 FocusFlight,两个人把番茄钟做成了一趟航班
前端·后端·程序员
量化分析码农20 小时前
【Python量化波动率实战 04】低波动异象:低波组真的跑赢高波组吗
后端