Spring 源碼解析 —— DispatcherServlet
版本: SpringMVC 5.14
Servlet 是 Tomcat 的入口,而 DispatcherServlet 則是 Spring MVC 的入口,主要負責調度功能。
既然是 Servlet 的實現類,所以從實現的接口看起最好,而 Servlet 最重要的方法就是 doService,就從 DispatcherServlet 的 doService 看起。
doService 方法很復雜,下面會具體解析,這裏說一下 Spring 代碼的一種主要思路:
首先,他有前置的一些條件,比如驗證參數合法性,Spring 會寫在最前面,這裏就是 logRequest 記錄日誌;
其次,開始執行該方法真正要做的事情;
最後,會做一些後置的事情,在有些時候就是資源清理。
這個思路在 Spring 中很常見,代碼寫多的人也會這樣做,對於新人的建議就是,寫代碼先驗證、在執行、後清理。不要一邊驗證、一邊執行、一邊清理,這樣代碼看起來很亂。
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. 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. 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()); 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 { if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { // Restore the original attribute snapshot, in case of an include. if (attributesSnapshot != null) { restoreAttributesAfterInclude(request, attributesSnapshot); } } } }
首先,logRequest 記錄了請求日誌。我用的是 Spring 5.14,可以看到大量方法使用了 Java 8 新增的 Stream 相關技術。函數式編程是一大趨勢,需要掌握。這裏記錄了請求相關的參數。
private void logRequest(HttpServletRequest request) { LogFormatUtils.traceDebug(logger, traceOn -> { String params; if (isEnableLoggingRequestDetails()) { params = request.getParameterMap().entrySet().stream() .map(entry -> entry.getKey() + ":" + Arrays.toString(entry.getValue())) .collect(Collectors.joining(", ")); } else { params = (request.getParameterMap().isEmpty() ? "" : "masked"); } String query = StringUtils.isEmpty(request.getQueryString()) ? "" : "?" + request.getQueryString(); String dispatchType = (!request.getDispatcherType().equals(DispatcherType.REQUEST) ? "\"" + request.getDispatcherType().name() + "\" dispatch for " : ""); String message = (dispatchType + request.getMethod() + " \"" + getRequestUri(request) + query + "\", parameters={" + params + "}"); if (traceOn) { List<String> values = Collections.list(request.getHeaderNames()); String headers = values.size() > 0 ? "masked" : ""; if (isEnableLoggingRequestDetails()) { headers = values.stream().map(name -> name + ":" + Collections.list(request.getHeaders(name))) .collect(Collectors.joining(", ")); } return message + ", headers={" + headers + "} in DispatcherServlet ‘" + getServletName() + "‘"; } else { return message; } }); }
後面有一段代碼是有關 JSP include 命令的,這裏就不多說了,因為現在大多數地方都不用 JSP 了。
// Keep a snapshot of the request attributes in case of an include, // to be able to restore the original attributes after the 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)); } } }
後面會看到在 request 上設置了很多屬性,這裏需要學習的一點就是,Spring 相關屬性的名字都是以常量方式定義的,這麽做的好處是,假如你很多地方用了一個值,這時候想修改該值,只需要修改常量的值即可;否則你需要每處都修改該值,容易出錯又麻煩。
還有,這集 localResolver 等等,他都是以接口定義的。這裏使用了策略模式,就比如 localResolver,這樣各種業務就可以有不同的實現。這麽做的原因,我舉個不太恰當的例子:假如我是中國人,現在解析到請求屬於臺灣,那我覺得這個 locale 可以定義為中國;可是 X 獨分子看到這段代碼肯定不這麽認為,這個 locale 屬於臺灣,他們就可以每個人自定一套 localeResolver。策略模式的有點就是,不同場景有不同的實現,這也是面向對象的靈活之處。後面你會發現很多地方都在使用這個技巧。
// Make framework objects available to handlers and view objects. 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());
下面的 flash 就不多說了,也是 JSP 的。
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); }
下面的 doDispatch 才是真正的“分發”。
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); } } }
todo
Spring 源碼解析 —— DispatcherServlet