我們首先引用《Spring in Action》上的一張圖來了解Spring MVC 的核心組件和大致處理流程:
從上圖中看到①、DispatcherServlet 是SpringMVC 中的前端控制器(Front Controller),負責接收Request 並將Request 轉發給對應的處理組件。
② 、HanlerMapping 是SpringMVC 中完成url 到Controller 映射的組件。DispatcherServlet 接收Request, 然后從HandlerMapping 查找處理Request 的Controller。
③、Controller 處理Request,並返回ModelAndView 對象,Controller 是SpringMVC中負責處理Request 的組件(類似於Struts2 中的Action),ModelAndView 是封裝結果視圖的組件。
④、⑤、⑥視圖解析器解析ModelAndView 對象並返回對應的視圖給客戶端。在前面的學習中我們已經大致了解到,容器初始化時會建立所有url 和Controller 中的Method 的對應關系,保存到HandlerMapping 中,用戶請求是根據Request 請求的url 快速定位到Controller 中的某個方法。在Spring 中先將url 和Controller 的對應關系,保存到Map<url,Controller>中。Web 容器啟動時會通知Spring 初始化容器(加載Bean 的定義信息和初始化所有單例Bean),然后SpringMVC 會遍歷容器中的Bean,獲取每一個Controller 中的所有方法訪問的url,然后將url 和Controller 保存到一個Map中;這樣就可以根據Request 快速定位到Controller,因為最終處理Request 的是Controller 中的方法,Map 中只保留了url 和Controller 中的對應關系,所以要根據Request 的url 進一步確認Controller 中的Method,這一步工作的原理就是拼接Controller 的url(Controller 上@RequestMapping 的值) 和方法的url(Method 上@RequestMapping 的值),與request 的url 進行匹配,找到匹配的那個方法;確定處理請求的Method 后,接下來的任務就是參數綁定,把Request 中參數綁定到方法的形式參數上,這一步是整個請求處理過程中最復雜的一個步驟。
Spring MVC 九大組件
MultipartResolver(多文件上傳組件)
其實這是一個大家很熟悉的組件,MultipartResolver 用於處理上傳請求,通過將普通的Request 包裝成MultipartHttpServletRequest 來實現。MultipartHttpServletRequest可以通過getFile() 直接獲得文件,如果是多個文件上傳,還可以通過調用getFileMap得到Map<FileName, File> 這樣的結構。MultipartResolver 的作用就是用來封裝普通的request,使其擁有處理文件上傳的功能。
LocaleResolver(本地語言環境組件)
在上面我們有看到ViewResolver 的resolveViewName()方法,需要兩個參數。那么第二個參數Locale 是從哪來的呢,這就是LocaleResolver 要做的事了。LocaleResolver用於從request 中解析出Locale, 在中國大陸地區,Locale 當然就會是zh-CN 之類,用來表示一個區域。這個類也是i18n 的基礎。
ThemeResolver(模板主題處理組件)
從名字便可看出,這個類是用來解析主題的。主題,就是樣式,圖片以及它們所形成的顯示效果的集合。Spring MVC 中一套主題對應一個properties 文件,里面存放着跟當前主題相關的所有資源,如圖片,css 樣式等。創建主題非常簡單,只需准備好資源,然后新建一個"主題名.properties" 並將資源設置進去,放在classpath 下,便可以在頁面中使用了。Spring MVC 中跟主題有關的類有ThemeResolver, ThemeSource 和Theme。ThemeResolver 負責從request 中解析出主題名, ThemeSource 則根據主題名找到具體的主題, 其抽象也就是Theme, 通過Theme 來獲取主題和具體的資源。
HandlerMappings
HandlerMapping 是用來查找Handler 的,也就是處理器,具體的表現形式可以是類也可以是方法。比如,標注了@RequestMapping 的每個method 都可以看成是一個Handler,由Handler 來負責實際的請求處理。HandlerMapping 在請求到達之后,它的作用便是找到請求相應的處理器Handler 和Interceptors。
HandlerAdapters (初始化攔截適配器)
從名字上看,這是一個適配器。因為Spring MVC 中Handler 可以是任意形式的,只要能夠處理請求便行, 但是把請求交給Servlet 的時候,由於Servlet 的方法結構都是如doService(HttpServletRequest req, HttpServletResponse resp) 這樣的形式,讓固定的Servlet 處理方法調用Handler 來進行處理,這一步工作便是HandlerAdapter 要做的事。
HandlerExceptionResolvers(異常處理組件)
從這個組件的名字上看,這個就是用來處理Handler 過程中產生的異常情況的組件。具體來說,此組件的作用是根據異常設置ModelAndView, 之后再交給render()方法進行渲染, 而render() 便將ModelAndView 渲染成頁面。不過有一點,HandlerExceptionResolver 只是用於解析對請求做處理階段產生的異常,而渲染階段的異常則不歸他管了,這也是Spring MVC 組件設計的一大原則分工明確互不干涉。
RequestToViewNameTranslator(視圖預處理器組件)
這個組件的作用,在於從Request 中獲取viewName. 因為ViewResolver 是根據ViewName 查找View, 但有的Handler 處理完成之后,沒有設置View 也沒有設置ViewName, 便要通過這個組件來從Request 中查找viewName。
ViewResolvers(試圖轉換器)
視圖解析器,相信大家對這個應該都很熟悉了。因為通常在SpringMVC 的配置文件中,都會配上一個該接口的實現類來進行視圖的解析。這個組件的主要作用,便是將String類型的視圖名和Locale 解析為View 類型的視圖。這個接口只有一個resolveViewName()方法。從方法的定義就可以看出,Controller 層返回的String 類型的視圖名viewName,最終會在這里被解析成為View。View 是用來渲染頁面的,也就是說,它會將程序返回的參數和數據填入模板中,最終生成html 文件。ViewResolver 在這個過程中,主要做兩件大事,即,ViewResolver 會找到渲染所用的模板(使用什么模板來渲染?)和所用的技術(其實也就是視圖的類型,如JSP 啊還是其他什么Blabla 的)填入參數。默認情況下,Spring MVC 會為我們自動配置一個InternalResourceViewResolver,這個是針對JSP 類型視圖的。
FlashMapManager()
說到FlashMapManager,就得先提一下FlashMap。FlashMap 用於重定向Redirect 時的參數數據傳遞,比如,在處理用戶訂單提交時,為了避免重復提交,可以處理完post 請求后redirect 到一個get 請求,這個get 請求可以用來顯示訂單詳情之類的信息。這樣做雖然可以規避用戶刷新重新提交表單的問題,但是在這個頁面上要顯示訂單的信息,那這些數據從哪里去獲取呢,因為redirect 重定向是沒有傳遞參數這一功能的,如果不想把參數寫進url(其實也不推薦這么做,url 有長度限制不說,把參數都直接暴露,感覺也不安全), 那么就可以通過flashMap 來傳遞。只需要在redirect 之前, 將要傳遞的數據寫入request ( 可以通過ServletRequestAttributes.getRequest() 獲得) 的屬性OUTPUT_FLASH_MAP_ATTRIBUTE 中,這樣在redirect 之后的handler 中Spring 就會自動將其設置到Model 中,在顯示訂單信息的頁面上,就可以直接從Model 中取得數據了。而FlashMapManager 就是用來管理FlashMap 的。
Spring MVC 源碼分析
根據上面分析的Spring MVC 工作機制,從三個部分來分析Spring MVC 的源代碼。
- ApplicationContext 初始化時用Map 保存所有url 和Controller 類的對應關系;
- 根據請求url 找到對應的Controller,並從Controller 中找到處理請求的方法;
- Request 參數綁定到方法的形參,執行方法處理請求,並返回結果視圖。
初始化階段
我們首先找到DispatcherServlet 這個類,必然是尋找init()方法。然后,我們發現其init方法其實在父類HttpServletBean 中,其源碼如下:
public final void init() throws ServletException { if (logger.isDebugEnabled()) { logger.debug("Initializing servlet '" + getServletName() + "'"); } // Set bean properties from init parameters. //從初始化參數設置bean屬性。 PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); if (!pvs.isEmpty()) { try { //定位資源 BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); //加載配置信息 ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext()); bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment())); initBeanWrapper(bw); bw.setPropertyValues(pvs, true); } catch (BeansException ex) { if (logger.isErrorEnabled()) { logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex); } throw ex; } } // Let subclasses do whatever initialization they like. //模板方法,子類可以去自定義 initServletBean(); if (logger.isDebugEnabled()) { logger.debug("Servlet '" + getServletName() + "' configured successfully"); } }
我們看到在這段代碼中, 又調用了一個重要的initServletBean() 方法。進入initServletBean()方法看到以下源碼:
protected final void initServletBean() throws ServletException { getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'"); if (this.logger.isInfoEnabled()) { this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started"); } long startTime = System.currentTimeMillis(); try { this.webApplicationContext = initWebApplicationContext(); initFrameworkServlet(); } catch (ServletException ex) { this.logger.error("Context initialization failed", ex); throw ex; } catch (RuntimeException ex) { this.logger.error("Context initialization failed", ex); throw ex; } if (this.logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " + elapsedTime + " ms"); } }
這段代碼中 initWebApplicationContext() 最主要的邏輯就是初始化IOC 容器,最終會調用refresh()方法,前面的IOC 容器的初始化細節我們已經詳細掌握,在此不再贅述。
private boolean refreshEventReceived = false; protected WebApplicationContext initWebApplicationContext() { WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContext wac = null; 。。。。。。。。if (wac == null) { // No context instance is defined for this servlet -> create a local one wac = createWebApplicationContext(rootContext); } if (!this.refreshEventReceived) { // Either the context is not a ConfigurableApplicationContext with refresh // support or the context injected at construction time had already been // refreshed -> trigger initial onRefresh manually here. onRefresh(wac); } 。。。。。。。。return wac; }
我們看到上面的代碼中,IOC 容器初始化之后,最后有調用了onRefresh()方法。這個方法最終是在DisptcherServlet 中實現,來看源碼:
@Override protected void onRefresh(ApplicationContext context) { initStrategies(context); } /** * Initialize the strategy objects that this servlet uses. * <p>May be overridden in subclasses in order to initialize further strategy objects. */ //初始化策略 protected void initStrategies(ApplicationContext context) { //多文件上傳的組件 initMultipartResolver(context); //初始化本地語言環境 initLocaleResolver(context); //初始化模板處理器 initThemeResolver(context); //handlerMapping initHandlerMappings(context); //初始化攔截適配器 initHandlerAdapters(context); //初始化異常攔截器 initHandlerExceptionResolvers(context); //初始化視圖預處理器 initRequestToViewNameTranslator(context); //初始化視圖轉換器 initViewResolvers(context); //FlashMap 管理器 initFlashMapManager(context); }
到這一步就完成了Spring MVC 的九大組件的初始化。接下來,我們來看url 和Controller的關系是如何建立的呢?HandlerMapping是個接口。先來看一下HandlerMapping 的實現類,會看到一個AbstractDetectingUrlHandlerMapping,可以看到其實現了ApplicationContextAware,在Spring容器會檢測容器中的所有Bean,如果發現某個Bean實現了ApplicationContextAware接口,Spring容器會在創建該Bean之后,自動調用該Bean的setApplicationContextAware()方法。看看他的類圖再慢慢去尋找這個觸發點。
最后我們找啊找,會在ApplicationObjectSupport 發現了這個方法,繼而調用到了HandlerMapping 的子類AbstractDetectingUrlHandlerMapping 中的initApplicationContext()方法,所以我們直接看子類中的初始化容器方法:
//建立當前ApplicationContext 中的所有Controller 和url 的對應關系 protected void detectHandlers() throws BeansException { ApplicationContext applicationContext = obtainApplicationContext(); if (logger.isDebugEnabled()) { logger.debug("Looking for URL mappings in application context: " + applicationContext); } // 獲取ApplicationContext 容器中所有bean 的Name String[] beanNames = (this.detectHandlersInAncestorContexts ? BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Object.class) : applicationContext.getBeanNamesForType(Object.class)); // Take any bean name that we can determine URLs for. // 遍歷beanNames,並找到這些bean 對應的url for (String beanName : beanNames) { // 找bean 上的所有url(Controller 上的url+方法上的url),該方法由對應的子類實現 String[] urls = determineUrlsForHandler(beanName); if (!ObjectUtils.isEmpty(urls)) { // URL paths found: Let's consider it a handler. // 保存urls 和beanName 的對應關系,put it to Map<urls,beanName>, // 該方法在父類AbstractUrlHandlerMapping 中實現 registerHandler(urls, beanName); } else { if (logger.isDebugEnabled()) { logger.debug("Rejected bean name '" + beanName + "': no URL paths identified"); } } } } /** 獲取Controller 中所有方法的url,由子類實現,典型的模板模式**/ protected abstract String[] determineUrlsForHandler(String beanName);
determineUrlsForHandler(String beanName)方法的作用是獲取每個Controller 中的url,不同的子類有不同的實現,這是一個典型的模板設計模式。因為開發中我們用的最多的就是用注解來配置Controller 中的url , BeanNameUrlHandlerMapping 是AbstractDetectingUrlHandlerMapping 的子類,處理注解形式的url 映射.所以我們這里以BeanNameUrlHandlerMapping 來進行分析。我們看BeanNameUrlHandlerMapping 是如何查beanName 上所有映射的url。
protected String[] determineUrlsForHandler(String beanName) { List<String> urls = new ArrayList<>(); if (beanName.startsWith("/")) { urls.add(beanName); } String[] aliases = obtainApplicationContext().getAliases(beanName); for (String alias : aliases) { if (alias.startsWith("/")) { urls.add(alias); } } return StringUtils.toStringArray(urls); }
到這里HandlerMapping 組件就已經建立所有url 和Controller 的對應關系。
運行調用階段
這一步步是由請求觸發的,所以入口為DispatcherServlet 的核心方法為doService(),doService()中的核心邏輯由doDispatch()實現,源代碼如下:
/** 中央控制器,控制請求的轉發**/ 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 { // 1.檢查是否是文件上傳的請求 processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. // 2.取得處理當前請求的Controller,這里也稱為hanlder,處理器, // 第一個步驟的意義就在這里體現了.這里並不是直接返回Controller, // 而是返回的HandlerExecutionChain 請求處理器鏈對象, // 該對象封裝了handler 和interceptors. mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } // Determine handler adapter for the current request. //3. 獲取處理request 的處理器適配器handler adapter HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Process last-modified header, if supported by the handler. // 處理last-modified 請求頭 String method = request.getMethod(); 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; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. // 4.實際的處理器處理請求,返回結果視圖對象 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } // 結果視圖對象的處理 applyDefaultViewName(processedRequest, mv); 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); }
//結果處理 調用view.render 方法做視圖解析 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()) { // Instead of postHandle and afterCompletion if (mappedHandler != null) { // 請求成功響應之后的方法 mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else { // Clean up any resources used by a multipart request. if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } }
getHandler(processedRequest)方法實際上就是從HandlerMapping 中找到url 和Controller 的對應關系。也就是Map<url,Controller>。我們知道,最終處理Request的是Controller 中的方法,我們現在只是知道了Controller,我們如何確認Controller中處理Request 的方法呢?繼續往下看。從Map<urls,beanName>中取得Controller 后,經過攔截器的預處理方法,再通過反射獲取該方法上的注解和參數,解析方法和參數上的注解,然后反射調用方法獲取ModelAndView 結果視圖。最后,調用的就是RequestMappingHandlerAdapter 的handle()中的核心邏輯由handleInternal(request, response, handler)實現。
/** 根據url 獲取處理請求的方法**/ protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { // 如果請求url 為,http://localhost:8080/web/hello.json, 則lookupPath=web/hello.json String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); if (logger.isDebugEnabled()) { logger.debug("Looking up handler method for path " + lookupPath); } this.mappingRegistry.acquireReadLock(); try { // 遍歷Controller 上的所有方法,獲取url 匹配的方法 HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); if (logger.isDebugEnabled()) { if (handlerMethod != null) { logger.debug("Returning handler method [" + handlerMethod + "]"); } else { logger.debug("Did not find handler method for [" + lookupPath + "]"); } } return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); } finally { this.mappingRegistry.releaseReadLock(); } }
通過上面的代碼分析,已經可以找到處理Request 的Controller 中的方法了,現在看如何解析該方法上的參數,並反射調用該方法。
/** 獲取處理請求的方法,執行並返回結果視圖**/ protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); try { WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) { invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } invocableMethod.setDataBinderFactory(binderFactory); invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request)); modelFactory.initModel(webRequest, mavContainer, invocableMethod); mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); asyncWebRequest.setTimeout(this.asyncRequestTimeout); WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); asyncManager.setTaskExecutor(this.taskExecutor); asyncManager.setAsyncWebRequest(asyncWebRequest); asyncManager.registerCallableInterceptors(this.callableInterceptors); asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors); if (asyncManager.hasConcurrentResult()) { Object result = asyncManager.getConcurrentResult(); mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0]; asyncManager.clearConcurrentResult(); if (logger.isDebugEnabled()) { logger.debug("Found concurrent result value [" + result + "]"); } invocableMethod = invocableMethod.wrapConcurrentResult(result); } invocableMethod.invokeAndHandle(webRequest, mavContainer); if (asyncManager.isConcurrentHandlingStarted()) { return null; } return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } }
invocableMethod.invokeAndHandle()最終要實現的目的就是:完成Request 中的參數和方法參數上數據的綁定。Spring MVC 中提供兩種Request 參數到方法中參數的綁定方式:
- 通過注解進行綁定,@RequestParam。
- 通過參數名稱進行綁定。
使用注解進行綁定,我們只要在方法參數前面聲明@RequestParam("name"),就可以將request 中參數name 的值綁定到方法的該參數上。使用參數名稱進行綁定的前提是必須要獲取方法中參數的名稱,Java 反射只提供了獲取方法的參數的類型,並沒有提供獲取參數名稱的方法。SpringMVC 解決這個問題的方法是用asm 框架讀取字節碼文件,來獲取方法的參數名稱。asm 框架是一個字節碼操作框架,關於asm 更多介紹可以參考其官網。個人建議,使用注解來完成參數綁定,這樣就可以省去asm 框架的讀取字節碼的操作。
到這里,方法的參數值列表也獲取到了,就可以直接進行方法的調用了。整個請求過程中最復雜的一步就是在這里了。到這里整個請求處理過程的關鍵步驟都已了解。理解了Spring MVC 中的請求處理流程,整個代碼還是比較清晰的。最后我們再來梳理一下Spring MVC 核心組件的關聯關系(如下圖):
時序圖:
Spring MVC 使用優化建議
上面我們已經對SpringMVC 的工作原理和源碼進行了分析,在這個過程發現了幾個優化點:
1、Controller 如果能保持單例,盡量使用單例這樣可以減少創建對象和回收對象的開銷。也就是說,如果Controller 的類變量和實例變量可以以方法形參聲明的盡量以方法的形參聲明,不要以類變量和實例變量聲明,這樣可以避免線程安全問題。
2、處理Request 的方法中的形參務必加上@RequestParam 注解這樣可以避免Spring MVC 使用asm 框架讀取class 文件獲取方法參數名的過程。即便Spring MVC 對讀取出的方法參數名進行了緩存,如果不要讀取class 文件當然是更好。
3、緩存URL,閱讀源碼的過程中,我們發現Spring MVC 並沒有對處理url 的方法進行緩存,也就是說每次都要根據請求url 去匹配Controller 中的方法url,如果把url 和Method 的關系緩存起來,會不會帶來性能上的提升呢?有點惡心的是,負責解析url 和Method 對應關系的ServletHandlerMethodResolver 是一個private 的內部類,不能直接繼承該類增強代碼,必須要該代碼后重新編譯。當然,如果緩存起來,必須要考慮緩存的線程安全問題。