很多小伙伴知道松哥最近在更 Spring 源码相关的文章和视频,视频现在已经全部录完了,公号后台回复 Spring 有视频详细介绍。
今天我想和大伙聊一些解决问题的思路,就像我在 Spring 视频中所讲,我不仅是想让小伙伴们理解 Spring 源码,看懂 Spring 源码,更是想让小伙伴们掌握 DEBUG 源码的思路和方法,相信各位在学习 Spring 源码视频的时候对此也会有所领悟。
这次刚好是有一个小伙伴在群里问了这样一个问题:
首先这个小伙伴提的这个问题很好懂:SpringMVC 工作流程是面试八股文中的经典,上面这一套流程看起来是前后端不分时候的工作流程(因为涉及到了页面渲染),现在都流行前后端分离架构,那么前后端分离之后,SpringMVC 工作流程还是这样吗?
如果你懂一点源码分析技巧,这个问题其实可以自己分析去解决,但是如果你只会背八股文,那这个问题就有点棘手了。
可能有的小伙伴认为这个并无必要,工作中不会用到,面试直接背八股文就行。但是!!!我觉得这样的分析其实是很有必要的,我们工作不仅仅是要养家糊口,我们也需要获得成就感,很多小伙伴总是感觉自己在公司天天 CRUD,工作没有挑战,是一个不折不扣的 CV 战士,那么现在这样一个思考的机会摆在你面前,你冲不冲?如果通过自己分析源码解决了心中的疑惑,会不会自信心爆棚呢?所以,尝试自己去分析这个问题是有意义的。
1. 知识储备
首先,想要自己 DEBUG 去解决问题,必须要有知识储备。不能啥都不懂,就掌握一点 IDEA 上的 DEBUG 技巧,上来就想解决问题,那无疑是天方夜谭。
对于上面这个问题,我们至少需要如下两个知识储备。
- HandlerAdapter
首先我们需要明白 HandlerAdapter 的作用,是真真正正的了解,不是背诵八股文那种了解。HandlerAdapter 是一个接口,这个接口中最重要的方法就是 handle 方法。
为什么会有 HandlerAdapter 存在呢?这是因为我们在 SpringMVC 中定义接口的方式有很多种,大家日常开发用的最多的就是通过 @Controller
或者 @RestController
注解来标记接口,但是这并不是接口唯一的定义方式,我们也可以通过实现 Controller 接口、HttpRequestHandler 接口甚至实现 Servlet 接口来完成接口的定义。
这些不同的接口定义方式,自然就对应了不同的调用方式,所以需要一个适配器,对于框架来说,总是通过调用 HandlerAdapter#handle 方法来调用接口方法,而不同的接口定义方式则需要分别提供各自的 HandlerAdapter。
1 | public interface HandlerAdapter { |
我们看到默认的 HandlerAdapter 有如下实现类,基本上每种实现类都对应了一个接口调用方式:
HandlerAdapter#handle 方法的返回值是 ModelAndView,也就是按理说每个接口都应该返回一个 ModelAndView,但是有时候我们的接口并不是返回这个,最典型的就是如果我们通过实现 Servlet 接口来定义接口,Servlet 接口中的方法返回值是 void,显然就不是 ModelAndView,那么对于这种情况我们该怎么处理呢?我们不妨来看下 SimpleServletHandlerAdapter,这个适配器专门用来处理通过 Servlet 定义的接口:
1 | public class SimpleServletHandlerAdapter implements HandlerAdapter { |
可以看到,这个源码可太简单了,直接把接口类强转为 Servlet 然后进行调用。至于 handle 方法的返回值,直接返回 null 算了。
这是我们需要的知识储备一,如果你懂得上面的内容,大概也就能猜出来,如果前后端分离中接口返回了 JSON,那么执行目标接口的 HandlerAdapter#handle 方法估计也是返回 null。
- HttpMessageConverter
第二个知识储备就是需要明白在 SpringMVC 中 JSON 的生成、解析是谁来完成的。
SpringMVC 返回 JSON 参数特别方便,接口方法直接返回对象就可以了,系统会自动将之转为 JSON 字符串然后写回去;如果提交的参数是 JSON 字符串,我们也只需要在接口中添加 @RequestBody 注解,这样系统就会自动将 JSON 字符串转为 Java 对象了。
这一切的实现,离不开 HttpMessageConverter。我们先来看看 HttpMessageConverter 接口:
1 | public interface HttpMessageConverter<T> { |
这个接口中最重要的就是 read 和 write 方法。其中 read 方法是将请求参数中的 JSON 字符串转为 Java 对象,write 方法是将请求响应中的 Java 对象转为 JSON 字符串。
每一个 JSON 处理工具都会提供自身的 HttpMessageConverter,以 Spring Boot 中的 jackson 为例,它的 HttpMessageConverter 是 MappingJackson2HttpMessageConverter。
好了,有了如上两点知识储备,接下来我们就可以结合 IDEA 中的 DEBUG 技能,快速梳理出问题的答案了。
2. 问题分析
那么这个问题从哪里切入呢?
既然服务端要返回 JSON,就必然调用到 HttpMessageConverter#write 方法,那么我们就写一个返回 JSON 的接口,然后在 MappingJackson2HttpMessageConverter#write 方法上打断点,因为最终要生成 JSON 必然会经过该方法。然后结合 IDEA 中 DEBUG 的方法调用栈,就能大致分析出来。
从这个方法调用栈我们可以看出来,确实是调用了 HandlerAdapter#handle 方法,从这个位置依次往上,我们就找到了触发 JSON 生成的方法:
1 | @Nullable |
顺着这个方法的调用栈,我们发现 JSON 的生成是在 invocableMethod.invokeAndHandle
方法中被触发的,包括 JSON 的写出都是在这个方法中完成的,这块代码简单,我就不贴图了。
那问题来了,JSON 已经写回去了,现在 handle 方法需要返回 ModelAndView 该怎么办呢?这就是接下来 getModelAndView 方法的作用了:
1 | @Nullable |
这个方法有一句判断 mavContainer.isRequestHandled()
,看方法名就知道表示检查请求是否已经被处理了,由于前面已经处理完 JSON 了,所以这个方法就返回 true,这就进而导致返回的 ModelAndView 是一个 null。
继续跟进方法调用栈的提示,看接下来的处理。
在这个位置调用了 HandlerAdapter#handle 方法,该方法返回了 null,解下来该去找试图解析器进行视图渲染了,视图渲染则是在接下来的 processDispatchResult 方法上:
1 | private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, |
大家可以看到,这里会判断这个 ModelAndView 是否为 null,不为 null 的话,会去调用 render 方法进行视图的渲染,这个时候就会去找到视图解析器,分析视图,渲染视图,这个松哥之前也都和大家聊过了。但我们这里由于 mv 是 null,所以这一步其实是跳过了,也就是没有去找试图解析器也没有去渲染视图了。
现在再回到本文一开始的问题,相信各位心中已经有答案了。
从这个问题的分析中大家也能看出来,单纯的背八股文真的不如自己去读一读源码理解一下,因为八股文只能解决面试问题,对于工作,对于自身技能的提升作用是有限的。