SpringBoot源碼解讀與原理分析(三十七)SpringBoot整合WebMvc(二)DispatcherServlet的工作全流程

文章目錄

    • 前言
    • 12.4 DispatcherServlet的工作全流程
      • 12.4.1 DispatcherServlet#service
      • 12.4.2 processRequest
      • 12.4.3 doService
        • 12.4.3.1 isIncludeRequest的判斷
        • 12.4.3.2 FlashMapManager的設計
      • 12.4.4 doDispatch
        • 12.4.4.1 處理文件上傳請求
        • 12.4.4.2 獲取可用的Handler
          • (1)HandlerMapping.getHandler
          • (2)getHandlerInternal
          • (3)getHandlerExecutionChain
        • 12.4.4.3 獲取HandlerAdapter
        • 12.4.4.4 回調攔截器
        • 12.4.4.5 執行Handler
          • (1)handleInternal
          • (2)invokeHandlerMethod
            • (a)初始化參數綁定器
            • (b)參數預綁定
            • (c)創建方法執行對象
            • (d)執行Controller的方法```invokeAndHandle```
            • (e)包裝ModelAndView
        • 12.4.4.6 再次回調攔截器
        • 12.4.4.7 處理視圖、解析異常
          • (1)處理異常
          • (2)渲染視圖
          • (3)第三次回調攔截器
      • 12.4.5 DispatcherServlet工作流程總結
    • 12.5 小結

前言

WebMvc的核心組件裝配完成之后,DispatcherServlet作為WebMvc的核心前端控制器正式投入工作,默認接收客戶端的所有請求,并調度其它核心組件處理請求,最終響應結果給客戶端。

本節內容研究WebMvc在實際運行期間DispatcherServlet對于請求處理和響應結果的全流程執行原理。本文內容由于不可割裂,因此具有超長預警。

本文沿用 SpringBoot源碼解讀與原理分析(三十六)SpringBoot整合WebMvc(一)@Controller控制器裝配原理 12.1 SpringBoot整合WebMvc案例 中編寫好的示例項目,并進行一些小改動。

  • 新增CustomAdvice類,標注@ControllerAdvice注解,并編寫兩個標注@InitBinder和@ExceptionHandler的方法
@ControllerAdvice
public class CustomAdvice {@InitBinderpublic void customDataBinder(WebDataBinder dataBinder) {// 請求URL中的時間格式為yyyy-MM-dd HH:mm:ss// 自動綁定到Date屬性參數中dataBinder.addCustomFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss"));}@ExceptionHandler({Exception.class})public String customExceptionHandler(Exception ex) {System.out.println("自定義異常發生了," + ex.getMessage());return "自定義異常返回";}}
  • 修改UserController類,新增標注@InitBinder注解的方法,test方法新增一個Date類型的參數
@Controller
@RequestMapping("/user")
public class UserController {@InitBinderpublic void myDataBinder(WebDataBinder dataBinder) {// 如果參數是字符串類型,則去除字符串的前后空格dataBinder.registerCustomEditor(String.class, new StringTrimmerEditor(true));}@RequestMapping(method = RequestMethod.GET, value = "/test")@ResponseBodypublic String test(String name, Date time) {System.out.println("請求參數 name=" + name);System.out.println("請求參數 time=" + time);return name;}
}
  • 新增攔截器CustomInterceptor類
public class CustomInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("攔截器的preHandle方法執行了...");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("攔截器的postHandle方法執行了...");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println("攔截器的afterCompletion方法執行了...");}
}
  • 修改配置類,加載攔截器及配置其路徑
@Configuration
public class PathConfig implements WebMvcConfigurer {@Overridepublic void configurePathMatch(PathMatchConfigurer configurer) {configurer.addPathPrefix("/api", c -> c.isAnnotationPresent(Controller.class));}@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new CustomInterceptor()).addPathPatterns("/api/user/test");}
}

至此,示例項目代碼編寫完成,執行主啟動類的main方法啟動項目。使用瀏覽器訪問 http://127.0.0.1:8080/api/user/test?name=aaa&time=2024-02-29 12:12:12,控制臺打印結果如下:

攔截器的preHandle方法執行了...
請求參數 name=aaa
請求參數 time=Thu Feb 29 12:12:12 CST 2024
攔截器的postHandle方法執行了...
攔截器的afterCompletion方法執行了...

12.4 DispatcherServlet的工作全流程

啟動示例項目后,在DispatcherServlet的父類FrameworkServlet的service方法上打入斷點,隨后使用瀏覽器發起請求,待程序停在斷點處,開始Debug調試。

12.4.1 DispatcherServlet#service

源碼1FrameworkServlet.javaprotected void service(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());if (httpMethod == HttpMethod.PATCH || httpMethod == null) {// 對PATCH類型的請求單獨處理processRequest(request, response);} else {// 調用父類的HttpServlet的service方法super.service(request, response);}
}@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);
}

由 源碼1 可知,service方法會對PATCH類型的請求單獨處理,但通常在項目開發中不會使用PATCH類型

繼續向下執行else塊的super.service方法,FrameworkServlet的父類HttpServlet會根據不同的請求類型將方法轉發至doXxx方法中,所以最終執行的是FrameworkServlet中重寫的doGetdoPostdoPutdoDelete等方法,而這些方法最終都會調用processRequest方法。

12.4.2 processRequest

源碼2FrameworkServlet.javaprotected final void processRequest(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {// 記錄接收請求的時間long startTime = System.currentTimeMillis();Throwable failureCause = null;// 獲取當前線程的LocaleContextLocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();// 創建當前線程的LocaleContextLocaleContext localeContext = buildLocaleContext(request);// 獲取當前線程的RequestAttributesRequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();// 創建當前線程的RequestAttributesServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());// 初始化ContextHolder,傳入新封裝好LocaleContext和RequestAttributesinitContextHolders(request, localeContext, requestAttributes);try {// 真正處理請求的方法,但這是子類的模板方法doService(request, response);} // catch ...finally {// 重新設置當前線程的LocaleContext和RequestAttributesresetContextHolders(request, previousLocaleContext, previousAttributes);if (requestAttributes != null) {requestAttributes.requestCompleted();}logResult(request, response, failureCause, asyncManager);// 發布ServletRequestHandledEvent事件publishRequestHandledEvent(request, response, startTime, failureCause);}
}private void initContextHolders(HttpServletRequest request,@Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {// 將全新的LocaleContext和RequestAttributes設置到當前線程中if (localeContext != null) {LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);}if (requestAttributes != null) {RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);}
}private void resetContextHolders(HttpServletRequest request,@Nullable LocaleContext prevLocaleContext, @Nullable RequestAttributes previousAttributes) {// 將預先保存的LocaleContext和RequestAttributes設置回線程中LocaleContextHolder.setLocaleContext(prevLocaleContext, this.threadContextInheritable);RequestContextHolder.setRequestAttributes(previousAttributes, this.threadContextInheritable);
}

由 源碼2 可知,processRequest方法會對請求做一些前置處理,再轉調doService方法真正處理請求。

在該方法的前置處理中,做了線程之間的隔離。首先獲取當前線程的LocaleContext和RequestAttributes并暫存在方法中,隨后創建全新的LocaleContext和RequestAttributes,調用initContextHolders方法設置到當前線程中,以此完成線程之間的隔離。

待請求完成處理后,在finally代碼塊中,調用resetContextHolders方法將預先保存好的LocaleContext和RequestAttributes設置回線程中,以恢復原來的線程。

12.4.3 doService

源碼3DispatcherServlet.javaprotected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {logRequest(request);Map<String, Object> attributesSnapshot = null;// 判斷請求是否由<jsp:include>標簽而來if (WebUtils.isIncludeRequest(request)) {attributesSnapshot = new HashMap<>();Enumeration<?> attrNames = request.getAttributeNames();while (attrNames.hasMoreElements()) {String attrName = (String) attrNames.nextElement();if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {attributesSnapshot.put(attrName, request.getAttribute(attrName));}}}// ......if (this.flashMapManager != null) {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 ...
}

由 源碼3 可知,doService方法也是在進行一些前置處理之后,轉調doDispatch方法真正處理請求。

12.4.3.1 isIncludeRequest的判斷
源碼4WebUtils.javapublic static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri";
public static boolean isIncludeRequest(ServletRequest request) {// 判斷當前請求中是否包含名為“javax.servlet.include.request_uri”的屬性return (request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE) != null);
}

由 源碼4 可知,doService方法的第一個if判斷結構會調用WebUtils.isIncludeRequest(request)方法。該方法會判斷當前請求中是否包含名為“javax.servlet.include.request_uri”的屬性。

在JSP中,使用 <jsp:incluedepage=“xxx.jsp”> 標簽可以組合其他JSP頁面,那么這個被組合的JSP頁面的加載請求就會帶上“javax.servlet.include.request_uri”屬性。因此,isIncludeRequest方法的作用是區別頁面的加載是否由<jsp:include>標簽而來。

12.4.3.2 FlashMapManager的設計

在用戶登錄的業務場景中,如果是前后端不分離的情況,通常是使用POST請求將用戶名、密碼等信息傳入后端以供認證,認證成功后使用重定向將客戶端引導至系統主頁。

在這個前提下有一個特殊的場景:如果用于登錄時提交的登錄表單中,有一些需要在跳轉至主頁時渲染的數據,則僅放入request域中無法解決問題。

Spring引入FlashMapManager來解決這個問題,可以在頁面重定向發生跳轉時,將需要渲染的數據暫時放入session中,這樣瀏覽器即便刷新也不會影響數據渲染。

12.4.4 doDispatch

該方法是處理請求和響應的核心方法,由于篇幅很長,下面拆解來看。

12.4.4.1 處理文件上傳請求
源碼5DispatcherServlet.javaprotected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {ModelAndView mv = null;Exception dispatchException = null;try {// 處理文件上傳請求processedRequest = checkMultipart(request);multipartRequestParsed = (processedRequest != request);// ......
}protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {if (request.getDispatcherType().equals(DispatcherType.REQUEST)) {// logger ...}} else if (hasMultipartException(request)) {// logger ...} else {try {return this.multipartResolver.resolveMultipart(request);} // catch ...}}return request;
}
源碼6StandardServletMultipartResolver.java@Override
public boolean isMultipart(HttpServletRequest request) {return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
}

由 源碼5 可知,doDispatch方法的第一部分邏輯是處理帶有文件上傳的請求,其核心組件是MultipartResolver。首先判斷請求是否是multipart請求,如果是,則將本由Servlet容器處理的HttpServletRequest對象轉換為可以訪問請求中的文件對象的MultipartHttpServletRequest子接口對象。

由 源碼6 可知,判斷請求是否是multipart請求的方法是判斷請求頭content-type是否以"multipart/"開頭。

由于當前Debug的請求只是一個普通的GET請求,所以不會進入該部分代碼。

12.4.4.2 獲取可用的Handler
源碼7DispatcherServlet.javaprotected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {// 處理文件上傳的請求 ...// 獲取可用的HandlermappedHandler = getHandler(processedRequest);if (mappedHandler == null) {noHandlerFound(processedRequest, response);return;}// ......
}protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {if (this.handlerMappings != null) {for (HandlerMapping mapping : this.handlerMappings) {HandlerExecutionChain handler = mapping.getHandler(request);if (handler != null) {return handler;}}}return null;
}

由 源碼7 可知,doDispatch方法的第二部分邏輯是獲取可用的Handler。在getHandler方法中,遍歷所有的HandlerMapping,從中找出可以返回非空HandlerExecutionChain對象的HandlerMapping。

HandlerMapping集合
Debug至getHandler方法的內部,可以發現有5個HandlerMapping可以選擇。由于實例項目編寫的Handler是以@Controller+@RequestMapping注解實現的,因此最終選擇出來的HandlerMapping是與之相匹配的RequestMappingHandlerMapping。

(1)HandlerMapping.getHandler
源碼8AbstractHandlerMapping.javapublic final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {// 留給子類實現的模板方法Object handler = getHandlerInternal(request);// ......// 構建HandlerExecutionChain對象HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);// ......return executionChain;
}

由 源碼8 可知,HandlerMapping的getHandler的方法的核心邏輯有兩步:由子類具體獲取Handler的具體對象;根據Handler對象構建HandlerExecutionChain對象。

(2)getHandlerInternal

Debug進入getHandlerInternal方法,發現程序會進入RequestMappingHandlerMapping的直接父類RequestMappingInfoHandlerMapping中。

源碼9RequestMappingInfoHandlerMapping.java@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);try {return super.getHandlerInternal(request);} finally {ProducesRequestCondition.clearMediaTypesAttribute(request);}
}

由 源碼9 可知,RequestMappingInfoHandlerMapping中的getHandlerInternal也是直接使用super.getHandlerInternal方法調用父類AbstractHandlerMethodMapping中的getHandlerInternal方法。

源碼10AbstractHandlerMethodMapping.java@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {// 獲取本次請求的URI,并設置到request域中String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);request.setAttribute(LOOKUP_PATH, lookupPath);this.mappingRegistry.acquireReadLock();try {// 獲取可以處理當前URI請求的HandlerMethod對象HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);// 創建全新的HandlerMethod對象return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);} finally {this.mappingRegistry.releaseReadLock();}
}

由 源碼10 可知,getHandlerInternal方法首先會借助UrlPathHelper獲取本次請求URI(示例項目是"/api/user/test"),并設置到request域中。隨后調用lookupHandlerMethod方法,獲取可以處理當前URI請求的HandlerMethod對象(實例項目是UserController的test方法)。

請求URI和HandlerMethod對象
注意,獲取到HandlerMethod對象之后,還要調用createWithResolvedBean方法創建一個全新的HandlerMethod對象。為什么還要再次創建呢?

注意觀察上圖,發現HandlerMethod對象中的bean屬性是一個字符串"userController",而不是BeanFactory中真實存在的UserController對象,其他屬性也沒有UserController對象的持有。

源碼11HandlerMethod.javapublic HandlerMethod createWithResolvedBean() {Object handler = this.bean;if (this.bean instanceof String) {Assert.state(this.beanFactory != null, "Cannot resolve bean name without BeanFactory");String beanName = (String) this.bean;// 從BeanFactory中取出bean對象handler = this.beanFactory.getBean(beanName);}// 重新封裝HandlerMethodreturn new HandlerMethod(this, handler);
}

由 源碼11 可知,createWithResolvedBean方法的作用就是從BeanFactory中取出UserController對象并重新封裝。

(3)getHandlerExecutionChain

回到 源碼8 ,getHandlerInternal執行完后獲得HandlerMethod對象,然后將該HandlerMethod對象傳入getHandlerExecutionChain方法,組合跟該Handler相關的攔截器,并封裝為執行鏈HandlerExecutionChain對象。

源碼12AbstractHandlerMapping.javaprotected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {// 構造HandlerExecutionChain對象HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));// 獲取請求路徑String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH);for (HandlerInterceptor interceptor : this.adaptedInterceptors) {if (interceptor instanceof MappedInterceptor) {// 匹配路徑的攔截器處理MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {chain.addInterceptor(mappedInterceptor.getInterceptor());}} else {// 普通攔截器直接添加chain.addInterceptor(interceptor);}}return chain;
}

由 源碼12 可知,getHandlerExecutionChain會根據HandlerInterceptor的類型分別進行處理,最終構造一個HandlerExecutionChain對象。

getHandlerExecutionChain方法
由上圖可知,對示例項目來說,構造的HandlerExecutionChain對象組合了三個攔截器,分別是ConversionServiceExposingInterceptor、ResourceUrlProviderExposingInterceptor,以及示例項目自定義的CustomInterceptor

獲得HandlerMethod對象并組合攔截器封裝成HandlerExecutionChain對象之后,HandlerMapping的工作全部完成,接下來回到DispatcherServlet的doDispatch方法。

12.4.4.3 獲取HandlerAdapter
源碼13DispatcherServlet.javaprotected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {ModelAndView mv = null;Exception dispatchException = null;try {// 處理文件上傳請求// 獲取可用的Handler// 獲取HandlerAdapterHandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());// ......
}protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {if (this.handlerAdapters != null) {for (HandlerAdapter adapter : this.handlerAdapters) {if (adapter.supports(handler)) {return adapter;}}}// throw ex ...
}

由 源碼13 可知,doDispatch方法的第三部分邏輯是獲取可以處理當前請求的HandlerAdapter,匹配規則是通過HandlerAdapter的supports方法。

Debug至getHandlerAdapter方法,發現有4個可選的HandlerAdapter實現類對象:

getHandlerAdapter方法
由上圖可知,示例項目選擇的HandlerAdapter實現類對象是RequestMappingHandlerAdapter。

源碼14AbstractHandlerMethodAdapter.java@Override
public final boolean supports(Object handler) {return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
}
源碼15RequestMappingHandlerAdapter.java@Override
protected boolean supportsInternal(HandlerMethod handlerMethod) {return true;
}

由 源碼14-15 可知,supports方法僅是判斷Handler類是否是HandlerMethod對象,supportsInternal方法默認返回true,此處supports方法一定返回true,因此最終會選擇可選HandlerAdapter對象集合中下標為0的對象,即RequestMappingHandlerAdapter。

12.4.4.4 回調攔截器
源碼16DispatcherServlet.javaprotected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {ModelAndView mv = null;Exception dispatchException = null;try {// 處理文件上傳請求// 獲取可用的Handler// 獲取HandlerAdapter// 回調攔截器if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}// ......
}boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {// 獲取HandlerExecutionChain對象中組合的攔截器HandlerInterceptor[] interceptors = getInterceptors();if (!ObjectUtils.isEmpty(interceptors)) {for (int i = 0; i < interceptors.length; i++) {HandlerInterceptor interceptor = interceptors[i];// 遍歷攔截器,執行preHandle方法if (!interceptor.preHandle(request, response, this.handler)) {triggerAfterCompletion(request, response, null);return false;}this.interceptorIndex = i;}}return true;
}

由 源碼16 可知,doDispatch方法的第四部分邏輯是回調HandlerExecutionChain對象中組合的攔截器,首先會獲取HandlerExecutionChain對象中組合的攔截器,然后遍歷這些攔截器,執行其preHandle方法。

注意,每個攔截器preHandle方法如果有一個返回false,則applyPreHandle方法直接返回false,doDispatch方法直接結束不再執行下面的邏輯。

由 12.4.4.2 節的getHandlerExecutionChain方法分析可知,HandlerExecutionChain對象中組合的攔截器有3個,分別是ConversionServiceExposingInterceptor和ResourceUrlProviderExposingInterceptor,以及示例項目自定義的CustomInterceptor。

源碼17ResourceUrlProviderExposingInterceptor.java@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {try {request.setAttribute(RESOURCE_URL_PROVIDER_ATTR, this.resourceUrlProvider);} // catch ...return true;
}
源碼18ConversionServiceExposingInterceptor.java@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws ServletException, IOException {request.setAttribute(ConversionService.class.getName(), this.conversionService);return true;
}
源碼19CustomInterceptor.javapublic class CustomInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("攔截器的preHandle方法執行了...");return true;}
}

有 源碼17-19 以及CustomInterceptor類可知,四兩個攔截器的preHandle方法一定會返回true,因此applyPreHandle方法返回true,doDispatch方法繼續執行下面的邏輯。

12.4.4.5 執行Handler
源碼20DispatcherServlet.javaprotected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {ModelAndView mv = null;Exception dispatchException = null;try {// 處理文件上傳請求// 獲取可用的Handler// 獲取HandlerAdapter// 回調攔截器// 實際執行Handlermv = ha.handle(processedRequest, response, mappedHandler.getHandler());// ......
}

由 源碼20 可知,doDispatch方法的第五部分邏輯是調用HandlerAdapter的```handle``方法實際執行Handler,返回一個ModelAndView。

借助IDE,可以發現handle方法的實現在DispatcherServlet的父類AbstractHandlerMethodAdapter中,其handle方法又會轉調子類RequestMappingHandlerAdapter的handleInternal方法。

(1)handleInternal
源碼21AbstractHandlerMethodAdapter.java@Override
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {return handleInternal(request, response, (HandlerMethod) handler);
}
源碼22RequestMappingHandlerAdapter.javaprivate boolean synchronizeOnSession = false;
@Override
protected ModelAndView handleInternal(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ModelAndView mav;checkRequest(request);// 同步session的配置,默認為fasleif (this.synchronizeOnSession) {// ......} else {// 執行Handler方法mav = invokeHandlerMethod(request, response, handlerMethod);}// ......return mav;
}

由 源碼21-22 可知,handleInternal方法最核心的邏輯是else代碼塊中的invokeHandlerMethod方法。

(2)invokeHandlerMethod

由于invokeHandlerMethod方法的篇幅很長,下面拆解來看。

(a)初始化參數綁定器

在WebMvc中,有兩個和參數綁定相關的注解:@InitBinder和@ControllerAdvice。

@InitBinder注解可以單獨聲明在一個Controller類中,執行當前Controller類中的方法時,會先執行標注了@InitBinder注解的方法,初始化一些參數綁定器的邏輯。

@ControllerAdvice注解可以配合@InitBinder注解標注的方法,實現全局的參數綁定器預初始化。

源碼23RequestMappingHandlerAdapter.javaprotected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ServletWebRequest webRequest = new ServletWebRequest(request, response);try {// 初始化參數綁定器WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);// ......
}public static final MethodFilter INIT_BINDER_METHODS = method ->AnnotatedElementUtils.hasAnnotation(method, InitBinder.class);
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {Class<?> handlerType = handlerMethod.getBeanType();Set<Method> methods = this.initBinderCache.get(handlerType);if (methods == null) {// 獲取當前Controller中全部標注了@InitBinder注解的方法,并放到initBinderCache緩存中methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);this.initBinderCache.put(handlerType, methods);}List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();// 遍歷標注了@ControllerAdvice注解的類this.initBinderAdviceCache.forEach((controllerAdviceBean, methodSet) -> {if (controllerAdviceBean.isApplicableToBeanType(handlerType)) {Object bean = controllerAdviceBean.resolveBean();// 遍歷這些類中的全部標注了@InitBinder注解的方法,添加到initBinderMethods緩存中for (Method method : methodSet) {initBinderMethods.add(createInitBinderMethod(bean, method));}}});// 遍歷當前Controller中全部標注了@InitBinder注解的方法// 將這些方法添加到initBinderMethods緩存中// 相當于合并了Controller類中和標注了@ControllerAdvice注解的類中的方法for (Method method : methods) {Object bean = handlerMethod.getBean();initBinderMethods.add(createInitBinderMethod(bean, method));}// 創建數據綁定工廠return createDataBinderFactory(initBinderMethods);
}

底層收集和執行@InitBinder注解的方法就是invokeHandlerMethod方法中的getDataBinderFactory方法。

由 源碼23 可知,getDataBinderFactory方法會分別收集Controller類中標注了@InitBinder注解的方法、標注了@ControllerAdvice注解的類中標注了@InitBinder注解的方法,并合并起來共同組成參數綁定器。

示例項目中編寫的兩類綁定器都被收集起來了,如圖:

自定義的參數綁定器
在UserController中定義的參數綁定器是:

@InitBinder
public void myDataBinder(WebDataBinder dataBinder) {// 如果參數是字符串類型,則去除字符串的前后空格dataBinder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
}

如果暫時把@InitBinder注解注釋掉,該綁定器不再生效,執行結果如下:

沒有@InitBinder時有空格
如果添加了把@InitBinder注解注解,該綁定器生效,執行結果如下:

有@InitBinder時無空格

CustomAdvice類中定義的參數綁定器是:

@ControllerAdvice
public class CustomAdvice {@InitBinderpublic void customDataBinder(WebDataBinder dataBinder) {// 請求URL中的時間格式為yyyy-MM-dd HH:mm:ss// 自動綁定到Date屬性參數中dataBinder.addCustomFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss"));}
}

如果時間類型的參數不按照 yyyy-MM-dd HH:mm:ss 的格式傳遞,在瀏覽器訪問:http://127.0.0.1:8080/api/user/test?name=aaa&time=2024年2月29日 12時12分12秒,結果如下:

IllegalArgumentException: Parse attempt failed for value [2024年2月29日 12時12分12秒]

如果時間類型的參數按照 yyyy-MM-dd HH:mm:ss 的格式傳遞,在瀏覽器訪問:http://127.0.0.1:8080/api/user/test?name=aaa&time=2024-02-29 12-12-12,則正常訪問。

通過以上測試,發現自定義的參數綁定器均生效了。

(b)參數預綁定
源碼24RequestMappingHandlerAdapter.javaprotected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ServletWebRequest webRequest = new ServletWebRequest(request, response);try {// 初始化參數綁定器 ...// 參數預綁定ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);// ......
}public static final MethodFilter MODEL_ATTRIBUTE_METHODS = method ->(!AnnotatedElementUtils.hasAnnotation(method, RequestMapping.class) &&AnnotatedElementUtils.hasAnnotation(method, ModelAttribute.class));
private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {SessionAttributesHandler sessionAttrHandler = getSessionAttributesHandler(handlerMethod);Class<?> handlerType = handlerMethod.getBeanType();Set<Method> methods = this.modelAttributeCache.get(handlerType);if (methods == null) {// 獲取當前Controller中全部標注了@ModelAttribute注解且沒有被@RequestMapping注解標注的方法// 并放到modelAttributeCache緩存中methods = MethodIntrospector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS);this.modelAttributeCache.put(handlerType, methods);}List<InvocableHandlerMethod> attrMethods = new ArrayList<>();// 遍歷標注了@ControllerAdvice注解的類this.modelAttributeAdviceCache.forEach((controllerAdviceBean, methodSet) -> {if (controllerAdviceBean.isApplicableToBeanType(handlerType)) {Object bean = controllerAdviceBean.resolveBean();// 遍歷這些類中的全部標注了@ModelAttribute注解的方法,添加到attrMethods緩存中for (Method method : methodSet) {attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));}}});// 遍歷當前Controller中全部標注了@ModelAttribute注解的方法// 將這些方法添加到attrMethods緩存中// 相當于合并了Controller類中和標注了@ControllerAdvice注解的類中的方法for (Method method : methods) {Object bean = handlerMethod.getBean();attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));}return new ModelFactory(attrMethods, binderFactory, sessionAttrHandler);
}

由 源碼24 可知,getModelFactory方法和上一步的getDataBinderFactory方法的邏輯結構幾乎一樣,都是將Controller類中的標注了某個注解的方法,與全局類中標注了某個注解的方法合并起來。

不同的是,getDataBinderFactory方法找的是@InitBinder注解,而getModelFactory方法找的是@ModelAttribute注解;相同的是,全局類都標注@ControllerAdvice注解。

(c)創建方法執行對象
源碼25RequestMappingHandlerAdapter.javaprotected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ServletWebRequest webRequest = new ServletWebRequest(request, response);try {// 初始化參數綁定器 ...// 參數預綁定 ...// 創建方法執行對象ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);// ......
}protected ServletInvocableHandlerMethod createInvocableHandlerMethod(HandlerMethod handlerMethod) {return new ServletInvocableHandlerMethod(handlerMethod);
}

由 源碼25 可知,createInvocableHandlerMethod方法只是將HandlerMethod對象二度封裝為ServletInvocableHandlerMethod對象。

(d)執行Controller的方法invokeAndHandle
源碼26RequestMappingHandlerAdapter.javaprotected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ServletWebRequest webRequest = new ServletWebRequest(request, response);try {// 初始化參數綁定器 ...// 參數預綁定 ...// 創建方法執行對象 ...// 創建ModelAndView的容器ModelAndViewContainer ...(不重要)// 對異步請求的支持 ...(不重要)// 執行Controller的方法invocableMethod.invokeAndHandle(webRequest, mavContainer);if (asyncManager.isConcurrentHandlingStarted()) {return null;}// 包裝ModelAndViewreturn getModelAndView(mavContainer, modelFactory, webRequest);} // finally ...
}

由 源碼26 可知,創建方法執行對象ServletInvocableHandlerMethod后,又創建了ModelAndView的容器ModelAndViewContainer,以及處理了對異步請求的支持,最后調用方法執行對象ServletInvocableHandlerMethod的invokeAndHandle方法執行Handler方法。

由于invokeAndHandle方法的篇幅很長,下面拆解來看。

  • (i)反射執行Controller方法
源碼27ServletInvocableHandlerMethod.javapublic void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {// 反射執行Controller方法Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);// ......
}
源碼28InvocableHandlerMethod.javapublic Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {// 獲取參數值列表Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);// logger ...return doInvoke(args);
}protected Object doInvoke(Object... args) throws Exception {ReflectionUtils.makeAccessible(getBridgedMethod());try {// 利用反射機制執行Handlerreturn getBridgedMethod().invoke(getBean(), args);}// catch ...
}

由 源碼27-28 可知,invokeAndHandle方法調用invokeForRequest方法,會先收集好執行Handler方法所需的參數列表,然后利用反射機制執行目標Handler方法。doInvoke方法執行完畢后,意味著項目中編寫的Controller類的方法已經執行完畢。

  • (ii)處理方法返回值
源碼29ServletInvocableHandlerMethod.javapublic void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {// 反射執行Controller方法Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);// ......// 處理方法返回值try {this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);} // catch ...
}
源碼30HandlerMethodReturnValueHandlerComposite.java@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {// 篩選出可以處理當前Controller返回的HandlerMethodReturnValueHandler對象HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);if (handler == null) {// throw ......}handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {boolean isAsyncValue = isAsyncReturnValue(value, returnType);// 遍歷HandlerMethodReturnValueHandler集合for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {continue;}// 找出支持當前返回值類型的if (handler.supportsReturnType(returnType)) {return handler;}}return null;
}

由 源碼29-30 可知,處理方法返回值的handleReturnValue方法會先篩選出可以處理當前Controller返回的HandlerMethodReturnValueHandler對象,再調用其handleReturnValue方法處理返回值。

篩選HandlerMethodReturnValueHandler對象的selectHandler方法,是利用for循環遍歷HandlerMethodReturnValueHandler對象集合,找出其中支持當前返回值類型的HandlerMethodReturnValueHandler對象。

借助Debug工具,可以發現可選的HandlerMethodReturnValueHandler對象有15個,示例項目選擇的是RequestResponseBodyMethodProcessor。

篩選HandlerMethodReturnValueHandler對象
在示例項目中,UserController的test方法上標注了@ResponseBody注解,因此響應的是JSON數據。處理JSON數據響應的底層實現就是RequestResponseBodyMethodProcessor。

源碼31RequestResponseBodyMethodProcessor.java@Override
public boolean supportsReturnType(MethodParameter returnType) {return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||returnType.hasMethodAnnotation(ResponseBody.class));
}

由 源碼31 可知,RequestResponseBodyMethodProcessor中supportsReturnType方法就是判斷方法所在類或者方法上是否標注@ResponseBody注解。再一次印證RequestResponseBodyMethodProcessor是處理JSON數據響應的底層實現。

  • (iii)處理JSON數據響應

獲取RequestResponseBodyMethodProcessor對象后,調用其handleReturnValue方法正式處理返回值。

源碼32RequestResponseBodyMethodProcessor.java@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest)throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {mavContainer.setRequestHandled(true);ServletServerHttpRequest inputMessage = createInputMessage(webRequest);ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);// 使用JSON序列化的方式將方法返回的數據轉化為文本// 并寫入HttpServletResponse的輸出流中writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}

由 源碼32 可知,handleReturnValue方法會執行writeWithMessageConverters方法,使用JSON序列化的方式將方法返回的數據轉化為文本,并寫入HttpServletResponse的輸出流中。

  • (iiii)處理視圖返回

示例項目中是響應JSON數據,但在實際開發中也有可能是響應視圖。對于返回視圖和返回JSON數據,底層使用的HandlerMethodReturnValueHandler并不相同。

如果一個Controller方法要跳轉視圖,則方法的返回值一定是一個字符串,并且方法和類上都沒有標注@ResponseBody注解,這種情況下底層使用的HandlerMethodReturnValueHandler是ViewNameMethodReturnValueHandler。

源碼33ViewNameMethodReturnValueHandler.java@Override
public boolean supportsReturnType(MethodParameter returnType) {Class<?> paramType = returnType.getParameterType();// 判斷返回值類型是否是CharSequencereturn (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
}@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {if (returnValue instanceof CharSequence) {String viewName = returnValue.toString();// 將String類型的返回值作為邏輯視圖名放入ModelAndView容器中mavContainer.setViewName(viewName);if (isRedirectViewName(viewName)) {mavContainer.setRedirectModelScenario(true);}} else if (returnValue != null) {// should not happen// throw ...}
}

由 源碼33 可知,處理視圖響應的handleReturnValue方法會將String類型的返回值作為邏輯視圖名稱,并放入ModelAndView容器ModelAndViewContainer對象中。

至此,invokeAndHandle方法執行完畢,Controller類中的方法已執行,ModelAndViewContainer中已經封裝了響應視圖名稱,或者將需要響應的數據寫入了HttpServletResponse中。

(e)包裝ModelAndView
源碼34RequestMappingHandlerAdapter.javaprotected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ServletWebRequest webRequest = new ServletWebRequest(request, response);try {// 初始化參數綁定器 ...// 參數預綁定 ...// 創建方法執行對象 ...// 創建ModelAndView的容器ModelAndViewContainer ...(不重要)// 對異步請求的支持 ...(不重要)// 執行Controller的方法 ...// 包裝ModelAndViewreturn getModelAndView(mavContainer, modelFactory, webRequest);} // finally ...
}private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {// ......// 取出容器中的ModelMapModelMap model = mavContainer.getModel();// 取出視圖名稱,構造ModelAndViewModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());// ......return mav;
}

由 源碼34 可知,invokeHandlerMethod方法的最后一步是調用getModelAndView方法包裝ModelAndView對象。在該方法在中會從上一步封裝的ModelAndViewContainer對象中取出ModelMap和視圖名稱,構造成ModelAndView對象并返回。

簡言之,getModelAndView方法完成了ModelAndViewContainer對象到ModelAndView對象的轉換。

至此,invokeHandlerMethod方法執行完畢,HandlerAdapter的工作全部完成,流程回到DispatcherServlet的doDispatch方法。

12.4.4.6 再次回調攔截器
源碼35DispatcherServlet.javaprotected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {ModelAndView mv = null;Exception dispatchException = null;try {// 處理文件上傳請求 ...// 獲取可用的Handler ...// 獲取HandlerAdapter ...// 回調攔截器 ...// 執行Handler ...// 再次回調攔截器mappedHandler.applyPostHandle(processedRequest, response, mv);// ......
}
源碼36HandlerExecutionChain.javavoid applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)throws Exception {HandlerInterceptor[] interceptors = getInterceptors();if (!ObjectUtils.isEmpty(interceptors)) {// 此處是倒序回調for (int i = interceptors.length - 1; i >= 0; i--) {HandlerInterceptor interceptor = interceptors[i];interceptor.postHandle(request, response, this.handler, mv);}}
}

由 源碼35-36 可知,再次回調攔截器的applyPostHandle方法和 12.4.4.4 節的applyPreHandle的邏輯是幾乎一樣,不一樣的是,這次回調攔截器的順序是反過來的,調用的是攔截器的postHandle方法。

12.4.4.7 處理視圖、解析異常
源碼37DispatcherServlet.javaprotected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {ModelAndView mv = null;Exception dispatchException = null;try {// 處理文件上傳請求 ...// 獲取可用的Handler ...// 獲取HandlerAdapter ...// 回調攔截器 ...// 執行Handler ...// 再次回調攔截器 ...mappedHandler.applyPostHandle(processedRequest, response, mv);} // catch ...// 處理視圖、解析異常processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);// ......
}

由 源碼37 可知,DispatcherServlet的doDispatch方法的最后一個關鍵步驟是processDispatchResult方法,該方法會進行視圖處理,以及解析整個請求處理中拋出的異常。

該方法分為三個步驟,下面拆解來看。

(1)處理異常
源碼38DispatcherServlet.javaprivate void processDispatchResult(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,@Nullable Exception exception) throws Exception {boolean errorView = false;// 處理異常if (exception != null) {if (exception instanceof ModelAndViewDefiningException) {// logger ...mv = ((ModelAndViewDefiningException) exception).getModelAndView();} else {Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);mv = processHandlerException(request, response, handler, exception);errorView = (mv != null);}}// ......
}

由 源碼38 可知,processDispatchResult會根據異常的類型做不同的處理。

DispatcherServlet在處理客戶端發起的請求時,中間調用Controller或者Service等組件時拋出的異常都幾乎不可能是ModelAndViewDefiningException(前面的源碼分析也沒有見過這個異常),因此if代碼塊不重要。

在else代碼塊中,會調用processHandlerException方法,該方法才是處理異常的核心方法。

源碼39DispatcherServlet.javaprotected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,@Nullable Object handler, Exception ex) throws Exception {// ......// 從HandlerExceptionResolver集合中找到可以處理當前異常的// 構造成ModelAndView對象并返回ModelAndView exMv = null;if (this.handlerExceptionResolvers != null) {for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {exMv = resolver.resolveException(request, response, handler, ex);if (exMv != null) {break;}}}// ......throw ex;
}

由 源碼39 可知,processHandlerException方法會從HandlerExceptionResolver集合中找到可以處理當前異常的,構造成ModelAndView對象并返回。

在示例項目中,CustomAdvice類標注了@ControllerAdvice注解,并且聲明了標注@ExceptionHandler注解的方法,可實現全局統一異常處理。

@ControllerAdvice
public class CustomAdvice {@ExceptionHandler({Exception.class})public String customExceptionHandler(Exception ex) {System.out.println("自定義異常發生了," + ex.getMessage());return "自定義異常返回";}}

在UserController的test方法加一行代碼:int i = 1/0,發起請求時一定會拋出異常,控制臺的結果如下:

自定義異常發生了,/ by zero

可見,全局統一異常處理生效了。

(2)渲染視圖
源碼40DispatcherServlet.javaprivate void processDispatchResult(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,@Nullable Exception exception) throws Exception {// 處理異常 ...// 渲染視圖if (mv != null && !mv.wasCleared()) {render(mv, request, response);if (errorView) {WebUtils.clearErrorRequestAttributes(request);}}// ......
}protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {// 國際化處理 ...View view;String viewName = mv.getViewName();if (viewName != null) {// 根據視圖名解析出視圖view = resolveViewName(viewName, mv.getModelInternal(), locale, request);if (view == null) {// throw ...}} else {// 否則,直接獲取視圖;如果還沒有視圖,則拋出異常view = mv.getView();if (view == null) {// throw ...}}// logger ...try {if (mv.getStatus() != null) {response.setStatus(mv.getStatus().value());}// 渲染視圖view.render(mv.getModelInternal(), request, response);} // catch ...
}

由 源碼40 可知,無論是正常響應還是拋出異常,最終都會生成一個ModelAndView對象,緊接著就要進行視圖的渲染,而渲染視圖的核心方法是render方法。

渲染視圖的render方法會從ModelAndView中獲取邏輯視圖的名稱,如果有名稱則借助ViewResolver去匹配視圖,如果成功匹配到則返回,如果匹配不到則拋出異常。匹配生成視圖View對象后,執行view.render方法實際渲染視圖。

(3)第三次回調攔截器
源碼41DispatcherServlet.javaprivate void processDispatchResult(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,@Nullable Exception exception) throws Exception {// 處理異常 ...// 渲染視圖 ...// 第三次回調攔截器if (mappedHandler != null) {mappedHandler.triggerAfterCompletion(request, response, null);}
}
源碼42HandlerExecutionChain.javavoid triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex)throws Exception {HandlerInterceptor[] interceptors = getInterceptors();if (!ObjectUtils.isEmpty(interceptors)) {// 遍歷攔截器,執行其afterCompletion方法for (int i = this.interceptorIndex; i >= 0; i--) {HandlerInterceptor interceptor = interceptors[i];try {interceptor.afterCompletion(request, response, this.handler, ex);} // catch ...}}
}

由 源碼41-42 可知,視圖渲染完成后,最后的收尾動作是回調所有攔截器的afterCompletion方法。

這已經是第三次回調攔截器了,第一次執行攔截器的preHandle方法,第二次倒序執行攔截器的postHandle方法,第三次執行攔截器的afterCompletion方法。其中,第一次是在Handler方法執行之前執行的,第二次和第三次是在之后執行的。

攔截器執行流程
因此,控制臺的輸出如下:

攔截器的preHandle方法執行了...
請求參數 name=aaa
請求參數 time=Thu Feb 29 12:12:12 CST 2024
攔截器的postHandle方法執行了...
攔截器的afterCompletion方法執行了...

至此,processDispatchResult方法執行完畢,DispatcherServlet的doDispatch方法也執行完畢,一次完整的DispatcherServlet請求處理和響應完成了。

12.4.5 DispatcherServlet工作流程總結

DispatcherServlet工作全流程示意圖如下:

DispatcherServlet工作全流程

1. 客戶端向服務端發起請求,由DispatcherServlet接收請求;
2. DispatcherServlet委托HandlerMapping,根據本次請求的URL匹配合適的Controller方法;
3. HandlerMapping找到合適的Controller方法后,組合可以應用于當前請求的攔截器,并封裝為一個HandlerExecutionChain對象返回給DispatcherServlet;
4. DispatcherServlet接收到HandlerExecutionChain對象后,委托HandlerAdapter,將該請求轉發給選定的Handler;
5. Handler接收到請求后,實際執行Controller方法;
6. Controller方法執行完畢后返回一個ModelAndView對象給HandlerAdapter;
7. HandlerAdapter接收到ModelAndView對象后返回給DispatcherServlet;
8. DispatcherServlet接收到ModelAndView對象后,委托ViewResolver進行視圖渲染;
9. ViewResolver視圖渲染完成后返回給DispatcherServlet,由DispatcherServlet負責響應視圖。

12.5 小結

第12章到此就梳理完畢了,本章的主題是:SpringBoot整合WebMvc。回顧一下本章的梳理的內容:

(三十六)SpringBoot整合WebMvc(一)@Controller控制器裝配原理
(三十七)SpringBoot整合WebMvc(二)DispatcherServlet的工作全流程

更多內容請查閱分類專欄:SpringBoot源碼解讀與原理分析

第13章主要梳理:SpringBoot整合WebFlux。主要內容包括:

  • 響應式編程與Reactive;
  • SpringBoot整合WebFlux的快速使用;
  • SpringBoot整合WebFlux的核心自動裝配;
  • DispatcherHandler的工作全流程。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/718378.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/718378.shtml
英文地址,請注明出處:http://en.pswp.cn/news/718378.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

sscanf 函數的用法

sscanf 函數是 C 語言標準庫 <stdio.h> 中的一個函數&#xff0c;用于按照指定的格式從一個字符串中讀取輸入。它的用法類似于 scanf 函數&#xff0c;但是 sscanf 從字符串中讀取輸入&#xff0c;而不是從標準輸入&#xff08;鍵盤&#xff09;中讀取輸入。 以下是 ssc…

優優嗨聚集團:美團代運營服務,商家增長的新引擎

在當今數字化時代&#xff0c;線上平臺已成為商家拓展業務、提升品牌影響力的重要渠道。美團作為國內領先的本地生活服務平臺&#xff0c;擁有龐大的用戶群體和豐富的商業資源。然而&#xff0c;對于許多商家而言&#xff0c;如何在美團平臺上進行有效運營&#xff0c;實現業務…

Redis做分布式鎖如何處理超時時間?

在使用Redis實現分布式鎖時&#xff0c;處理超時時間是非常重要的&#xff0c;以確保在獲取鎖的客戶端在一定時間內未能完成任務時&#xff0c;鎖能夠自動釋放&#xff0c;避免造成死鎖或長時間的阻塞。下面是一種處理超時時間的方法&#xff1a; 獲取鎖時設置超時時間&#xf…

雙線服務器有哪些安全防御措施?

雙線服務器的出現給企業帶來了更廣泛的業務發展&#xff0c;用戶不再是固定的群體&#xff0c;而是有了一定的選擇性&#xff0c;服務器的性能與可靠性進行了增強&#xff0c;使網絡的運行速度變得更加流暢&#xff0c;給用戶帶來了良好的體驗感。 今天我們主要就來聊一聊雙線服…

【IOS】啟動報錯Cannot launch ‘/private/var/containers/Bundle/Application/....‘

問題 IOS項目啟動報錯Cannot launch ‘/private/var/containers/Bundle/Application/***.app’: Sending qLaunchSuccess packet failed 或者類似報錯問題 無法啟動launch的 解決 問題定位 我是在操作期間更換了應用的簽名證書 也就是Signing & Capablities -> Sign…

【LeetCode:232. 用棧實現隊列 + 棧 | 隊列】

&#x1f680; 算法題 &#x1f680; &#x1f332; 算法刷題專欄 | 面試必備算法 | 面試高頻算法 &#x1f340; &#x1f332; 越難的東西,越要努力堅持&#xff0c;因為它具有很高的價值&#xff0c;算法就是這樣? &#x1f332; 作者簡介&#xff1a;碩風和煒&#xff0c;…

力扣74. 搜索二維矩陣(二分查找)

Problem: 74. 搜索二維矩陣 文章目錄 題目描述思路復雜度Code 題目描述 思路 思路1&#xff1a;映射為一維數組二分查找 1.由于題目矩陣中的元素整體是升序的&#xff0c;我們可以將其放置在一個大小為 m n m \times n mn的一維數組array中進行二分查找 2.對應的映射關系是ar…

NACOS在Windows和Linux下的安裝教程

目錄 1、Windows安裝 1.1、下載安裝包 1.2、解壓 1.3、端口配置 1.4、啟動 1.5、訪問 2、Linux安裝 2.1、安裝JDK 2.2、上傳安裝包 2.3、解壓 2.4、端口配置 2.5、啟動 3、Nacos的依賴 1、Windows安裝 開發階段采用單機安裝即可。 1.1、下載安裝包 在Nacos的Git…

Vue+SpringBoot打造圖書借閱系統

目錄 一、摘要1.1 項目介紹1.2 項目錄屏 二、功能模塊2.1 登陸注冊模塊2.2 圖書管理模塊2.3 圖書評論模塊2.4 圖書預定模塊2.5 圖書資訊模塊 三、系統設計3.1 系統結構設計3.1.1登陸注冊模塊的結構設計3.1.2圖書管理模塊的結構設計3.1.3圖書評論模塊的結構設計3.1.4圖書預定模塊…

clickhouse 隨心所欲的聚合模型-AggregatingMergeTree

clickhouse 強大的 MergeTree 系列引擎令人信服&#xff0c;其 ReplacingMergeTree、SummingMergeTree 在數據唯一性和匯總場景中表現非凡。但你是否還有保留最小(大)、平均等預聚合需求&#xff0c;甚至在一個模型中既有唯一性語意也有匯總、最小、最大、平均值語意該如何處理…

Spring-靜態代理VS動態代理/實現代理ProxyFactory

文章目錄 靜態代理VS動態代理Spring實現代理ProxyFactory 工作中遇到問題整理動態代理異常com.sun.proxy.$Proxy0 cannot be cast to 靜態代理VS動態代理 靜態代理VS動態代理 參考URL: https://blog.csdn.net/qq_25881443/article/details/103245938 【java項目實戰】代理模式…

【C語言】剖析qsort函數的實現原理

主頁&#xff1a;17_Kevin-CSDN博客 專欄&#xff1a;《C語言》 本文將從回調函數&#xff0c;qsort函數的應用&#xff0c;qsort函數的實現原理三個方面進行講解&#xff0c;請自行跳轉至相對位置進行閱讀~ 目錄 回調函數 qsort函數的應用 qsort函數實現原理 回調函數 什…

mysql主從庫Slave_SQL_Running: No問題經驗分享

最近在創建mysql主從庫的時候&#xff0c;遇到一個問題。執行 mysql> SHOW SLAVE STATUS\G結果顯示 Slave_IO_Running: Yes Slave_SQL_Running: No 很是苦惱&#xff0c;查詢了很久沒有解決 執行 mysql> SELECT * FROM performance_schema.replication_applier_status_…

獨立游戲《星塵異變》UE5 C++程序開發日志1——項目與代碼管理

寫在前面&#xff1a;本日志系列將會向大家介紹在《星塵異變》這款模擬經營游戲&#xff0c;在開發時用到的與C相關的泛用代碼與算法&#xff0c;主要記錄UE5C與原生C的用法區別&#xff0c;以及遇到的問題和解決辦法&#xff0c;因為這是我本人從ACM退役以后第一個從頭開始的項…

代碼隨想錄算法訓練營第五十天 | 買股票2

目錄 買賣股票的最佳時機III買賣股票的最佳時機IV LeetCode 123.買賣股票的最佳時機III LeetCode 123.買賣股票的最佳時機IV 買賣股票的最佳時機III 給定一個數組&#xff0c;它的第 i 個元素是一支給定的股票在第 i 天的價格。 設計一個算法來計算你所能獲取的最大利潤。…

牛客周賽 Round 35(A,B,C,D,E,F,G)

這場簡單&#xff0c;甚至賽時90分鐘不到就AK了。比賽鏈接&#xff0c;隊友題解友鏈 剛入住學校監獄&#xff0c;很不適應&#xff0c;最近難受的要死&#xff0c;加上最近幾場CF打的都不順利&#xff0c;san值要爆掉了&#xff0c;只能慢慢補題了。 這場C是個滑動窗口&#…

冒泡排序 和 qsort排序

目錄 冒泡排序 冒泡排序部分 輸出函數部分 主函數部分 總代碼 控制臺輸出顯示 總代碼解釋 冒泡排序優化 冒泡排序 主函數 總代碼 代碼優化解釋 qsort 排序 qsort 的介紹 使用qsort排序整型數據 使用qsort排序結構數據 冒泡排序 首先&#xff0c;我先介紹我的冒泡…

模糊搜索小案例

C#窗體實現數據錄入與模糊搜索小案例 記錄一下 主要代碼 private void button1_Click(object sender, EventArgs e){string name textBox1.Text;string hometown textBox4.Text;string school textBox6.Text;string sex textBox5.Text;string lat textBox3.Text;string …

c#打印BarTend標簽提示:具名數據源沒有cuckoo*具名數據(解決)

c#打印BarTend標簽提示&#xff1a;具名數據源沒有cuckoo*具名數據&#xff08;解決&#xff09; 今天咕咕更新打印模板的時候遇到的問題&#xff0c;就是在模版中配置了字段名&#xff0c;但是啟動c#應用&#xff0c;后端發送json數據打印的時候c#報錯提示&#xff0c;沒有在…

python 小游戲《2048》字符版非圖形界面

參考鏈接&#xff1a; 閑談2048小游戲和數組的旋轉及翻轉和轉置 目錄 2048 一、方陣類 二、隨機插入1或2 三、 合并和遞增 四、 判斷和移動 五、 鍵盤控制 完整源代碼 玩法過程 2048 上回說到2048小游戲中數組的各種旋轉、翻轉的方法&#xff0c;就是為代碼編程作準…