陌生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 小时前
免费、简单、直观的数据库设计工具和 SQL 生成器
后端·mysql
初晴~2 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱581362 小时前
InnoDB 的页分裂和页合并
数据库·后端
小_太_阳2 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾2 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
星就前端叭3 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
小林coding4 小时前
阿里云 Java 后端一面,什么难度?
java·后端·mysql·spring·阿里云
AI理性派思考者4 小时前
【保姆教程】手把手教你在Linux系统搭建早期alpha项目cysic的验证者&证明者
后端·github·gpu
从善若水4 小时前
【2024】Merry Christmas!一起用Rust绘制一颗圣诞树吧
开发语言·后端·rust