SpringMVC系列原始碼:DispatcherServlet

glmapper發表於2018-01-13

轉載請宣告出處:https://juejin.im/post/5a587430518825734859e45d

前面兩篇文章直接對SpringMVC裡面的元件進行了原始碼分析,可能很多小夥伴都會覺得有點摸不著頭腦。所以今天再岔回來說一說SpringMVC的核心控制器,以此為軸心來學習整個SpringMVC的知識體系。

SpringMVC在專案中如何使用的?

前面在《專案開發框架-SSM》一篇文章中已經詳細的介紹過了SSM專案中關於Spring的一些配置檔案,對於一個Spring應用,必不可少的是:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <!-- <param-value>classpath*:config/applicationContext.xml</param-value> -->
    <param-value>classpath:spring/applicationContext.xml</param-value>
</context-param>
<!-- 配置一個監聽器將請求轉發給 Spring框架 -->
<!-- Spring監聽器 -->
<listener>
	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
複製程式碼

通過ContextLoadListener來完成Spring容器的初始化以及Bean的裝載《Spring技術內幕學習:Spring的啟動過程》。那麼如果在我們需要提供WEB功能,則還需要另外一個,那就是SpringMVC,當然我們同樣需要一個用來初始化SpringMVC的配置(初始化9大元件的過程:前面兩篇《SpringMVC原始碼系列:HandlerMapping》和《SpringMVC原始碼系列:AbstractHandlerMapping》是關於HnadlerMapping的,當然不僅僅這兩個,還有其他幾個重要的子類,後續會持續更新):

<servlet>
	<servlet-name>mvc-dispatcher</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<!-- 配置springMVC需要載入的配置檔案 spring-dao.xml,spring-service.xml,spring-web.xml 
		Mybatis(如果有) - > spring -> springmvc -->
	<init-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:spring/spring-mvc.xml</param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
	<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
	<servlet-name>mvc-dispatcher</servlet-name>
	<!-- 預設匹配所有的請求 -->
	<url-pattern>*.htm</url-pattern>
</servlet-mapping>
複製程式碼

當我們在web.xml中配置好上述內容(當然還得保證我們們的Spring的配置以及SpringMVC的配置檔案沒有問題的情況下),啟動web容器(如jetty),就可以通過在瀏覽器輸入諸如:http://localhost:80/myproject/index.do 的方式來訪問我們的應用了。

俗話說知其然,之氣所以然;那麼為什麼在配置好相關的配置檔案之後,我們就能訪問我們的SSM專案了呢?從傳送一條那樣的請求(http://localhost:80/myproject/index.do)展示出最後的介面,這個過程在,Spring幫我們做了哪些事情呢?(SpringIOC容器的初始化在《Spring技術內幕-容器重新整理:wac.refresh》文中已經大概的說了下大家可以參考一下)

SpringMVC處理請求的過程

先通過下面這張圖來整個瞭解下SpringMVC請求處理的過程;圖中從1-13,大體上描述了請求從傳送到介面展示的這樣一個過程。

SpringMVC系列原始碼:DispatcherServlet
從上面這張圖中,我們可以很明顯的看到有一個DispatcherServlet這樣一個類,處於各個請求處理過程中的分發站。實際上,在SpringMVC中,整個處理過程的頂層設計都在這裡面。通常我們將DispatcherServlet稱為SpringMVC的前端控制器,它是SpringMVC中最核心的類。下面我們就來揭開DispatcherServlet的面紗吧!

DispatcherServlet

OK,我們直接來看DispatcherServlet的類定義:

public class DispatcherServlet extends FrameworkServlet 
複製程式碼

DispatcherServlet繼承自FrameworkServlet,就這樣?

SpringMVC系列原始碼:DispatcherServlet
下面才是他家的族譜:

SpringMVC系列原始碼:DispatcherServlet

首先為什麼要有綠色的部門,有的同學可能已經想到了,綠色部分不是Spring的,而是java自己的;Spring通過HttpServletBean這位年輕人成功的擁有了JAVA WEB 血統(本來Spring就是用JAVA寫的,哈哈)。關於Servlet這個小夥伴可以看下我之前的文章,有簡單的介紹了這個介面。

話說回來,既然DispatcherServlet歸根揭底是一個Servlet,那麼就肯定具有Servlet功能行為。

敲黑板!!!Servlet的生命週期是啥(init->service->destroy : 載入->例項化->服務->銷燬)。

其實這裡我想說的就是service這個方法,當然,在DispatcherServlet中並沒有service方法,但是它有一個doService方法!(引的好難...)

doService是DispatcherServlet的入口,我們來看下這個方法:

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    if (logger.isDebugEnabled()) {
    	String resumed = WebAsyncUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : "";
    	logger.debug("DispatcherServlet with name '" + getServletName() + "'" + resumed +
    			" processing " + request.getMethod() + " request for [" + getRequestUri(request) + "]");
    }
    
    // 在include的情況下保留請求屬性的快照,以便能夠在include之後恢復原始屬性。
    Map<String, Object> attributesSnapshot = null;
    //確定給定的請求是否是包含請求,即不是從外部進入的頂級HTTP請求。
    //檢查是否存在“javax.servlet.include.request_uri”請求屬性。 可以檢查只包含請求中的任何請求屬性。
    //(可以看下面關於isIncludeRequest解釋)
    if (WebUtils.isIncludeRequest(request)) {
    	attributesSnapshot = new HashMap<String, Object>();
    	Enumeration<?> attrNames = request.getAttributeNames();
    	while (attrNames.hasMoreElements()) {
    		String attrName = (String) attrNames.nextElement();
    		if (this.cleanupAfterInclude || attrName.startsWith("org.springframework.web.servlet")) {
    			attributesSnapshot.put(attrName, request.getAttribute(attrName));
    		}
    	}
    }
    
    // 使框架可用於handler和view物件。
    request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
    request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
    request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
    request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
    //FlashMap用於儲存轉發請求的引數的
    FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
    if (inputFlashMap != null) {
    	request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
    }
    request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
    request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
    
    try {
    	doDispatch(request, response);
    }
    finally {
    	if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
    		// Restore the original attribute snapshot, in case of an include.
    		if (attributesSnapshot != null) {
    			restoreAttributesAfterInclude(request, attributesSnapshot);
    		}
    	}
    }
}
複製程式碼

PS:“javax.servlet.include.request_uri”是INCLUDE_REQUEST_URI_ATTRIBUTE常量的值。isIncludeRequest(request)方法的作用我們可以藉助一條JSP的指令來理解:

<jsp:incluede page="index.jsp"/>
複製程式碼

這條指令是指在一個頁面中巢狀了另一個頁面,那麼我們知道JSP在執行期間是會被編譯成相應的Servlet類來執行的,所以在Servlet中也會有類似的功能和呼叫語法,這就是RequestDispatch.include()方法。 那麼在一個被別的servlet使用RequestDispatcher的include方法呼叫過的servlet中,如果它想知道那個呼叫它的servlet的上下文資訊該怎麼辦呢,那就可以通過request中的attribute中的如下屬性獲取:

javax.servlet.include.request_uri
javax.servlet.include.context_path
javax.servlet.include.servlet_path
javax.servlet.include.path_info
javax.servlet.include.query_string
複製程式碼

在doService中,下面的try塊中可以看到:

try {
    doDispatch(request, response);
}
複製程式碼

doService並沒有直接進行處理,二是將請求交給了doDispatch進行具體的處理。當然在呼叫doDispatch之前,doService也是做了一些事情的,比如說判斷請求是不是inclde請求,設定一些request屬性等。

FlashMap支撐的Redirect引數傳遞問題

在doService中除了webApplicationContext、localeResolver、themeResolve和themeSource四個提供給handler和view使用的四個引數外,後面的三個都是和FlashMap有關的,程式碼如下:

//FlashMap用於儲存轉發請求的引數的
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
	request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
複製程式碼

註釋中提到,FlashMap主要用於Redirect轉發時引數的傳遞;

SpringMVC系列原始碼:DispatcherServlet
就拿表單重複提交這個問題來說,一種方案就是:在處理完post請求之後,然後Redirect到一個get的請求,這樣即使使用者重新整理也不會有重複提交的問題。但是問題在於,前面的post請求時提交訂單,提交完後redirect到一個顯示訂單的頁面,顯然在顯示訂單的頁面我們需要知道訂單的資訊,但是redirect本身是沒有引數傳遞功能的,按照普通的模式如果想傳遞引數,就只能將引數拼接在url中,但是url在get請求下又是有長度限制的;另外,對於一些場景下,我們也不希望自己的引數暴露在url中。

對於上述問題,我們就可以用FlashMap來進行引數傳遞了;我們需要在redirect之前將需要的引數寫入OUTPUT_FLASH_MAP_ATTRIBUTE,例如:

ServletRequestAttributes SRAttributes = (ServletRequestAttributes)(RequestContextHolder.getRequestAttributes());
HttpServletRequest req = SRAttributes.getRequest();
FlashMap flashMap = (FlashMap)(req.getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE));
flashMap.put("myname","glmapper_2018");
複製程式碼

這樣在redirect之後的handler中spring就會自動將其設定到model裡面。但是如果僅僅是這樣,每次redirect時都寫上面那樣一段程式碼是不是又顯得很雞肋呢?當然,spring也為我們提供了更加方便的用法,即在我們的handler方法的引數中使用RedirectAttributes型別變數即可(前段時間用到這個,本來是想單獨寫一篇關於引數傳遞問題的,藉此機會就省略一篇吧,吼吼...),來看一段程式碼:

@RequestMapping("/detail/{productId}")
public ModelAndView detail(HttpServletRequest request,HttpServletResponse 
    response,RedirectAttributes attributes, @PathVariable String productId) {
    if (StringUtils.isNotBlank(productId)) {
	logger.info("[產品詳情]:detail = {}",JSONObject.toJSONString(map));
	mv.addObject("detail",JSONObject.toJSONString(getDetail(productId)));
	mv.addObject("title", "詳情");
	mv.setViewName("detail.ftl");
}
//如果沒有獲取到productId
else{
	attributes.addFlashAttribute("msg", "產品不存在");
	attributes.addFlashAttribute("productName", productName);
	attributes.addFlashAttribute("title", "有點問題!");
	mv.setViewName("redirect:"/error/fail.htm");
}
return mv;
}
複製程式碼

這段程式碼時我前段時間做全域性錯誤處理模組時對原有業務邏輯錯誤返回的一個抽象,因為要將錯誤統一處理,就不可能在具體的handler中直接返回到錯誤介面,所以就將所有的錯誤處理都redirect到error/fail.htm這個handler method中處理。redirect的引數問題上面已經描述過了,這裡就不在細說,就是簡單的例子和背景,知道怎麼去使用RedirectAttributes。

RedirectAttributes這個原理也很簡單,就是相當於存在了一個session中,但是這個session在用過一次之後就銷燬了,即在fail.htm這個方法中獲取之後如果再進行redirect,引數還會丟失,那麼就在fail.htm中繼續使用RedirectAttributes來儲存引數再傳遞到下一個handler。

doDispatch方法

為了偷懶,上面強行插入了對Spring中redirect引數傳遞問題的解釋。迴歸到我們們的doDispatch方法。

作用:處理實際的排程到handler。handler將通過按順序應用servlet的HandlerMappings來獲得。 HandlerAdapter將通過查詢servlet已安裝的HandlerAdapter來查詢支援處理程式類的第一個HandlerAdapter。所有的HTTP方法都由這個方法處理。這取決於HandlerAdapter或處理程式自己決定哪些方法是可以接受的。

其實在doDispatch中最核心的程式碼就4行,我們來看下:

  • 根據request找到我們的handler
// Determine handler for the current request.
   mappedHandler = getHandler(processedRequest);
複製程式碼
  • 根據handler找到對應的HandlerAdapter
// Determine handler adapter for the current request.
   HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
複製程式碼
  • HandlerAdapter處理handler
// Actually invoke the handler.
   mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
複製程式碼
  • 呼叫processDispatchResult方法處理上述過程中得結果綜合,當然也包括找到view並且渲染輸出給使用者
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
複製程式碼

我們以上述為軸心,來看下它的整個原始碼(具體程式碼含義在程式碼中標註):

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    //當前請求request
    HttpServletRequest processedRequest = request;
    //處理器鏈(handler和攔截器)
    HandlerExecutionChain mappedHandler = null;
    //使用者標識multipartRequest(檔案上傳請求)
    boolean multipartRequestParsed = false;
    
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    
    try {
        //很熟悉吧,這個就是我們返回給使用者的包裝檢視
    	ModelAndView mv = null;
    	//處理請求過程中丟擲的異常。這個異常是不包括渲染過程中丟擲的異常的
    	Exception dispatchException = null;
    
    	try {
    	    //檢查是不是上傳請求
    		processedRequest = checkMultipart(request);
    		multipartRequestParsed = (processedRequest != request);
    
    		// 通過當前請求確定相應的handler
    		mappedHandler = getHandler(processedRequest);
    		//如果沒有找到:就會報異常,這個異常我們在搭建SpringMVC應用時會經常遇到:
    		//No mapping found for HTTP request with URI XXX in
    		//DispatcherServlet with name XXX
    		if (mappedHandler == null || mappedHandler.getHandler() == null) {
    			noHandlerFound(processedRequest, response);
    			return;
    		}
    
    		// 根據handler找到HandlerAdapter
    		HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    
    		//處理GET和Head請求的Last-Modified
    		
    		//獲取請求方法
    		String method = request.getMethod();
    		//這個方法是不是GET方法
    		boolean isGet = "GET".equals(method);
    		if (isGet || "HEAD".equals(method)) {
    			long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
    			if (logger.isDebugEnabled()) {
    				logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
    			}
    			if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
    				return;
    			}
    		}
            //這裡就是我們SpringMVC攔截器的preHandle方法的處理
    		if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    			return;
    		}
            
    		// 呼叫具體的Handler,並且返回我們的mv物件.
    		mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
            //如果需要非同步處理的話就直接返回
    		if (asyncManager.isConcurrentHandlingStarted()) {
    			return;
    		}
            //這個其實就是處理檢視(view)為空的情況,會根據request設定預設的view
    		applyDefaultViewName(processedRequest, mv);
    		//這裡就是我們SpringMVC攔截器的postHandle方法的處理
    		mappedHandler.applyPostHandle(processedRequest, response, mv);
    	}
    	catch (Exception ex) {
    		dispatchException = ex;
    	}
    	catch (Throwable err) {
    		// As of 4.3, we're processing Errors thrown from handler methods as well,
    		// making them available for @ExceptionHandler methods and other scenarios.
    		dispatchException = new NestedServletException("Handler dispatch failed", err);
    	}
    	//處理返回結果;(異常處理、頁面渲染、攔截器的afterCompletion觸發等)
    	processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
    	triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    catch (Throwable err) {
    	triggerAfterCompletion(processedRequest, response, mappedHandler,
    			new NestedServletException("Handler processing failed", err));
    }
    finally {
        //判斷是否執行非同步請求
    	if (asyncManager.isConcurrentHandlingStarted()) {
    		// 如果是的話,就替代攔截器的postHandle 和 afterCompletion方法執行
    		if (mappedHandler != null) {
    			mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
    		}
    	}
    	else {
    		// 刪除上傳請求的資源
    		if (multipartRequestParsed) {
    			cleanupMultipart(processedRequest);
    		}
    	}
    }
}
複製程式碼

整體來看,doDispatch做了兩件事情:

  • 處理請求
  • 頁面渲染

doDispatch處理過程流程圖

SpringMVC系列原始碼:DispatcherServlet

那上面就是整個DispatcherServlet的一個大概內容了,關於SpringMVC容器的初始化,我們在先把DispatcherServlet中涉及到的九大元件擼完之後再回頭來學習。關於九大元件目前已經有過兩篇是關於HandlerMapping的了,由於我們打算對於整個SpringMVC體系結構都進行一次梳理,因此,會將九大元件從介面設計以及子類都會通過原始碼的方式來呈現。

SpringMVC原始碼系列:HandlerMapping

SpringMVC原始碼系列:AbstractHandlerMapping

大家如果有什麼意見或者建議可以在下方評論區留言,也可以給我們發郵件(glmapper_2018@163.com)!歡迎小夥伴與我們一起交流,一起成長。

相關文章