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

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

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

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

相关推荐
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
枫叶_v4 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
杜杜的man5 小时前
【go从零单排】Closing Channels通道关闭、Range over Channels
开发语言·后端·golang
java小吕布5 小时前
Java中Properties的使用详解
java·开发语言·后端
2401_857610036 小时前
Spring Boot框架:电商系统的技术优势
java·spring boot·后端
杨哥带你写代码8 小时前
网上商城系统:Spring Boot框架的实现
java·spring boot·后端
camellias_8 小时前
SpringBoot(二十一)SpringBoot自定义CURL请求类
java·spring boot·后端
背水8 小时前
初识Spring
java·后端·spring
晴天飛 雪9 小时前
Spring Boot MySQL 分库分表
spring boot·后端·mysql
weixin_537590459 小时前
《Spring boot从入门到实战》第七章习题答案
数据库·spring boot·后端