SpringMVC源碼分析7、DispatcherServlet處理請求源碼分析

通過前面幾篇文章的分析, 此時此刻咱們再來看SpringMVC執行請求的流程就基本不會遇到盲點了, 在整個SpringMVC 的源碼分析中, 只分析了HandlerMapping、HandlerAdapter兩個組件, 至於ViewResovler, 筆者決定不去分析了, 由於這個組件不太難, 若是將HandlerAdapter都拿下來了的話, 讀ViewResolver的源碼就會很輕鬆, 由於都是相似 的, 找到對應的視圖解析器, 調用解析的方法進行解析, 這前提是咱們返回的是一個視圖名稱, 若是用了 @ResponseBody註解, 那麼視圖解析也就沒啥必要性了, 由於數據已經在ReturnValueResolver中利用消息轉換器寫 回到請求端了, 因此視圖解析器實際上是針對於咱們返回了視圖的狀況, 有了前面的Model以及ModelAndViewContainer 知識的鋪墊, 看這一塊代碼簡直不要過輕鬆..........本篇文章則對SpringMVC的執行流程進行分析, 看看咱們的請求 到達了Servlet後究竟是如何被處理的前端

引入

在前面的文章中, 咱們對DispatcherServlet的初始化流程進行了分析, 首先要明白的一點是, 通過此次的初始化,
整個Web容器已經被建立好了, SpringMVC的九大組件也已經被放置到了DispatcherServlet中了, 請求的處理其實就
是對這些組件的應用而已, 咱們假設有以下的測試用例:
@Controller
public class TestController {
    @RequestMapping( value="/test", method = RequestMethod.GET )
    @ResponseBody
    public List<String> test () {
        return Arrays.asList( "a", "b" );
    }
}

經過瀏覽器請求了以下域名: localhost/test
複製代碼

DispatcherServlet請求的處理

入口點: FrameworkServlet

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

分析:
    FrameworkServlet重寫了HttpServlet的service方法, 請求進來的時候, 首先是進入到了這個service方法,
    經過從請求中拿到請求方法, 將其轉爲枚舉類HttpMethod, 最後仍是調用了HttpServlet的service方法, 之所
    以重寫這個方法, 緣由是增長了對請求方法類型爲patch的處理, 在HttpServlet的service中是沒有對這個類型
    的請求進行處理的, 咱們先不理會processRequest方法是作啥的, 首先須要知道, 當請求是其餘類型的時候, 最
    終會調用到HttpServlet的service方法的, 在這個方法中有各類doXXX方法, 然而!!FrameworkServlet重寫了
    這些doXXX方法, 以doPost爲例:
        protected final void doPost(HttpServletRequest request, HttpServletResponse response) {
            processRequest(request, response);
        }

    ok, 到這裏爲止, 你們應該就清楚了, 真正處理請求的是這個processRequest方法, 全部的請求最終都會到
    FrameworkServlet中的doXX方法, 最終用processRequest方法來調用
複製代碼

processRequest: 真正處理請求的方法

  • processRequest引入
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) {
    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    LocaleContext localeContext = buildLocaleContext(request);

    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes 
                            = buildRequestAttributes(request, response, previousAttributes);

    initContextHolders(request, localeContext, requestAttributes);

    try {
        doService(request, response);
    } finally {
        resetContextHolders(request, previousLocaleContext, previousAttributes);
        if (requestAttributes != null) {
            requestAttributes.requestCompleted();
        }
        publishRequestHandledEvent(request, response, startTime, failureCause);
    }
}

分析:
    這個方法主要分爲六個部分, 接下來咱們對這些進行拆分, 一個個分析
複製代碼
  • LocalContext&RequestAttributes
首先來看前三個部分, LocalContext、RequestAttributes、initContextHolders

先來講一下什麼是LocalContext吧, 翻譯過來是語言環境, 好比國際化相關的功能就跟語言環境有關, 中文環境仍是
英文環境這樣的意思, request請求對象中有一個getLocale方法用來獲取當前的語言環境, 將其保存在LocalContext
中的局部變量中, 以SimpleLocaleContext爲例:
public class SimpleLocaleContext implements LocaleContext {
	private final Locale locale;
	public SimpleLocaleContext(@Nullable Locale locale) {
		this.locale = locale;
	}

	public Locale getLocale() { return this.locale; }
}

很簡單的一個代碼, 說明了語言環境實際上是將Local對象保存了起來而已, 若是想要在整個請求任何地方都能拿到這個
語言環境, 或者說拿到這個Local對象, 你們應該很容易想到用ThreadLocal來保存就行了, LocaleContextHolder
就是一個ThreadLocal, 用來保存LocaleContext的, 接下來看看上面這個processRequest的第一部分:
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);

很簡單, 從TheadLocal中拿到以前的語言環境, 臨時保存起來, 而後利用buildLocaleContext方法構建當前的語言
環境對象, 這個方法被DispatcherServlet類重寫了:
    protected LocaleContext buildLocaleContext(final HttpServletRequest request) {
		LocaleResolver lr = this.localeResolver;
		if (lr instanceof LocaleContextResolver) {
			return ((LocaleContextResolver) lr).resolveLocaleContext(request);
		}
		else {
			return () -> (lr != null ? lr.resolveLocale(request) : request.getLocale());
		}
	}

看到上面的代碼, 能夠看到, 最終都是利用一個LocaleResolver來解析語言環境對象的, 即SpringMVC利用這個組件
來完成對Local的解析, 那這第一部分就很清晰了, 將以前的語言環境臨時保存, 構建本次請求的語言環境, 在原生的
請求上增長了LocaleResolver語言環境解析器來進行解析

ServletRequestAttributes對象, 其實這個對象在以前咱們就用到了, 以前講解@SessionAttributes註解的時候,
將Model數據放入到session中最終就是利用這個ServletRequestAttributes對象的, 該對象的功能很簡單, 保存了
當前請求的HttpServletRequest、HttpServletResponse、HttpSession, 提供了對session屬性的設置和獲取, 此
時再來看這第二部分的代碼:
    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes 
                            = buildRequestAttributes(request, response, previousAttributes);

先從RequestContextHolder中獲取原來的ServletRequestAttributes, 臨時保存起來, 而後利用request、
response、previousAttributes構建一個ServletRequestAttributes, 在上面是構建LocaleContext

到此爲止, LocaleContext和ServletRequestAttributes就已經構建好了, 咱們以前只看到了從
LocaleContextHolder和RequestContextHolder中獲取這兩個對象, 接下來第三部分的代碼就是將當前請求的這兩個
對象放入到對應的ThreadLocal中, 即initContextHolders(request, localeContext, requestAttributes);
的調用, 咱們來看看即initContextHolders方法:

private void initContextHolders(HttpServletRequest request, LocaleContext localeContext, RequestAttributes requestAttributes) {

    if (localeContext != null) {
        LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
    }
    if (requestAttributes != null) {
        RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
    }
}

小小的總結: 
    整個processRequest方法的前三部分, 就是構建LocaleContext、ServletRequestAttributes, 而後將他們保
    存到ThreadLocal中, 這樣咱們在整個請求的任何地方都能獲取到這兩個對象了, 舉個例子:
    HttpServletRequest request = ( (ServletRequestAttributes)RequestContextHolder
                                                .getRequestAttributes() ).getRequest();

    這樣咱們就能在請求的任意地方獲取HttpServletRequest對象了
複製代碼
  • resetContextHolders&publishRequestHandledEvent
在processRequest方法的finally語句中, 調用了這兩個方法, resetContextHolders方法的調用就是由於此時請求
結束了, 須要將當前線程上下文ThreaLocal中的LocaleContext以及ServletRequestAttributes恢復到請求以前的
狀態, 而publishRequestHandledEvent方法就是發佈了一個事件而已, 你們有興趣看下, 這個對咱們分析執行流程
沒多大意義, 就不進行分析了
複製代碼
  • doService方法
一共六個部分, 前面已經分析了五個部分, 代碼都比較簡單, 而第四部分doService方法纔是真正用來處理請求的, 這
個方法裏面作的事情對執行流程的分析也沒多大意義, 咱們簡單的過一下, 最後着重來說該方法中觸發執行流程處理的
核心方法, 在這個方法中, 先是對request attributes中知足必定規則的key-value進行了保存, 最後在finally中
進行了restore, 這裏咱們不用去理會, 若是你們感興趣能夠研究下是幹嗎的, 筆者也沒去研究, 以後往request的
Attribute中添加了一些key-value, 將WebApplicationContext、localeResolver、themeResolver、
ThemeSource放到了請求域中, 而後是對flashMapManager的處理, 這個涉及到重定向相關的應用, 你們有興趣瞭解
一下, 由於重定向的時候是多個不一樣的請求, 請求域是不能共享的, 那麼若是將一些數據從A請求帶到重定向後的B請求
又不借助session呢, SpringMVC提供了一個FlashMap組件專門完成這樣的功能, 你們有興趣能夠了解下, 因爲用的
很少就不進行展開了, 最後, 調用了doDispatch方法開始真正的處理請求!!!同時也是處理請求中最爲複雜的部分!!
複製代碼

doDispatch處理請求的核心

  • 大體的流程分析
在前面的文章中, 咱們對HandlerMapping和HandlerAdapter進行了詳細的分析, 由此能夠知道, 平常工做中使用的
最多的@RequestMapping註解來表示一個請求的狀況下, @RequestMapping註解自己由RequestMappingInfo這個類
對象來表示, 而被該註解標註的方法則是用HandlerMethod來表示的, 在AbstractHandlerMethodMapping中有一個
MappingRegistry對象, url到RequestMappingInfo, RequestMappingInfo到HandlerMethod的映射都是在這個對
象中存儲的, 這也是HandlerMapping的做用, 咱們能經過url找到對應的HandlerMethod

上面描述中處理請求的HandlerMethod在SpringMVC中也被稱爲handler, handler的類型是不肯定的, 僅僅說對於
@RequestMapping這樣的狀況下handler的表現形式是HandlerMethod而已, 當咱們不是經過@RequestMapping來完成
映射的時候, handler就不同了, 好比說Controller接口, 注意, 不是@Controller註解, 該接口也能夠被用來處
理請求, 只不過是被配置在SimpleUrlHandlerMapping裏面而已, Controller接口的實現類也被稱爲handler

handler的種類這麼多, 爲了統一調用, 從而引入了適配器模式, 提供一個公共的適配器, 不一樣類型的handler經過實
現該接口的方法的公共方法來實現本身的調用, 從而就有了各類HandlerAdapter, 咱們以前着重講解了
RequestMappingHandlerAdapter, 全部的適配器中, 經過supports方法來判斷該適配器是否可以處理當前的
handler, 若是能, 則調用handle方法來完成調用

以上的內容在前面幾篇文章中筆者已經詳細的講解了其中的細節, 而doDispatch處理請求很簡單, 經過當前的url以及
遍歷全部的HandlerMapping, 若是在一個HandlerMapping中可以經過url找到對應的url, 則返回其中存儲的Handler
對於RequestMappingHandlerMapping來講, 返回的就是一個HandlerMethod, 而對於SimpleUrlHandlerMapping來
說, 可能返回的就是Controller的實現類, 注意, 這裏是可能....由於還可能有其餘類型的handler也是存儲在這個
HandlerMapping的

在SpringMVC中, 利用handler處理請求的時候, 同時提供了攔截器, 即HandlerInterceptor, 攔截器有三個方法,
分別能夠在handler被調用前, 調用後, 調用完成的同時被調用, 僞代碼以下:
    try {
        interceptor.preHandle();
        handler.handle();
        interceptor.postHandle();
    } catch (Exception) {}
    finally {
        interceptor.afterCompletion();
    }

當咱們在HandlerMapping中找到了對應的handler後, 就會將handler和HandlerInterceptor封裝爲
HandlerExecutionChain對象, 從而方便直接獲取兩個對象, 再日後, 遍歷全部的HandlerAdapter, 調用其
supports方法對該handler進行驗證, 若是找到了對應的Adapter則返回, 以後調用這個Adapter的handle方法完成對
請求的處理, 該方法返回一個ModelAndView對象, 當咱們返回一個字符串表示jsp文件的時候, 這個jsp的路徑就被存
儲在了這個ModelAndView中, 其實一開始是存儲在以前咱們分析的ModelAndViewContainer中的, 只不過最後將其放
在了ModelAndView返回了而已, 最後利用ModelAndView進行視圖的渲染, 大體的流程就是這樣, 接下來咱們開始對
源碼進行分析
複製代碼
  • doDispatch源碼(省略了一些異常處理以及異步請求的內容)
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        processedRequest = checkMultipart(request);
        multipartRequestParsed = (processedRequest != request);

        mappedHandler = getHandler(processedRequest);
        if (mappedHandler == null) {
            noHandlerFound(processedRequest, response);
            return;
        }

        HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

        if (!mappedHandler.applyPreHandle(processedRequest, response)) {
            return;
        }

        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

        applyDefaultViewName(processedRequest, mv);
        mappedHandler.applyPostHandle(processedRequest, response, mv);
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    } catch (Throwable err) {
        triggerAfterCompletion(processedRequest, response, mappedHandler,
                new NestedServletException("Handler processing failed", err));
    }
}

縱觀整個精簡後的doDispatch方法, 結構很是的清晰, processedRequest表示真正的請求對象, mappedHandler就
是以前咱們分析的, 將handler和HandlerInterceptor封裝起來的對象, multipartRequestParsed表示是否對文件
上傳這樣的功能進行了解析, 由於SpringMVC是有提供文件上傳功能的, 因此不能直接用HttpServletRequest來表示
若是是文件上傳, 那麼還要進行一次請求的解析, 纔可以獲得最終的請求對象processedRequest

mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
    noHandlerFound(processedRequest, response);
    return;
}

這段代碼就是找到對應的handler, 將handler和HandlerIntercepter進行合併, 變成一個HandlerExecutionChain
對象, 若是經過url沒有找到handler, 就執行if語句塊的內容, 即拋出一個異常

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

mappedHandler.getHandler()獲取對應的handler, 利用這個handler來遍歷全部的HandlerAdapter, 找到合適的
HandlerAdapter並返回

if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; }
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);

遍歷HandlerExecutionChain中的全部攔截器, 調用其preHandle方法, 若是返回true, 那麼就繼續執行, 返回
false則就不執行了, 調用HandlerAdapter的handle方法, 若是是@RequestMapping的狀況, 則調用的是
AbstractHandlerMethodAdapter的hanlde方法, 返回一個ModelAndView對象, 這個方法的調用咱們在上一篇文章已
經詳細的講解了, 這裏就再也不進行展開了, 最後調用HandlerExecutionChain中全部攔截器的postHandle方法, 
applyDefaultViewName是由於當咱們返回的ModelAndView中沒有View的時候, 好比咱們@RequestMapping標註的方
法返回的是void或者有被@ResponseBody標註的時候, 就是沒有視圖的, 此時會賦予一個默認的視圖, 裏面的代碼很
簡單, 你們有興趣能夠看下

在上面doDispatch的代碼中, 咱們能夠看到在異常捕獲後調用了triggerAfterCompletion方法, 裏面其實就是對
HandlerExecutionChain中全部攔截器的afterCompletion方法的調用, 代碼也很簡單
複製代碼
  • processDispatchResult處理視圖
上面的分析中, 已經將doDispatch分析完了, 有了前面文章的鋪墊, 對這個方法的分析簡直是太簡單了, 咱們還剩下
一個事情沒有作, 假設返回了視圖, 怎麼對這個視圖進行處理呢?好比:
    @RequestMapping( "/test" )
    public String test () {
        return "index"
    }

下面咱們直接利用註釋對processDispatchResult方法進行描述, 由於比較簡單, 就不採用以前的方式進行分析了:

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) {
    boolean errorView = false;

    /** * 在doDispatch中, 會將異常進行捕獲, 放入到一個Exception對象中, 對視圖進行解析的時候會傳進來 * 若是存在異常, 那麼就對異常進行處理, 好比說返回一個異常視圖(若是配置了異常視圖的話) */
    if (exception != null) {
        if (exception instanceof ModelAndViewDefiningException) {
            mv = ((ModelAndViewDefiningException) exception).getModelAndView();
        }
        else {
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
            mv = processHandlerException(request, response, handler, exception);
            errorView = (mv != null);
        }
    }

    // 若是視圖不是空的, 就開始渲染視圖到前端頁面
    if (mv != null && !mv.wasCleared()) {
        render(mv, request, response);
    }

    // 調用全部攔截器的afterCompletion方法
    if (mappedHandler != null) {
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
}

render方法就不進行分析了, 你們有興趣看下, 其實就是利用ViewResolver解析viewName, 獲取到一個視圖View對
象, 而後對http的狀態進行一下設置, 最後調用視圖對象View的render方法完成渲染, 對於JSP文件來講, 其實就是
forward到對應的jsp文件而已
複製代碼
相關文章
相關標籤/搜索