背八股文和 DEBUG 原始碼,差別在哪?

張哥說技術發表於2024-01-11

來源:江南一點雨背八股文和 DEBUG 原始碼,差別在哪?

首先這個小夥伴提的這個問題很好懂:SpringMVC 工作流程是面試八股文中的經典,上面這一套流程看起來是前後端不分時候的工作流程(因為涉及到了頁面渲染),現在都流行前後端分離架構,那麼前後端分離之後,SpringMVC 工作流程還是這樣嗎?

如果你懂一點原始碼分析技巧,這個問題其實可以自己分析去解決,但是如果你只會背八股文,那這個問題就有點棘手了。

1. 知識儲備

首先,想要自己 DEBUG 去解決問題,必須要有知識儲備。不能啥都不懂,就掌握一點 IDEA 上的 DEBUG 技巧,上來就想解決問題,那無疑是天方夜譚。

對於上面這個問題,我們至少需要如下兩個知識儲備。

  1. HandlerAdapter

首先我們需要明白 HandlerAdapter 的作用,是真真正正的瞭解,不是背誦八股文那種瞭解。HandlerAdapter 是一個介面,這個介面中最重要的方法就是 handle 方法。

為什麼會有 HandlerAdapter 存在呢?這是因為我們在 SpringMVC 中定義介面的方式有很多種,大家日常開發用的最多的就是透過 @Controller 或者 @RestController 註解來標記介面,但是這並不是介面唯一的定義方式,我們也可以透過實現 Controller 介面、HttpRequestHandler 介面甚至實現 Servlet 介面來完成介面的定義。

這些不同的介面定義方式,自然就對應了不同的呼叫方式,所以需要一個介面卡,對於框架來說,總是透過呼叫 HandlerAdapter#handle 方法來呼叫介面方法,而不同的介面定義方式則需要分別提供各自的 HandlerAdapter。

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 有如下實現類,基本上每種實現類都對應了一個介面呼叫方式:

背八股文和 DEBUG 原始碼,差別在哪?

HandlerAdapter#handle 方法的返回值是 ModelAndView,也就是按理說每個介面都應該返回一個 ModelAndView,但是有時候我們的介面並不是返回這個,最典型的就是如果我們透過實現 Servlet 介面來定義介面,Servlet 介面中的方法返回值是 void,顯然就不是 ModelAndView,那麼對於這種情況我們該怎麼處理呢?我們不妨來看下 SimpleServletHandlerAdapter,這個介面卡專門用來處理透過 Servlet 定義的介面:

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 介面:

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
  */

 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 的方法呼叫棧,就能大致分析出來。

背八股文和 DEBUG 原始碼,差別在哪?

從這個方法呼叫棧我們可以看出來,確實是呼叫了 HandlerAdapter#handle 方法,從這個位置依次往上,我們就找到了觸發 JSON 生成的方法:

@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 方法的作用了:

@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。

繼續跟進方法呼叫棧的提示,看接下來的處理。

背八股文和 DEBUG 原始碼,差別在哪?

在這個位置呼叫了 HandlerAdapter#handle 方法,該方法返回了 null,解下來該去找試圖解析器進行檢視渲染了,檢視渲染則是在接下來的 processDispatchResult 方法上:

背八股文和 DEBUG 原始碼,差別在哪?
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,所以這一步其實是跳過了,也就是沒有去找試圖解析器也沒有去渲染檢視了。

現在再回到本文一開始的問題,相信各位心中已經有答案了。

從這個問題的分析中大家也能看出來,單純的背八股文真的不如自己去讀一讀原始碼理解一下,因為八股文只能解決面試問題,對於工作,對於自身技能的提升作用是有限的。

來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70024923/viewspace-3003431/,如需轉載,請註明出處,否則將追究法律責任。

相關文章