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

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

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

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

相关推荐
废柴程序员_熊熊1 分钟前
用 Docker 搭 n8n 私有服务器:2 个容器、382 MB 内存,附可导入的测试工程
后端
杨运交25 分钟前
086-对象存储模块-基于Spring Boot的文件存储模块监控设计与实现
java·spring boot·后端
利威尔xu38 分钟前
万字详解|Go 进阶(五):sync 包底层:Mutex 自旋 4 次转饥饿,WaitGroup 漏 1 个 Done 挂起 3000 goroutine
后端·golang·并发编程·源码分析·sync
贾公子1 小时前
检测结果怎么落库——表结构、状态机与三类对账
后端·mysql
136096757232 小时前
LLM 当裁判可靠吗:位置偏见、长度偏见、自我偏好
后端
拖孩2 小时前
流量主门槛从 500 降到 100,我的小程序上线 18 小时够到了线
前端·后端·微信小程序
养军博客2 小时前
Spring Security 4.x / 5.x / 6.x 配置全对比:前后端不分离与前后端分离
java·后端·spring·springsecurity
wifi___2 小时前
SpringBoot自动装配原理
java·spring boot·后端
日日大王学编程2 小时前
AI 时代后端方向新人的未来规划与看法
人工智能·后端·python·ai·golang
Fox爱分享3 小时前
都问程序员双十一买什么,我的答案一定是这块专业编程显示器
后端·程序员·ai编程