前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >背八股文和 DEBUG 源码,差别在哪?

背八股文和 DEBUG 源码,差别在哪?

作者头像
江南一点雨
发布2024-01-10 15:56:35
1090
发布2024-01-10 15:56:35
举报
文章被收录于专栏:玩转JavaEE玩转JavaEE

很多小伙伴知道松哥最近在更 Spring 源码相关的文章和视频,视频现在已经全部录完了,公号后台回复 Spring 有视频详细介绍。

今天我想和大伙聊一些解决问题的思路,就像我在 Spring 视频中所讲,我不仅是想让小伙伴们理解 Spring 源码,看懂 Spring 源码,更是想让小伙伴们掌握 DEBUG 源码的思路和方法,相信各位在学习 Spring 源码视频的时候对此也会有所领悟。

这次刚好是有一个小伙伴在群里问了这样一个问题:

首先这个小伙伴提的这个问题很好懂:SpringMVC 工作流程是面试八股文中的经典,上面这一套流程看起来是前后端不分时候的工作流程(因为涉及到了页面渲染),现在都流行前后端分离架构,那么前后端分离之后,SpringMVC 工作流程还是这样吗?

如果你懂一点源码分析技巧,这个问题其实可以自己分析去解决,但是如果你只会背八股文,那这个问题就有点棘手了。

可能有的小伙伴认为这个并无必要,工作中不会用到,面试直接背八股文就行。但是!!!我觉得这样的分析其实是很有必要的,我们工作不仅仅是要养家糊口,我们也需要获得成就感,很多小伙伴总是感觉自己在公司天天 CRUD,工作没有挑战,是一个不折不扣的 CV 战士,那么现在这样一个思考的机会摆在你面前,你冲不冲?如果通过自己分析源码解决了心中的疑惑,会不会自信心爆棚呢?所以,尝试自己去分析这个问题是有意义的。

1. 知识储备

首先,想要自己 DEBUG 去解决问题,必须要有知识储备。不能啥都不懂,就掌握一点 IDEA 上的 DEBUG 技巧,上来就想解决问题,那无疑是天方夜谭。

对于上面这个问题,我们至少需要如下两个知识储备。

  1. HandlerAdapter

首先我们需要明白 HandlerAdapter 的作用,是真真正正的了解,不是背诵八股文那种了解。HandlerAdapter 是一个接口,这个接口中最重要的方法就是 handle 方法。

为什么会有 HandlerAdapter 存在呢?这是因为我们在 SpringMVC 中定义接口的方式有很多种,大家日常开发用的最多的就是通过 @Controller 或者 @RestController 注解来标记接口,但是这并不是接口唯一的定义方式,我们也可以通过实现 Controller 接口、HttpRequestHandler 接口甚至实现 Servlet 接口来完成接口的定义。

这些不同的接口定义方式,自然就对应了不同的调用方式,所以需要一个适配器,对于框架来说,总是通过调用 HandlerAdapter#handle 方法来调用接口方法,而不同的接口定义方式则需要分别提供各自的 HandlerAdapter。

代码语言:javascript
复制
public interface HandlerAdapter {
 boolean supports(Object handler);
 @Nullable
 ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
 @Deprecated
 long getLastModified(HttpServletRequest request, Object handler);
}

我们看到默认的 HandlerAdapter 有如下实现类,基本上每种实现类都对应了一个接口调用方式:

HandlerAdapter#handle 方法的返回值是 ModelAndView,也就是按理说每个接口都应该返回一个 ModelAndView,但是有时候我们的接口并不是返回这个,最典型的就是如果我们通过实现 Servlet 接口来定义接口,Servlet 接口中的方法返回值是 void,显然就不是 ModelAndView,那么对于这种情况我们该怎么处理呢?我们不妨来看下 SimpleServletHandlerAdapter,这个适配器专门用来处理通过 Servlet 定义的接口:

代码语言:javascript
复制
public class SimpleServletHandlerAdapter implements HandlerAdapter {

 @Override
 public boolean supports(Object handler) {
  return (handler instanceof Servlet);
 }

 @Override
 @Nullable
 public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
   throws Exception {

  ((Servlet) handler).service(request, response);
  return null;
 }

 @Override
 @SuppressWarnings("deprecation")
 public long getLastModified(HttpServletRequest request, Object handler) {
  return -1;
 }

}

可以看到,这个源码可太简单了,直接把接口类强转为 Servlet 然后进行调用。至于 handle 方法的返回值,直接返回 null 算了。

这是我们需要的知识储备一,如果你懂得上面的内容,大概也就能猜出来,如果前后端分离中接口返回了 JSON,那么执行目标接口的 HandlerAdapter#handle 方法估计也是返回 null。

  1. HttpMessageConverter

第二个知识储备就是需要明白在 SpringMVC 中 JSON 的生成、解析是谁来完成的。

SpringMVC 返回 JSON 参数特别方便,接口方法直接返回对象就可以了,系统会自动将之转为 JSON 字符串然后写回去;如果提交的参数是 JSON 字符串,我们也只需要在接口中添加 @RequestBody 注解,这样系统就会自动将 JSON 字符串转为 Java 对象了。

这一切的实现,离不开 HttpMessageConverter。我们先来看看 HttpMessageConverter 接口:

代码语言:javascript
复制
public interface HttpMessageConverter<T> {

    // 省略。。。

 /**
  * Read an object of the given type from the given input message, and returns it.
  * @param clazz the type of object to return. This type must have previously been passed to the
  * {@link #canRead canRead} method of this interface, which must have returned {@code true}.
  * @param inputMessage the HTTP input message to read from
  * @return the converted object
  * @throws IOException in case of I/O errors
  * @throws HttpMessageNotReadableException in case of conversion errors
  */
 T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
   throws IOException, HttpMessageNotReadableException;

 /**
  * Write a given object to the given output message.
  * @param t the object to write to the output message. The type of this object must have previously been
  * passed to the {@link #canWrite canWrite} method of this interface, which must have returned {@code true}.
  * @param contentType the content type to use when writing. May be {@code null} to indicate that the
  * default content type of the converter must be used. If not {@code null}, this media type must have
  * previously been passed to the {@link #canWrite canWrite} method of this interface, which must have
  * returned {@code true}.
  * @param outputMessage the message to write to
  * @throws IOException in case of I/O errors
  * @throws HttpMessageNotWritableException in case of conversion errors
  */
 void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
   throws IOException, HttpMessageNotWritableException;

}

这个接口中最重要的就是 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 生成的方法:

代码语言:javascript
复制
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
  HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
 //省略。。。
 invocableMethod.invokeAndHandle(webRequest, mavContainer);
 if (asyncManager.isConcurrentHandlingStarted()) {
  return null;
 }
 return getModelAndView(mavContainer, modelFactory, webRequest);
}

顺着这个方法的调用栈,我们发现 JSON 的生成是在 invocableMethod.invokeAndHandle 方法中被触发的,包括 JSON 的写出都是在这个方法中完成的,这块代码简单,我就不贴图了。

那问题来了,JSON 已经写回去了,现在 handle 方法需要返回 ModelAndView 该怎么办呢?这就是接下来 getModelAndView 方法的作用了:

代码语言:javascript
复制
@Nullable
private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
  ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
 modelFactory.updateModel(webRequest, mavContainer);
 if (mavContainer.isRequestHandled()) {
  return null;
 }
 //省略。。。
}

这个方法有一句判断 mavContainer.isRequestHandled(),看方法名就知道表示检查请求是否已经被处理了,由于前面已经处理完 JSON 了,所以这个方法就返回 true,这就进而导致返回的 ModelAndView 是一个 null。

继续跟进方法调用栈的提示,看接下来的处理。

在这个位置调用了 HandlerAdapter#handle 方法,该方法返回了 null,解下来该去找试图解析器进行视图渲染了,视图渲染则是在接下来的 processDispatchResult 方法上:

代码语言:javascript
复制
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
  @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
  @Nullable Exception exception) throws Exception {
 //省略
 // Did the handler return a view to render?
 if (mv != null && !mv.wasCleared()) {
  render(mv, request, response);
  if (errorView) {
   WebUtils.clearErrorRequestAttributes(request);
  }
 }
 else {
  if (logger.isTraceEnabled()) {
   logger.trace("No view rendering, null ModelAndView returned.");
  }
 }
    //省略。。。
}

大家可以看到,这里会判断这个 ModelAndView 是否为 null,不为 null 的话,会去调用 render 方法进行视图的渲染,这个时候就会去找到视图解析器,分析视图,渲染视图,这个松哥之前也都和大家聊过了。但我们这里由于 mv 是 null,所以这一步其实是跳过了,也就是没有去找试图解析器也没有去渲染视图了。

现在再回到本文一开始的问题,相信各位心中已经有答案了。

从这个问题的分析中大家也能看出来,单纯的背八股文真的不如自己去读一读源码理解一下,因为八股文只能解决面试问题,对于工作,对于自身技能的提升作用是有限的。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-01-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 江南一点雨 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 知识储备
  • 2. 问题分析
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档