SpringMVC 源碼分析之 FrameworkServlet

松哥原創的 Spring Boot 視頻教程已經殺青,感興趣的小夥伴戳這裏-->Spring Boot+Vue+微人事視頻教程


前面和小夥伴們聊了 SpringMVC 的初始化流程,相信大家對於 SpringMVC 的初始化過程都有一個基本認知了,今天我們就來看看當一個請求到達後,它的執行流程是什麼樣的?當然這個流程比較長,松哥這裏可能會分兩篇文章來和大家分享。

很多小夥伴都知道 SpringMVC 的核心是 DispatcherServlet,而 DispatcherServlet 的父類就是 FrameworkServlet,因此我們先來看看 FrameworkServlet,這有助於我們理解 DispatcherServlet。

1.FrameworkServlet

FrameworkServlet 繼承自 HttpServletBean,而 HttpServletBean 繼承自 HttpServlet,HttpServlet 就是 JavaEE 裏邊的東西了,這裏我們不做討論,從 HttpServletBean 開始就是框架的東西了,但是 HttpServletBean 比較特殊,它的特殊在於它沒有進行任何的請求處理,只是參與了一些初始化的操作,這些比較簡單,而且我們在上篇文章中也已經分析過了,所以這裏我們對 HttpServletBean 不做分析,就直接從它的子類 FrameworkServlet 開始看起。

和所有的 Servlet 一樣,FrameworkServlet 對請求的處理也是從 service 方法開始,我們先來看看該方法 FrameworkServlet#service:

@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
  throws ServletException, IOException 
{
 HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
 if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
  processRequest(request, response);
 }
 else {
  super.service(request, response);
 }
}

可以看到,在該方法中,首先獲取到當前請求方法,然後對 patch 請求額外關照了下,其他類型的請求統統都是 super.service 進行處理。

然而在 HttpServlet 中並未對 doGet、doPost 等請求進行實質性處理,所以 FrameworkServlet 中還重寫了各種請求對應的方法,如 doDelete、doGet、doOptions、doPost、doPut、doTrace 等,其實就是除了 doHead 之外的其他方法都重寫了。

我們先來看看 doDelete、doGet、doPost 以及 doPut 四個方法:

@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
  throws ServletException, IOException 
{
 processRequest(request, response);
}
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
  throws ServletException, IOException 
{
 processRequest(request, response);
}
@Override
protected final void doPut(HttpServletRequest request, HttpServletResponse response)
  throws ServletException, IOException 
{
 processRequest(request, response);
}
@Override
protected final void doDelete(HttpServletRequest request, HttpServletResponse response)
  throws ServletException, IOException 
{
 processRequest(request, response);
}

可以看到,這裏又把請求交給 processRequest 去處理了,在 processRequest 方法中則會進一步調用到 doService,對不同類型的請求分類處理。

doOptions 和 doTrace 則稍微有些差異,如下:

@Override
protected void doOptions(HttpServletRequest request, HttpServletResponse response)
  throws ServletException, IOException 
{
 if (this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) {
  processRequest(request, response);
  if (response.containsHeader("Allow")) {
   return;
  }
 }
 super.doOptions(request, new HttpServletResponseWrapper(response) {
  @Override
  public void setHeader(String name, String value) {
   if ("Allow".equals(name)) {
    value = (StringUtils.hasLength(value) ? value + ", " : "") + HttpMethod.PATCH.name();
   }
   super.setHeader(name, value);
  }
 });
}
@Override
protected void doTrace(HttpServletRequest request, HttpServletResponse response)
  throws ServletException, IOException 
{
 if (this.dispatchTraceRequest) {
  processRequest(request, response);
  if ("message/http".equals(response.getContentType())) {
   return;
  }
 }
 super.doTrace(request, response);
}

可以看到這兩個方法的處理多了一層邏輯,就是去選擇是在當前方法中處理對應的請求還是交給父類去處理,由於 dispatchOptionsRequest 和 dispatchTraceRequest 變量默認都是 false,因此默認情況下,這兩種類型的請求都是交給了父類去處理。

2.processRequest

我們再來看 processRequest,這算是 FrameworkServlet 的核心方法了:

protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
  throws ServletException, IOException 
{
 long startTime = System.currentTimeMillis();
 Throwable failureCause = null;
 LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
 LocaleContext localeContext = buildLocaleContext(request);
 RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
 ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
 WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
 asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
 initContextHolders(request, localeContext, requestAttributes);
 try {
  doService(request, response);
 }
 catch (ServletException | IOException ex) {
  failureCause = ex;
  throw ex;
 }
 catch (Throwable ex) {
  failureCause = ex;
  throw new NestedServletException("Request processing failed", ex);
 }
 finally {
  resetContextHolders(request, previousLocaleContext, previousAttributes);
  if (requestAttributes != null) {
   requestAttributes.requestCompleted();
  }
  logResult(request, response, failureCause, asyncManager);
  publishRequestHandledEvent(request, response, startTime, failureCause);
 }
}

這個方法雖然比較長,但是其實它的核心就是最中間的 doService 方法,以 doService 爲界,我們可以將該方法的內容分爲三部分:

  1. doService 之前主要是一些準備工作,準備工作主要乾了兩件事,第一件事就是從 LocaleContextHolder 和 RequestContextHolder 中分別獲取它們原來保存的 LocaleContext 和 RequestAttributes 對象存起來,然後分別調用 buildLocaleContext 和 buildRequestAttributes 方法獲取到當前請求的 LocaleContext 和 RequestAttributes 對象,再通過 initContextHolders 方法將當前請求的 LocaleContext 和 RequestAttributes 對象分別設置到 LocaleContextHolder 和 RequestContextHolder 對象中;第二件事則是獲取到異步管理器並設置攔截器。
  2. 接下來就是 doService 方法,這是一個抽象方法,具體的實現在 DispatcherServlet 中,這個松哥放到 DispatcherServlet 中再和大家分析。
  3. 第三部分就是 finally 中,這個裏邊幹了兩件事:第一件事就是將 LocaleContextHolder 和 RequestContextHolder 中對應的對象恢復成原來的樣子(參考第一步);第二件事就是通過 publishRequestHandledEvent 方法發佈一個 ServletRequestHandledEvent 類型的消息。

經過上面的分析,大家發現,processRequest 其實主要做了兩件事,第一件事就是對 LocaleContext 和 RequestAttributes 的處理,第二件事就是發佈事件。我們對這兩件事分別來研究。

2.1 LocaleContext 和 RequestAttributes

LocaleContext 和 RequestAttributes 都是接口,不同的是裏邊存放的對象不同。

2.1.1 LocaleContext

LocaleContext 裏邊存放着 Locale,也就是本地化信息,如果我們需要支持國際化,就會用到 Locale。

國際化的時候,如果我們需要用到 Locale 對象,第一反應就是從 HttpServletRequest 中獲取,像下面這樣:

Locale locale = req.getLocale();

但是大家知道,HttpServletRequest 只存在於 Controller 中,如果我們想要在 Service 層獲取 HttpServletRequest,就得從 Controller 中傳參數過來,這樣就比較麻煩,特別是有的時候 Service 中相關方法都已經定義好了再去修改,就更頭大了。

所以 SpringMVC 中還給我們提供了 LocaleContextHolder,這個工具就是用來保存當前請求的 LocaleContext 的。當大家看到 LocaleContextHolder 時不知道有沒有覺得眼熟,松哥在之前的 Spring Security 系列教程中和大家聊過 SecurityContextHolder,這兩個的原理基本一致,都是基於 ThreadLocal 來保存變量,進而確保不同線程之間互不干擾,對 ThreadLocal 不熟悉的小夥伴,可以看看松哥的 Spring Security 系列,之前有詳細分析過(公號後臺回覆 ss)。

有了 LocaleContextHolder 之後,我們就可以在任何地方獲取 Locale 了,例如在 Service 中我們可以通過如下方式獲取 Locale:

Locale locale = LocaleContextHolder.getLocale();

上面這個 Locale 對象實際上就是從 LocaleContextHolder 中的 LocaleContext 裏邊取出來的。

需要注意的是,SpringMVC 中還有一個 LocaleResolver 解析器,所以前面 req.getLocale() 並不總是獲取到 Locale 的值,這個松哥在以後的文章中再和小夥伴們細聊。

2.1.2 RequestAttributes

RequestAttributes 是一個接口,這個接口可以用來 get/set/remove 某一個屬性。

RequestAttributes 有諸多實現類,默認使用的是 ServletRequestAttributes,通過 ServletRequestAttributes,我們可以 getRequest、getResponse 以及 getSession。

在 ServletRequestAttributes 的具體實現中,會通過 scope 參數判斷操作 request 還是操作 session(如果小夥伴們不記得 Spring 中的作用域問題,可以公號後臺回覆 spring,看看松哥錄製的免費的 Spring 入門教程,裏邊有講),我們來看一下 ServletRequestAttributes#setAttribute 方法(get/remove 方法執行邏輯類似):

public void setAttribute(String name, Object value, int scope) {
    if (scope == 0) {
        if (!this.isRequestActive()) {
            throw new IllegalStateException("Cannot set request attribute - request is not active anymore!");
        }
        this.request.setAttribute(name, value);
    } else {
        HttpSession session = this.obtainSession();
        this.sessionAttributesToUpdate.remove(name);
        session.setAttribute(name, value);
    }
}

可以看到,這裏會先判斷 scope,scope 爲 0 就操作 request,scope 爲 1 就操作 session。如果操作的是 request,則需要首先通過 isRequestActive 方法判斷當前 request 是否執行完畢,如果執行完畢,就不可以再對其進行其他操作了(當執行了 finally 代碼塊中的 requestAttributes.requestCompleted 方法後,isRequestActive 就會返回 false)。

和 LocaleContext 類似,RequestAttributes 被保存在 RequestContextHolder 中,RequestContextHolder 的原理也和 SecurityContextHolder 類似,這裏不再贅述。

看了上面的講解,大家應該發現了,在 SpringMVC 中,如果我們需要在 Controller 之外的其他地方使用 request、response 以及 session,其實不用每次都從 Controller 中傳遞 request、response 以及 session 等對象,我們完全可以直接通過 RequestContextHolder 來獲取,像下面這樣:

ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
HttpServletResponse response = servletRequestAttributes.getResponse();

是不是非常 easy!

2.2 事件發佈

最後就是 processRequest 方法中的事件發佈了。

在 finally 代碼塊中會調用 publishRequestHandledEvent 方法發送一個 ServletRequestHandledEvent 類型的事件,具體發送代碼如下:

private void publishRequestHandledEvent(HttpServletRequest request, HttpServletResponse response,
  long startTime, @Nullable Throwable failureCause)
 
{
 if (this.publishEvents && this.webApplicationContext != null) {
  // Whether or not we succeeded, publish an event.
  long processingTime = System.currentTimeMillis() - startTime;
  this.webApplicationContext.publishEvent(
    new ServletRequestHandledEvent(this,
      request.getRequestURI(), request.getRemoteAddr(),
      request.getMethod(), getServletConfig().getServletName(),
      WebUtils.getSessionId(request), getUsernameForRequest(request),
      processingTime, failureCause, response.getStatus()));
 }
}

可以看到,事件的發送需要 publishEvents 爲 true,而該變量默認就是 true。如果需要修改該變量的值,可以在 web.xml 中配置 DispatcherServlet 時,通過 init-param 節點順便配置一下該變量的值。正常情況下,這個事件總是會被髮送出去,如果項目有需要,我們可以監聽該事件,如下:

@Component
public class ServletRequestHandleListener implements ApplicationListener<ServletRequestHandledEvent{
    @Override
    public void onApplicationEvent(ServletRequestHandledEvent servletRequestHandledEvent) {
        System.out.println("請求執行完畢-"+servletRequestHandledEvent.getRequestUrl());
    }
}

當一個請求執行完畢時,該事件就會被觸發。

3.小結

這篇文章主要和小夥伴們分享了 SpringMVC 中 DispatcherServlet 的父類 FrameworkServlet,FrameworkServlet 的功能其實比較簡單,主要就是在 service 方法中增加了對 PATCH 的處理,然後其他類型的請求都被歸類到 processRequest 方法中進行統一處理,processRequest 方法則又分了三部分,首先是對 LocaleContext 和 RequestAttributes 的處理,然後執行 doService,最後在 finally 代碼塊中對 LocaleContext 和 RequestAttributes 屬性進行復原,同時發佈一個請求結束的事件。

doService 是重頭戲,松哥將在下篇文章中和大家分享。好啦,今天就先和小夥伴們聊這麼多~

本文分享自微信公衆號 - 江南一點雨(a_javaboy)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章