陌生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 的回溯功能:

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

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

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

相关推荐
码云之上4 分钟前
聊聊如何设计一个高效、稳定的 Node.js 接入层
前端·后端·node.js
IT_陈寒1 小时前
Vite项目build后路由404了?你可能漏了这个小配置
前端·人工智能·后端
宸津-代码粉碎机2 小时前
Spring AI企业级实战|从RAG优化到Agent多工具调度
java·大数据·人工智能·后端·python·spring
吴佳浩2 小时前
AI Infra 的真相:Go 没输,rust也不是取代
后端·rust·go
喵个咪2 小时前
实时游戏网络协议深度对比:KCP vs WebRTC vs WebSocket
后端·websocket·webrtc
普通网友2 小时前
springboot之集成Elasticsearch
spring boot·后端·elasticsearch
QuZero2 小时前
Guava Cache Deep Dive
java·后端·算法·guava
leeyi3 小时前
SSE 实时推流 —— Token 怎么一个个蹦出来
后端·agent
leeyi3 小时前
ReAct 循环的 50 行 Go 实现,逐行拆解
后端·agent
leeyi3 小时前
HITL:让人类随时叫停 AI,并且能优雅地继续
后端·agent