DispatcherServlet 分發流程

0 太長不看版

  • HTTPServletService 方法將請求按類進行分解
    • 主要是根據HTTP方法的類型調用 doXXX 方法
    • GET 和 HEAD 方法需要對 if-modified-since 進行特殊處理,其他是直接調用
  • FrameworkServlet 重寫 doXXX 方法,統一調用 doService 方法
    • doXXX 方法統一調用 processRequest 方法
      • doOptionsdoTrace 有額外的處理
      • 其他是直接調用
    • processRequest 主要是初始化 ThreadLocal ,調用 doService 方法,並進行日誌等處理
      • ThreadLocalLocalContextAttributes
      • doService 方法執行核心邏輯,是抽象方法
      • 完成後會清空 ThreadLocal,列印日誌,產生事件。
  • DispatcherServlet 進行具體的實現
    • 重寫 doService 方法
      1. 添加 DispatcherServlet 特有的請求屬性
      2. 對 HTML 的 include 請求進行處理
      3. 對重定向的請求進行處理
      4. 將請求轉交給 doDispatch 方法進行實際的分發
    • doDispatch 方法的邏輯為:
      1. 查找是否有合適的 Handler,該過程在基於RESTful API 設計的 SpringMVC 中有性能問題
      2. 查找 Handler 是否有支援的 Adapter
      3. 執行攔截器
      4. 執行處理
      5. 解析結果並返回

1,DispatcherServlet 的父類做了什麼

DistpathcerServlet 的類圖如下,可見其父類為 FrameworkServlet ,同時是一個 HttpServlet .
UML類圖

1.1 HttpServlet 的分發邏輯:

service 方法作為入口, 其邏輯如下:

protected void service(HttpServletRequest req, HttpServletResponse resp)
	throws ServletException, IOException {

	String method = req.getMethod();

	if (method.equals(METHOD_GET)) {
		long lastModified = getLastModified(req);
		if (lastModified == -1) {
			// servlet doesn't support if-modified-since, no reason
			// to go through further expensive logic
			doGet(req, resp);
		} else {
			long ifModifiedSince;
			try {
				ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
			} catch (IllegalArgumentException iae) {
				// Invalid date header - proceed as if none was set
				ifModifiedSince = -1;
			}
			if (ifModifiedSince < (lastModified / 1000 * 1000)) {
				// If the servlet mod time is later, call doGet()
				// Round down to the nearest second for a proper compare
				// A ifModifiedSince of -1 will always be less
				maybeSetLastModified(resp, lastModified);
				doGet(req, resp);
			} else {
				resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
			}
		}

	} else if (method.equals(METHOD_HEAD)) {
		long lastModified = getLastModified(req);
		maybeSetLastModified(resp, lastModified);
		doHead(req, resp);

	} else if (method.equals(METHOD_POST)) {
		doPost(req, resp);

	} else if (method.equals(METHOD_PUT)) {
		doPut(req, resp);

	} else if (method.equals(METHOD_DELETE)) {
		doDelete(req, resp);

	} else if (method.equals(METHOD_OPTIONS)) {
		doOptions(req,resp);

	} else if (method.equals(METHOD_TRACE)) {
		doTrace(req,resp);

	} else {
		//
		// Note that this means NO servlet supports whatever
		// method was requested, anywhere on this server.
		//

		String errMsg = lStrings.getString("http.method_not_implemented");
		Object[] errArgs = new Object[1];
		errArgs[0] = method;
		errMsg = MessageFormat.format(errMsg, errArgs);

		resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
	}
}

可見對於 POST PUT DELETE OPTIONS TRACE 方法都是直接調用對應的具體方法 doXXX, 而 GET 方法會增加 if-modified-since 檢查,符合條件後才進行 doGet, 這是為了支援 HTTP 協議的 if-modified-since 請求頭。而 HEAD 方法的額外處理也是檢查是否需要設置 last-modified 屬性。

HTTP 協議的 if-modified-since 請求是條件請求,要求返回的資源在指定日期後發生過修改。如果發生修改,則返回 200 OK 以及對應資源,否則返回 304 Not Modified.

1.2 FrameworkServlet 做了什麼

1.2.1 重寫 doXXX 方法

FrameworkServlet 沒有修改 HttpServlet 的分發邏輯,而是將所有的 doXXX 方法調用了同一方法 processRequest

@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
	throws ServletException, IOException {

	processRequest(request, response);
}

doOptionsdoTrace 方法進行了一些額外處理:

1.2.1.1 doOptions

@Override
protected void doOptions(HttpServletRequest request, HttpServletResponse response)
	throws ServletException, IOException {

	// 如果允許轉發,並且是 CORS 請求,那麼檢查它是否是 pre-flight 預檢請求
	if (this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) {
		processRequest(request, response);
		if (response.containsHeader("Allow")) {
			// Proper OPTIONS response coming from a handler - we're done.
			// 如果返回值是 "Allow",即方法允許,那就直接返回
			return;
		}
	}

	// 否則使用 HttpServlet 默認的方法檢查是否允許
	// Use response wrapper in order to always add PATCH to the allowed methods
	super.doOptions(request, new HttpServletResponseWrapper(response) {
		@Override
		public void setHeader(String name, String value) {
			// 如果結果是 "Allow", 那麼以指定的格式返回
			if ("Allow".equals(name)) {
				value = (StringUtils.hasLength(value) ? value + ", " : "") + HttpMethod.PATCH.name();
			}
			super.setHeader(name, value);
		}
	});
}

doOptions 方法例外在 OPTIONS 方法由多個來源,而 Tomcat 只處理來自 CORS 的預檢命令。對於不接受 CORS 的 Servlet 或其他來源的 OPTIONS 請求,就調用默認的方法實現。

1.2.1.2 doTrace

@Override
protected void doTrace(HttpServletRequest request, HttpServletResponse response)
	throws ServletException, IOException {

	if (this.dispatchTraceRequest) {
		processRequest(request, response);
		if ("message/http".equals(response.getContentType())) {
			// Proper TRACE response coming from a handler - we're done.
			return;
		}
	}
	super.doTrace(request, response);
}

doTrace 方法就簡單很多,它只需判斷是否已經處理了命令,如果沒有,則調用默認的方法。

為什麼不直接用 processRequest 重寫 service 方法?
除了滿足里氏替換原則以外,根據 1.1 的分析,我們也可以看到,service 方法中處理了快取機制,即 last-modified 屬性和 if-modified-since 屬性的相關處理,以及 OPTIONS 和 TRACE 方法所需的一些額外處理

1.2.2 processRequest

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());

	// 初始化上下文,實際上是把後兩個參數用 ThreadLocal 快取起來
	initContextHolders(request, localeContext, requestAttributes);

	// 執行真正的業務邏輯 doService
	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 {
		// 由於 ThreadLocal 是執行緒專用的,在執行緒池場景下需要清空,否則會影響下一次使用。
		resetContextHolders(request, previousLocaleContext, previousAttributes);
		if (requestAttributes != null) {
			requestAttributes.requestCompleted();
		}
		// 輸出日誌
		logResult(request, response, failureCause, asyncManager);
		// 輸出事件
		publishRequestHandledEvent(request, response, startTime, failureCause);
	}
}

processRequest 主要是進行封裝和異常處理,並調用 doService 方法進行核心實現。而 doService 是一個抽象方法, DispatcherServlet 就實現了這一方法。

2 DispatcherServlet 的分發流程

2.1 doService 方法

doService 方法的文檔注釋為:暴露 DispatcherServlet 特有的請求屬性,並將請求轉交給 doDispatch 進行實際的分發

@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
	logRequest(request);

	// Keep a snapshot of the request attributes in case of an include,
	// to be able to restore the original attributes after the include.
	// 如果是 include 請求,保存請求的一個快照,以在 include 中保存原始屬性
	Map<String, Object> attributesSnapshot = null;
	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));
			}
		}
	}

	// Make framework objects available to handlers and view objects.
	// 將框架對象暴露給 handler(controller)和 VO
	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 恢復重定向請求的屬性
	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);
	}

	// 獲取之前的 Path,並將當前 Path 添加到屬性中
	RequestPath previousRequestPath = null;
	if (this.parseRequestPath) {
		previousRequestPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
		ServletRequestPathUtils.parseAndCache(request);
	}

	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);
			}
		}
		if (this.parseRequestPath) {
			// 恢復 RequestPath
			/* 該方法的邏輯為:如果 previousRequestPath 不為空,則將 request 的 PATH_ATTRIBUTE 
			   屬性設為 previousRequestPath, 否則刪除 PATH_ATTRIBUTE 屬性
			 */
			ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);
		}
	}
}

該方法的功能是:

  • 添加 DispatcherServlet 特有的請求屬性
  • 對 HTML 的 include 請求進行處理
  • 對重定向的請求進行處理
  • 將請求轉交給 doDispatch 方法進行實際的分發

HTML 的 include 會引入另一個頁面。這裡採用了快照技術,將原始屬性存放到快照中,並在處理完成後恢復原屬性,以避免 include 後對原頁面產生影響。

FlashMap 將一個 Request 的屬性存儲,並存放在 Session 中,這樣如果發生了重定向,新產生的 Request 就可以從 flashMapManager 中獲取前一個請求的屬性(INPUT_FLASH_MAP_ATTRIBUTE)

2.2 doDispatch 方法

@SuppressWarnings("deprecation")
protected 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 {
			// 通過 multipartResolver 判斷是否是文件的多部分請求
			processedRequest = checkMultipart(request);
			multipartRequestParsed = (processedRequest != request);

			// Determine handler for the current request.
			// 獲取 handler
			mappedHandler = getHandler(processedRequest);
			if (mappedHandler == null) {
				noHandlerFound(processedRequest, response);
				return;
			}

			// Determine handler adapter for the current request.
			// 獲取 HandlerAdapter
			HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

			// Process last-modified header, if supported by the handler.
			// 進行 last-modified 處理
			String method = request.getMethod();
			boolean isGet = HttpMethod.GET.matches(method);
			if (isGet || HttpMethod.HEAD.matches(method)) {
				long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
				if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
					return;
				}
			}

			// 檢查在 HandlerExecutionChain 中被註冊的攔截器,調用 preHandle 方法,返回值為是否放行
			// 內部使用了責任鏈模式
			if (!mappedHandler.applyPreHandle(processedRequest, response)) {
				return;
			}

			// Actually invoke the handler.
			// 最終執行 handler,並獲取 ModelAndView
			mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

			if (asyncManager.isConcurrentHandlingStarted()) {
				return;
			}

			// 如果返回的是 Model(即沒有 View ),那麼使用默認的 ViewName
			applyDefaultViewName(processedRequest, mv);
			// 執行攔截器的 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.
			// 從 Spring 4.3 起,handler 發生的 Error 也會被處理
			dispatchException = new NestedServletException("Handler dispatch failed", err);
		}
		// 處理分發的結果,即解析後的 View 對象
		processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
	}
	// 出現異常,則需要繼續執行完成攔截器的 afterCompletion 方法
	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()) {
			// Instead of postHandle and afterCompletion
			// 如果是並發執行,則非同步結束攔截器
			if (mappedHandler != null) {
				mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
			}
		}
		else {
			// Clean up any resources used by a multipart request.
			// 如果是同步執行,則所有攔截器已經結束,這裡把 MultiPart 請求過程中佔用的文件全部釋放
			if (multipartRequestParsed) {
				cleanupMultipart(processedRequest);
			}
		}
	}
}

doDispatch 方法的邏輯為:

  1. 查找是否有合適的 Handler
  2. 查找 Handler 是否有支援的 Adapter
  3. 執行攔截器
  4. 執行處理
  5. 解析結果並返回

MultipartResolver 解析請求是否是以允許的方法請求多部分文件,用於文件上傳

3 相關問題

3.1 RESTful API 的性能問題

問題來源參見:SpringMVC RESTful 性能優化

doDispatch 方法查找是否有合適的 Handler 的調用鏈為:

DispatcherServlet::doDispatch -> DispatcherServlet::getHandler -> AbstractHandleMapping::getHandle -> AbstractHandlerMethodMapping::getHandlerInternal -> AbstractHandlerMethodMapping::lookupHandlerMethod

源碼為:

@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
	List<Match> matches = new ArrayList<>();
	// 查詢是否有直接匹配
	List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
	// 如果有直接匹配,那麼就將結果快取
	if (directPathMatches != null) {
		addMatchingMappings(directPathMatches, matches, request);
	}
	// 如果沒有直接匹配,或者直接匹配的快取結果為空,那麼就將所有的記錄加入
	if (matches.isEmpty()) {
		addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
	}
	if (!matches.isEmpty()) {
		// 如果此時有了匹配,那就查找
		Match bestMatch = matches.get(0);
		// 如果匹配的 match 不止 1 個,那麼就檢查模糊性
		if (matches.size() > 1) {
			// 如果有超過一個匹配,則排序
			Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
			matches.sort(comparator);
			bestMatch = matches.get(0);
			// 日誌 TRACE 級
			if (logger.isTraceEnabled()) {
				logger.trace(matches.size() + " matching mappings: " + matches);
			}
			// 檢查模糊性
			if (CorsUtils.isPreFlightRequest(request)) {
				// 如果是 CORS 的預檢命令,檢查是否有 match 含有 CORS 配置
				for (Match match : matches) {
					if (match.hasCorsConfig()) {
						return PREFLIGHT_AMBIGUOUS_MATCH;
					}
				}
			}
			else {
				// 如果不是 CORS 預檢命令,檢查次佳匹配是否和最佳匹配同等最佳
				Match secondBestMatch = matches.get(1);
				if (comparator.compare(bestMatch, secondBestMatch) == 0) {
					Method m1 = bestMatch.getHandlerMethod().getMethod();
					Method m2 = secondBestMatch.getHandlerMethod().getMethod();
					String uri = request.getRequestURI();
					throw new IllegalStateException(
						"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
				}
			}
		}
		request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());
		handleMatch(bestMatch.mapping, lookupPath, request);
		return bestMatch.getHandlerMethod();
	}
	else {
		// 如果此時 matches 依然為空,那就直接返回
		return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
	}
}

AbstractHandlerMethodMapping::lookupHandlerMethod 中的查找邏輯為:

  1. 查找 mappingRegistry 中是否快取有該 URL 的結果快取。如果有,那就將快取直接加入 matches
  2. 如果沒有直接匹配,或者直接匹配的快取結果為空,那麼就將將所有註冊的映射加入 matches 中,並遍歷檢查是否匹配
  3. 如果 matches 為空,直接返回 handleNoMatch
  4. 否則,檢查最佳匹配是否唯一,如果唯一,則返回最佳匹配的 Handler, 否則返回模糊匹配錯誤

由於在 RESTful API 中,會有大量相似的 URL,第二步中的遍歷將不得不使用正則表達式匹配,而正則表達式匹配的時間複雜度是很高的。因此當 RESTful API 不斷增長的時候,性能也會不斷變差。

解決方案就是SpringMVC RESTful 性能優化中提到的繼承 AbstractHandlerMethodMapping 類並重寫其匹配邏輯,然後替換掉 SpringMVC 的默認組件。

Tags: