前面兩篇文章直接對SpringMVC裏面的組件進行了源碼分析,可能不少小夥伴都會以爲有點摸不着頭腦。因此今天再岔回來講一說SpringMVC的核心控制器,以此爲軸心來學習整個SpringMVC的知識體系。前端
SpringMVC在項目中如何使用的?
前面在《項目開發框架-SSM》一篇文章中已經詳細的介紹過了SSM項目中關於Spring的一些配置文件,對於一個Spring應用,必不可少的是:java
<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的,固然不只僅這兩個,還有其餘幾個重要的子類,後續會持續更新):ios
<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 的方式來訪問咱們的應用了。web
俗話說知其然,之氣因此然;那麼爲何在配置好相關的配置文件以後,咱們就能訪問咱們的SSM項目了呢?從發送一條那樣的請求(http://localhost:80/myproject/index.do)展現出最後的界面,這個過程在,Spring幫咱們作了哪些事情呢?(SpringIOC容器的初始化在《Spring技術內幕-容器刷新:wac.refresh》文中已經大概的說了下你們能夠參考一下)spring
SpringMVC處理請求的過程
先經過下面這張圖來整個瞭解下SpringMVC請求處理的過程;圖中從1-13,大致上描述了請求從發送到界面展現的這樣一個過程。 瀏覽器
從上面這張圖中,咱們能夠很明顯的看到有一個DispatcherServlet這樣一個類,處於各個請求處理過程當中的分發站。實際上,在SpringMVC中,整個處理過程的頂層設計都在這裏面。一般咱們將DispatcherServlet稱爲SpringMVC的前端控制器,它是SpringMVC中最核心的類。下面咱們就來揭開DispatcherServlet的面紗吧!spring-mvc
DispatcherServlet
OK,咱們直接來看DispatcherServlet的類定義:網絡
public class DispatcherServlet extends FrameworkServlet
DispatcherServlet繼承自FrameworkServlet,就這樣? session
下面纔是他家的族譜:mvc
首先爲何要有綠色的部門,有的同窗可能已經想到了,綠色部分不是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轉發時參數的傳遞;
就拿表單重複提交這個問題來講,一種方案就是:在處理完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處理過程流程圖
那上面就是整個DispatcherServlet的一個大概內容了,關於SpringMVC容器的初始化,咱們在先把DispatcherServlet中涉及到的九大組件擼完以後再回頭來學習。關於九大組件目前已經有過兩篇是關於HandlerMapping的了,因爲咱們打算對於整個SpringMVC體系結構都進行一次梳理,所以,會將九大組件從接口設計以及子類都會經過源碼的方式來呈現。
SpringMVC源碼系列:AbstractHandlerMapping
你們若是有什麼意見或者建議能夠在下方評論區留言,也能夠給咱們發郵件(glmapper_2018@163.com)!歡迎小夥伴與咱們一塊兒交流,一塊兒成長。