近期接手了一个陌生的 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 的结果。
如果顺着代码执行过程,深度优先去阅读,理论上最终肯定可以找到结果,但那会陷入无穷的细节中,很容易迷路。
一个合理的办法是,利用广度优先的办法,找到关键行,然后再深入这一行,继续广度优先执行,直至找到最终代码为止。
这个过程可以描述如下:
- dedug 到起始点
- "step over" 执行每一行,观察日志或者变量,以确定该行是否是关键行
- 找到关键行之后,"step in" 该行的函数,并以该函数为新的起始点,重复执行过程 1 和 2
- 搜集所有的关键行,就是一条核心代码执行路径
相比于深度优先,广度优先需要重复多次 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 的回溯功能:
通过这个回溯,迅速可以迅速知晓整个链路的执行情况。
在"预期终点"打上断点,然后进行回溯的办法,我称之为"断点回溯法",而前面那个从起点开始搜索的办法,我们姑且称之为"正向搜索法"。
断点回溯法非常香,大大加速了我的代码阅读进程。但这个办法也不是完美的,在使用的过程中,依然踩了不少的坑。更多的经验,敬请期待下一篇分享。