首頁>技術>

經過前面的 AOP(面向切面程式設計) 和 Transaction(事務管理),這次來到了 MVC(Web 應用,進行請求分發和處理)

Spring MVC 定義:

分離了控制器(Controller)、模型(Model)、分配器(Adapter)、檢視(View)和處理程式物件(Handler,實際上呼叫的是 Controller 中定義的邏輯)。

基於 Servlet 功能實現,通過實現了 Servlet 介面的 DispatcherServlet 來封裝其核心功能實現,通過將請求分派給處理程式,同時帶有可配置的處理程式對映、檢視解析、本地語言、主題解析以及上傳檔案支援。

同樣老套路,本篇按照以下思路展開:

(1) 介紹如何使用

(2) 輔助工具類 ContextLoaderContext

(3) DispatcherServlet 初始化

(4) DispatcherServlet 處理請求

Table of Contents generated with DocToc

如何使用ContextLoaderContextDispatcherServlet 初始化容器初始化WebApplicationContext 的初始化根容器查詢根據 contextAttribute 尋找重新建立例項獲取上下文類 contextClassconfigureAndRefreshWebApplicationContextApplicationContextInitializer載入 Spring 配置註冊 mvc 解析器mvc 初始化預設策略multipartResolver 檔案上傳相關LocalResolver 與國際化相關ThemeResolver 主題更換相關HandlerMapping 與匹配處理器相關HandlerAdapter 介面卡HandlerExceptionResolver 處理器異常解決器RequestToViewNameTranslator 處理邏輯檢視名稱ViewResolver 檢視渲染FlashMapManager 儲存屬性RequestMappingHandlerMappingRegistryRequestMappingHandlerAdapterDispatcherServlet 的邏輯處理請求上下文請求分發 doDispatch尋找處理器 mappedHandler尋找介面卡 HandlerAdapter請求處理Session 程式碼塊自定義引數解析邏輯處理返回值解析檢視渲染render總結題外話參考資料如何使用

程式碼結構如下:(詳細程式碼可在文章末尾下載)

├── java│ ├── domains│ └── web│ └── controller│ └── BookController.java├── resources│ └── configs└── webapp│ └── WEB-INF│ ├── views│ │ ├── bookView.jsp│ │ └── index.jsp├── ├── applicationContext.xml│ ├── spring-mvc.xml│ └── web.xml└── build.gradle

(1)配置 web.xml

在該檔案中,主要配置了兩個關鍵點:

1. contextConfigLocation :使 Web 和 Spring 的配置檔案相結合的關鍵配置

2. DispatcherServlet : 包含了 SpringMVC 的請求邏輯,使用該類攔截 Web 請求並進行相應的邏輯處理

(2) 配置 applicationContext.xml

(3) 配置 spring-mvc.xml

(4) 建立 BookController

@Controllerpublic class BookController {@RequestMapping(value = "/", method = RequestMethod.GET)public String welcome() {return "index";}@RequestMapping(value = "bookView", method = RequestMethod.GET)public String helloView(Model model) {ComplexBook book1 = new ComplexBook("Spring 原始碼深度分析", "技術類");ComplexBook book2 = new ComplexBook("雪國", "文學類");List<ComplexBook> list = new ArrayList<>(2);list.add(book1);list.add(book2);model.addAttribute("bookList", list);return "bookView";}@RequestMapping(value = "plain")@ResponseBodypublic String plain(@PathVariable String name) {return name;}}

可以看出,與書中示例並不一樣,使用的是更貼合我們實際開發中用到的 @RequestMapping 等註解作為例子。根據請求的 URL 路徑,匹配到對應的方法進行處理。

(5) 建立 jsp 檔案

index.jsp<html><head> <title>Hello World!</title></head><body><h1>Hello JingQ!</h1></body></html>---bookView.jsp<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><%@ page contentType="text/html;charset=UTF-8" language="java" %><html><head> <title>Book Shop</title></head><body><c:forEach items="${bookList}" var="book"> <c:out value="${book.name}"/> <c:out value="${book.tag}"/></c:forEach></body></html>

按照現在前後端分離的大趨勢,我其實並不想用 jsp 檢視技術作為例子,但考慮到之前入門時也接觸過,也為了跟我一樣不會寫前端的同學更好理解,所以還是記錄一下如何使用 jsp。

(6) 新增依賴 build.gradle

// 引入 spring-web 和 spring-webmvc,如果不是跟我一樣使用原始碼進行編譯,請到 mvn 倉庫中尋找對應依賴optional(project(":spring-web"))optional(project(":spring-webmvc"))// 引入這個依賴,使用 jsp 語法 https://mvnrepository.com/artifact/javax.servlet/jstlcompile group: 'javax.servlet', name: 'jstl', version: '1.2'

(7) 啟動 Tomcat 如何配置和啟動,網上也有很多例子,參考資料 3 是個不錯的例子,下面是請求處理結果:

http://localhost:8080/bookView (使用了 JSP 檢視進行渲染)

http://localhost:8080/plain/value (前後端分離的話,常用的是這種,最後可以返回簡單字元或者 json 格式的物件等)

在剛才的 web.xml 中有兩個關鍵配置,所以現在學習下這兩個配置具體是幹啥的。

ContextLoaderContext

作用:在啟動 web 容器時,自動裝載 ApplicationContext 的配置資訊。

下面是它的繼承體系圖:

這是一個輔助工具類,可以用來傳遞配置資訊引數,在 web.xml 中,將路徑以 context-param 的方式註冊並使用 ContextLoaderListener 進行監聽讀取。

從圖中能看出,它實現了 ServletContextListener 這個介面,只要在 web.xml 配置了這個監聽器,容器在啟動時,就會執行 contextInitialized(ServletContextEvent) 這個方法,進行應用上下文初始化。

public void contextInitialized(ServletContextEvent event) {initWebApplicationContext(event.getServletContext());}

每個 Web 應用都會有一個 ServletContext 貫穿生命週期(在應用啟動時建立,關閉時銷燬),跟 Spring 中 ApplicationContext 類似,在全域性範圍內有效。

實際上初始化的工作,是由父類 ContextLoader 完成的:(簡略版)

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { // demo 中用到的根容器是 Spring 容器 WebApplicationContext.class.getName() + ".ROOT"if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {// web.xml 中存在多次 ContextLoader 定義throw new IllegalStateException();}long startTime = System.currentTimeMillis(); // 將上下文儲存在本地例項變數中,以保證在 ServletContext 關閉時可用。 if (this.context == null) { // 初始化 context this.context = createWebApplicationContext(servletContext); } if (this.context instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context; if (!cwac.isActive()) { if (cwac.getParent() == null) { ApplicationContext parent = loadParentContext(servletContext); cwac.setParent(parent); } configureAndRefreshWebApplicationContext(cwac, servletContext); } } // 記錄在 ServletContext 中 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); ClassLoader ccl = Thread.currentThread().getContextClassLoader(); if (ccl == ContextLoader.class.getClassLoader()) { currentContext = this.context; } else if (ccl != null) { currentContextPerThread.put(ccl, this.context); } if (logger.isInfoEnabled()) { // 計數器,計算初始化耗時時間 long elapsedTime = System.currentTimeMillis() - startTime; logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms"); } return this.context;}

該函式主要是體現了建立 WebApplicationContext 例項的一個功能架構,實現的大致步驟如下:

1. WebApplicationContext 存在性的驗證: 只能初始化一次,如果有多個宣告,將會擾亂 Spring 的執行邏輯,所以有多個宣告將會報錯。 2. 建立 WebApplicationContext 例項: createWebApplicationContext(servletContext);

protected Class<?> determineContextClass(ServletContext servletContext) { // defaultStrategies 是個靜態變數,在靜態程式碼塊中初始化 contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());}/** * 預設策略 */private static final Properties defaultStrategies;static {try {// 從 ContextLoader.properties 檔案中載入預設策略// 在這個目錄下:org/springframework/web/context/ContextLoader.propertiesClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);}catch (IOException ex) {throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());}}org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

如果按照預設策略,它將會從配置檔案 ContextLoader.properties 中讀取需要建立的實現類:XmlWebApplicationContext

3. 將例項記錄在 servletContext 中 4. 對映當前的類載入器與建立的例項到全域性變數 currentContextPerThread 中

通過以上步驟,完成了建立 WebApplicationContext 例項,它繼承自 ApplicaitonContext,在父類的基礎上,追加了一些特定於 web 的操作和屬性,可以把它當成我們之前初始化 Spring 容器時所用到的 ClassPathApplicaitonContext 那樣使用。

DispatcherServlet 初始化

該類是 spring-mvc 的核心,該類進行真正邏輯實現,DisptacherServlet 實現了 Servlet 介面。

介紹:

servlet 是一個 Java 編寫的程式,基於 Http 協議,例如我們常用的 Tomcat,也是按照 servlet 規範編寫的一個 Java 類

servlet 的生命週期是由 servlet 的容器來控制,分為三個階段:初始化、執行和銷燬。

在 servlet 初始化階段會呼叫其 init 方法:

HttpServletBean#init

public final void init() throws ServletException {// 解析 init-param 並封裝到 pvs 變數中PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);// 將當前的這個 Servlet 類轉換為一個 BeanWrapper,從而能夠以 Spring 的方式對 init—param 的值注入BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());// 註冊自定義屬性編輯器,一旦遇到 Resource 型別的屬性將會使用 ResourceEditor 進行解析bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));// 空實現,留給子類覆蓋initBeanWrapper(bw);bw.setPropertyValues(pvs, true);// 初始化 servletBean (讓子類實現,這裡它的實現子類是 FrameworkServlet)initServletBean();}

在這裡初始化 DispatcherServlet,主要是通過將當前的 servlet 型別例項轉換為 BeanWrapper 型別例項,以便使用 Spring 中提供的注入功能進行相應屬性的注入。

從上面註釋,可以看出初始化函式的邏輯比較清晰,封裝引數、轉換成 BeanWrapper 例項、註冊自定義屬性編輯器、屬性注入,以及關鍵的初始化 servletBean。

容器初始化

下面看下初始化關鍵邏輯:

FrameworkServlet#initServletBean

剝離了日誌列印後,剩下的兩行關鍵程式碼

protected final void initServletBean() throws ServletException {// 僅剩的兩行關鍵程式碼this.webApplicationContext = initWebApplicationContext();// 留給子類進行覆蓋實現,但我們例子中用的 DispatcherServlet 並沒有覆蓋,所以先不用管它initFrameworkServlet();}WebApplicationContext 的初始化

FrameworkServlet#initWebApplicationContext

該函式的主要工作就是建立或重新整理 WebApplicationContext 例項並對 servlet 功能所使用的變數進行初始化。

protected WebApplicationContext initWebApplicationContext() {// 從根容器開始查詢WebApplicationContext rootContext =WebApplicationContextUtils.getWebApplicationContext(getServletContext());WebApplicationContext wac = null;if (this.webApplicationContext != null) {// 有可能在 Spring 載入 bean 時,DispatcherServlet 作為 bean 載入進來了// 直接使用在建構函式被注入的 context 例項wac = this.webApplicationContext;if (wac instanceof ConfigurableWebApplicationContext) {ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;if (!cwac.isActive()) {if (cwac.getParent() == null) {cwac.setParent(rootContext);}// 重新整理上下文環境configureAndRefreshWebApplicationContext(cwac);}}}if (wac == null) {// 根據 contextAttribute 屬性載入 WebApplicationContextwac = findWebApplicationContext();}if (wac == null) {// 經過上面步驟都沒找到,那就來建立一個wac = createWebApplicationContext(rootContext);}if (!this.refreshEventReceived) {synchronized (this.onRefreshMonitor) {// 重新整理,初始化很多策略方法onRefresh(wac);}}if (this.publishContext) {// Publish the context as a servlet context attribute.String attrName = getServletContextAttributeName();getServletContext().setAttribute(attrName, wac);}return wac;}根容器查詢

我們最常用到的 spring-mvc,是 spring 容器和 web 容器共存,這時 rootContext 父容器就是 spring 容器。

在前面的 web.xml 配置的監聽器 ContextLaoderListener,已經將 Spring 父容器進行了載入

WebApplicationContextUtils#getWebApplicationContext(ServletContext)

public static WebApplicationContext getWebApplicationContext(ServletContext sc) {// key 值 :WebApplicationContext.class.getName() + ".ROOT"// (ServletContext) sc.getAttribute(attrName) ,return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);}

同時,根據上面程式碼,了解到 Spring 父容器,是以 key 值為 : WebApplicationContext.class.getName() + ".ROOT" 儲存到 ServletContext 上下文中。

根據 contextAttribute 尋找

雖然有預設 key,但使用者可以重寫初始化邏輯(在 web.xml 檔案中設定 servlet 引數 contextAttribute),使用自己建立的 WebApplicaitonContext,並在 servlet 的配置中通過初始化引數 contextAttribute 指定 key。

protected WebApplicationContext findWebApplicationContext() {String attrName = getContextAttribute();if (attrName == null) {return null;}// attrName 就是使用者在`web.xml` 檔案中設定的 `servlet` 引數 `contextAttribute`WebApplicationContext wac =WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);if (wac == null) {throw new IllegalStateException("No WebApplicationContext found: initializer not registered?");}return wac;}重新建立例項

通過前面的方法都沒找到,那就來重新建立一個新的例項:

FrameworkServlet#createWebApplicationContext(WebApplicationContext)

protected WebApplicationContext createWebApplicationContext(@Nullable WebApplicationContext parent) {return createWebApplicationContext((ApplicationContext) parent);}protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {// 允許我們自定義容器的型別,通過 contextClass 屬性進行配置// 但是型別必須要繼承 ConfigurableWebApplicationContext,不然將會報錯Class<?> contextClass = getContextClass();if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {throw new ApplicationContextException();}// 通過反射來建立 contextClassConfigurableWebApplicationContext wac =(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);wac.setEnvironment(getEnvironment());wac.setParent(parent);// 獲取 contextConfigLocation 屬性,配置在 servlet 初始化函式中String configLocation = getContextConfigLocation(); wac.setConfigLocation(configLocation);// 初始化 Spring 環境包括載入配置環境configureAndRefreshWebApplicationContext(wac);return wac;}獲取上下文類 contextClass

預設使用的是 XmlWebApplicationContext,但如果需要配置自定義上下文,可以在 web.xml 中的 <init-param> 標籤中修改 contextClass 屬性對應的 value

configureAndRefreshWebApplicationContext

使用該方法,用來對已經建立的 WebApplicaitonContext 進行配置以及重新整理

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) { // 遍歷 ApplicationContextInitializer,執行 initialize 方法applyInitializers(wac);// 關鍵的重新整理,載入配置檔案及整合 parent 到 wacwac.refresh();}ApplicationContextInitializer

該類可以通過 <init-param> 的 contextInitializerClasses 進行自定義配置:

<init-param> <param-name>contextInitializerClasses</param-name> <param-value>自定義類,需繼承於 `ApplicationContextInitializer`</param-value></init-param>

正如程式碼中的順序一樣,是在 mvc 容器建立前,執行它的 void initialize(C applicationContext) 方法:

protected void applyInitializers(ConfigurableApplicationContext wac) { AnnotationAwareOrderComparator.sort(this.contextInitializers);for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {initializer.initialize(wac);}}

所有如果沒有配置的話,預設情況下 contextInitializers 列表為空,表示沒有 ApplicationContextInitializer 需要執行。

載入 Spring 配置

wac.refresh(),實際呼叫的是我們之前就很熟悉的重新整理方法:

org.springframework.context.support.AbstractApplicationContext#refresh

從圖中能夠看出,重新整理方法的程式碼邏輯與之前一樣,通過父類 AbstractApplicationContext 的 refresh 方法,進行了配置檔案的載入。

在例子中的 web.xml 配置中,指定了載入 spring-mvc.xml 配置檔案

在 spring-mvc.xml 配置中,主要配置了三項

org.springframework.web.servlet.config.MvcNamespaceHandler

public void init() {// MVC 標籤解析需要註冊的解析器registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());registerBeanDefinitionParser("default-servlet-handler", new DefaultServletHandlerBeanDefinitionParser());registerBeanDefinitionParser("interceptors", new InterceptorsBeanDefinitionParser());registerBeanDefinitionParser("resources", new ResourcesBeanDefinitionParser());registerBeanDefinitionParser("view-controller", new ViewControllerBeanDefinitionParser());registerBeanDefinitionParser("redirect-view-controller", new ViewControllerBeanDefinitionParser());registerBeanDefinitionParser("status-controller", new ViewControllerBeanDefinitionParser());registerBeanDefinitionParser("view-resolvers", new ViewResolversBeanDefinitionParser());registerBeanDefinitionParser("tiles-configurer", new TilesConfigurerBeanDefinitionParser());registerBeanDefinitionParser("freemarker-configurer", new FreeMarkerConfigurerBeanDefinitionParser());registerBeanDefinitionParser("groovy-configurer", new GroovyMarkupConfigurerBeanDefinitionParser());registerBeanDefinitionParser("script-template-configurer", new ScriptTemplateConfigurerBeanDefinitionParser());registerBeanDefinitionParser("cors", new CorsBeanDefinitionParser());}

可以看到,mvc 提供了很多便利的註解,有攔截器、資源、檢視等解析器,但我們常用的到的是 anntation-driven 註解驅動,這個註解通過 AnnotationDrivenBeanDefinitionParser 類進行解析,其中會註冊兩個重要的 bean :

class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {public static final String HANDLER_MAPPING_BEAN_NAME = RequestMappingHandlerMapping.class.getName();public static final String HANDLER_ADAPTER_BEAN_NAME = RequestMappingHandlerAdapter.class.getName();...}

跳過其他熟悉的 Spring 初始化配置,通過上面的步驟,完成了 Spring 配置檔案的解析,將掃描到的 bean 載入到了 Spring 容器中。

那麼下面就正式進入 mvc 的初始化。

mvc 初始化

onRefresh 方法是 FrameworkServlet 類中提供的模板方法,在子類 DispatcherServlet 進行了重寫,主要用來重新整理 Spring 在 Web 功能實現中所必須用到的全域性變數:

protected void onRefresh(ApplicationContext context) {initStrategies(context);}protected void initStrategies(ApplicationContext context) {// 初始化 multipartResolver 檔案上傳相關initMultipartResolver(context);// 初始化 LocalResolver 與國際化相關initLocaleResolver(context);// 初始化 ThemeResolver 與主題更換相關initThemeResolver(context);// 初始化 HandlerMapping 與匹配處理器相關initHandlerMappings(context);// 初始化 HandlerAdapter 處理當前 Http 請求的處理器介面卡實現,根據處理器對映返回相應的處理器型別initHandlerAdapters(context);// 初始化 HandlerExceptionResolvers,處理器異常解決器initHandlerExceptionResolvers(context);// 初始化 RequestToViewNameTranslator,處理邏輯檢視名稱initRequestToViewNameTranslator(context);// 初始化 ViewResolver 選擇合適的檢視進行渲染initViewResolvers(context);// 初始化 FlashMapManager 使用 flash attributes 提供了一個請求儲存屬性,可供其他請求使用(重定向時常用)initFlashMapManager(context);}

該函式是實現 mvc 的關鍵所在,先來大致介紹一下初始化的套路:

尋找使用者自定義配置沒有找到,使用預設配置

顯然,Spring 給我們提供了高度的自定義,可以手動設定想要的解析器,以便於擴充套件功能。

如果沒有找到使用者配置的 bean,那麼它將會使用預設的初始化策略: getDefaultStrategies 方法

預設策略

DispatcherServlet#getDefaultStrategies(縮減版)

protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {// 策略介面名稱String key = strategyInterface.getName();// 預設策略列表String value = defaultStrategies.getProperty(key);String[] classNames = StringUtils.commaDelimitedListToStringArray(value);List<T> strategies = new ArrayList<>(classNames.length);for (String className : classNames) {// 例項化Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());Object strategy = createDefaultStrategy(context, clazz);strategies.add((T) strategy);}return strategies;}// 預設策略列表private static final Properties defaultStrategies;static {// 路徑名稱是:DispatcherServlet.propertiestry {ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);}}

從靜態預設策略屬性 defaultStrategies 的載入過程中,讀取的是 DispatcherServlet.properties 檔案內容,看完下面列出來的資訊,相信你跟我一樣恍然大悟,了解 Spring 配置了哪些預設策略:

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolverorg.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolverorg.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\\org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\\org.springframework.web.servlet.function.support.RouterFunctionMappingorg.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\\org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\\org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\\org.springframework.web.servlet.function.support.HandlerFunctionAdapterorg.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\\org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\\org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolverorg.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslatororg.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolverorg.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

接下來看看它們各自的初始化過程以及使用場景:

multipartResolver 檔案上傳相關private void initMultipartResolver(ApplicationContext context) { try {this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);catch (NoSuchBeanDefinitionException ex) {// Default is no multipart resolver.this.multipartResolver = null; }}

預設情況下,Spring 是沒有 mulitpart 處理,需要自己設定

LocalResolver 與國際化相關

LocalResolver 介面定義了如何獲取客戶端的地區

當然還有其他兩種,基於 session 和基於 cookie 的配置,想要深入了解的可以去細看~

ThemeResolver 主題更換相關

主題是一組靜態資源(例如樣式表 css 和圖片 image),也可以理解為應用面板,使用 Theme 更改主題風格,改善使用者體驗。

預設註冊的 id 是 themeResolver,型別是 FixedThemeResolver,表示使用的是一個固定的主題,以下是它的繼承體系圖:

工作原理是通過攔截器攔截,配置對應的主題解析器,然後返回主題名稱,還是使用上面的解析器作為例子:

FixedThemeResolver#resolveThemeName

public String resolveThemeName(HttpServletRequest request) {return getDefaultThemeName();}public String getDefaultThemeName() {return this.defaultThemeName;}HandlerMapping 與匹配處理器相關

首先判斷 detectAllHandlerMappings 變數是否為 true,表示是否需要載入容器中所有的 HandlerMapping,false 將會載入使用者配置的。

如註釋所說,至少得保證有一個 HandlerMapping,如果前面兩個分支都沒尋找到,那麼就進行預設策略載入。

private void initHandlerMappings(ApplicationContext context) {this.handlerMappings = null;if (this.detectAllHandlerMappings) {// 預設情況下,尋找應用中所有的 HandlerMapping ,包括祖先容器(其實就是 Spring 容器啦)Map<String, HandlerMapping> matchingBeans =BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);if (!matchingBeans.isEmpty()) {this.handlerMappings = new ArrayList<>(matchingBeans.values());// handlerMapping 有優先順序,需要排序AnnotationAwareOrderComparator.sort(this.handlerMappings);}}else {// 從上下文中,獲取名稱為 handlerMapping 的 beanHandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);this.handlerMappings = Collections.singletonList(hm);}// 需要保證,至少有一個 HandlerMapping// 如果前面兩步都沒找到 mapping,將會由這裡載入預設策略if (this.handlerMappings == null) {this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);}}

通過 Debug 得知,之前在載入 Spring 配置時,就已經注入了 RequestMappingHandlerMapping 和 BeanNameUrlHandlerMapping

HandlerAdapter 介面卡

套路與前面的一樣,使用的預設策略是:HttpRequestHandlerAdapter 、SimpleControllerHandlerAdapter、 RequestMappingHandlerAdapter 和 HandlerFunctionAdapter。

說到介面卡,可以將它理解為,將一個類的介面適配成使用者所期待的,將兩個介面不相容的工作類,通過介面卡連線起來。

HandlerExceptionResolver 處理器異常解決器

套路也與前面一樣,使用的預設策略是:ExceptionHandlerExceptionResolver、 ResponseStatusExceptionResolver 和 DefaultHandlerExceptionResolver。

實現了 HandlerExceptionResolver 介面的 resolveException 方法,在方法內部對異常進行判斷,然後嘗試生成 ModelAndView 返回。

public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {if (shouldApplyTo(request, handler)) {prepareResponse(ex, response);ModelAndView result = doResolveException(request, response, handler, ex);return result;}else {return null;}}RequestToViewNameTranslator 處理邏輯檢視名稱

初始化程式碼邏輯與前面一樣,使用的預設策略是:DefaultRequestToViewNameTranslator

使用場景:當 Controller 處理器方法沒有返回邏輯檢視名稱時,Spring 通過該類的約定,提供一個邏輯檢視名稱。

由於本地測試不出來,所以引用參考資料 7 的例子:

DefaultRequestToViewNameTranslator的轉換例子:

http://localhost:8080/gamecast/display.html -> display(檢視)

ViewResolver 檢視渲染

套路還是跟前面一樣,預設策略使用的是:InternalResourceViewResolver

同時,這也是 demo 中,我們手動配置的檢視解析器

FlashMapManager 儲存屬性

預設使用的是:SessionFlashMapManager,通過與 FlashMap 配合使用,用於在重定向時儲存/傳遞引數。

例如 Post/Redirect/Get 模式,Flash attribute 在重定向之前暫存(根據類名,可以知道範圍是 session 級別有效),以便重定向之後還能使用。

RequestMappingHandler

該類作用:配合 @Controller 和 @RequestMapping 註解使用,通過 URL 來找到對應的處理器。

前面在 spring-mvc.xml 檔案載入時,初始化了兩個重要配置,其中一個就是下面要說的 RequestMappingHandler,先來看它的繼承體系圖:

從繼承圖中看到,它實現了 InitializingBean 介面,所以在初始化時,將會執行 afterPropertiesSet 方法(圖片中註釋寫錯方法,請以下面為準),核心呼叫的初始化方法是父類 AbstractHandlerMethodMapping#initHandlerMethods 方法

AbstractHandlerMethodMapping#initHandlerMethods

protected void initHandlerMethods() {// 獲取容器中所有 bean 名字for (String beanName : this.detectHandlerMethodsInAncestorContexts ?BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :obtainApplicationContext().getBeanNamesForType(Object.class)) {if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { // 如果字首不是 scopedTarget. // 執行 detectHandlerMethods() 方法Class<?> beanType = obtainApplicationContext().getType(beanName);if (beanType != null && isHandler(beanType)) { detectHandlerMethods(beanName); }}}// 列印數量,可以當成空實現handlerMethodsInitialized(getHandlerMethods());}protected void detectHandlerMethods(Object handler) {Class<?> handlerType = (handler instanceof String ?obtainApplicationContext().getType((String) handler) : handler.getClass());if (handlerType != null) {Class<?> userType = ClassUtils.getUserClass(handlerType);// 通過反射,獲取類中所有方法// 篩選出 public 型別,並且帶有 @RequestMapping 註解的方法Map<Method, T> methods = MethodIntrospector.selectMethods(userType,(MethodIntrospector.MetadataLookup<T>) method -> {// 通過 RequestMappingHandlerMapping.getMappingForMethod 方法組裝成 RequestMappingInfo(對映關係)return getMappingForMethod(method, userType);});methods.forEach((method, mapping) -> {Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);// 通過 mappingRegistry 進行註冊上面獲取到的對映關係registerHandlerMethod(handler, invocableMethod, mapping);});}}

梳理一下程式碼邏輯,initHandlerMethods 方法將會掃描註冊 bean 下所有公共 public 方法,如果帶有 @RequestMapping 註解的,將會組裝成 RequestMappingInfo 對映關係,然後將它註冊到 mappingRegistry 變數中。之後可以通過對映關係,輸入 URL 就能夠找到對應的處理器 Controller。

MappingRegistry

該類是 AbstractHandlerMethodMapping 的內部類,是個工具類,用來儲存所有 Mapping 和 handler method,通過暴露加鎖的公共方法,避免了多執行緒對該類的內部變數的覆蓋修改。

下面是註冊的邏輯:

public void register(T mapping, Object handler, Method method) {this.readWriteLock.writeLock().lock();try {// 包裝 bean 和方法HandlerMethod handlerMethod = createHandlerMethod(handler, method);// 校驗validateMethodMapping(handlerMethod, mapping);this.mappingLookup.put(mapping, handlerMethod);List<String> directUrls = getDirectUrls(mapping);for (String url : directUrls) {this.urlLookup.add(url, mapping);}String name = null;if (getNamingStrategy() != null) {name = getNamingStrategy().getName(handlerMethod, mapping);addMappingName(name, handlerMethod);}// 跨域引數CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);if (corsConfig != null) {this.corsLookup.put(handlerMethod, corsConfig);}// 將對映關係放入 Map<T, MappingRegistration<T>> registrythis.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));}finally {this.readWriteLock.writeLock().unlock();}}

通過前面的包裝和校驗方法,最後對映關係將會放入這裡 Map<T, MappingRegistration<T>> registry。它是一個泛型的 Map,key 型別是 RequestMappingInfo,儲存了 @RequestMapping 各種屬性的集合,value 型別是 AbstractHandlerMethodMapping,儲存的是我們的對映關係。

從圖中可以看出,如果輸入的 URL 是 /plain/{name},將會找到對應的處理方法 web.controller.BookController#plain{String}。

RequestMappingHandlerAdapter

而另一個重要的配置就是處理器介面卡 RequestMappingHandlerAdapter,由於它的繼承體系與 RequestMappingHandler 類似,所以我們直接來看它在載入時執行的方法

RequestMappingHandlerAdapter#afterPropertiesSet

public void afterPropertiesSet() {// 首先執行這個方法,可以新增 responseBody 切面 beaninitControllerAdviceCache();// 引數處理器if (this.argumentResolvers == null) {List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);}// 處理 initBinder 註解if (this.initBinderArgumentResolvers == null) {List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);}// 初始化結果處理器if (this.returnValueHandlers == null) {List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);}}

所以看到這個介面卡中,初始化了很多工具變數,用來處理 @ControllerAdvice 、InitBinder 等註解和引數。不過核心還是待會要講到的 handleInternal() 方法,它將適配處理器呼叫,然後返回 ModelView 檢視。

DispatcherServlet 的邏輯處理

請求處理的入口定義在 HttpServlet,主要有以下幾個方法:

當然,父類 HttpServlet 只是給出了定義,直接呼叫父類這些方法將會報錯,所以 FrameworkServlet 將它們覆蓋重寫了處理邏輯:

protected final void doGet(HttpServletRequest request, HttpServletResponse response) {// 註解 10. 具體呼叫的是 processRequest 方法processRequest(request, response);}protected final void doPost(HttpServletRequest request, HttpServletResponse response) {processRequest(request, response);}

可以看到 doGet 、doPost 這些方法,底層呼叫的都是 processRequest 方法進行處理,關鍵方法是委託給子類 DispatcherServlet 的 doServie() 方法

DispatcherServlet#doService

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {logRequest(request);// 暫存請求引數Map<String, Object> attributesSnapshot = null;...// 經過前面的準備(屬性、輔助變數),進入請求處理過程doDispatch(request, response);}

請求分發和處理邏輯的核心是在 doDispatch(request, response) 方法中,在進入這個方法前,還有些準備工作需要執行。

請求上下文

在 processRequest 的 doServie() 方法執行前,主要做了這以下準備工作:

(1) 為了保證當前執行緒的 LocaleContext 以及 RequestAttributes 可以在當前請求後還能恢復,提取當前執行緒的兩個屬性。 (2) 根據當前 request 建立對應的 LocaleContext 以及 RequestAttributes,繫結到當前執行緒 (3) 往 request 物件中設定之前載入過的 localeResolver、flashMapManager 等輔助工具變數

請求分發 doDispatch

經過前面的配置設定,doDispatch 函式展示了請求的完成處理過程:

DispatcherServlet#doDispatch

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;// 註釋 10. 檢查是否 MultipartContent 型別processedRequest = checkMultipart(request);// 根據 request 資訊尋找對應的 HandlermappedHandler = getHandler(processedRequest);if (mappedHandler == null) {// 沒有找到 handler,通過 response 向用戶返回錯誤資訊noHandlerFound(processedRequest, response);return;}// 根據當前的 handler 找到對應的 HandlerAdapter 介面卡HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());// 如果當前 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 (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {return;}}// 攔截器的 preHandler 方法的呼叫if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}// 真正啟用 handler 進行處理,並返回檢視mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}// 檢視名稱轉換(有可能需要加上前後綴)applyDefaultViewName(processedRequest, mv);// 應用所有攔截器的 postHandle 方法mappedHandler.applyPostHandle(processedRequest, response, mv);// 處理分發的結果(如果有 mv,進行檢視渲染和跳轉)processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);}

上面貼出來的程式碼略有縮減,不過從上面示例中能看出,整體的邏輯都挺清晰的,主要步驟如下:

1. 尋找處理器 mappedandler 2. 根據處理器,尋找對應的介面卡 HandlerAdapter 3. 啟用 handler,呼叫處理方法 4. 返回結果(如果有 mv,進行檢視渲染和跳轉)

尋找處理器 mappedHandler

以 demo 說明,尋找處理器,就是根據 URL 找到對應的 Controller 方法

DispatcherServlet#getHandler

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {if (this.handlerMappings != null) {// 遍歷註冊的全部 handlerMappingfor (HandlerMapping mapping : this.handlerMappings) {HandlerExecutionChain handler = mapping.getHandler(request);if (handler != null) {return handler;}}}return null;}

實際上,在這一步遍歷了所有註冊的 HandlerMapping,然後委派它們去尋找處理器,如果找到了合適的,就不再往下尋找,直接返回。

同時,HandlerMapping 之間有優先順序的概念,根據 mvc 包下 AnnotationDrivenBeanDefinitionParser 的註釋:

This class registers the following {@link HandlerMapping HandlerMappings} @link RequestMappingHandlerMapping ordered at 0 for mapping requests to annotated controller methods.

說明了 RequestMappingHandlerMapping 的優先順序是最高的,優先使用它來尋找介面卡。

具體尋找呼叫的方法:

AbstractHandlerMapping#getHandler

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {// 根據 Request 獲取對應的 handlerObject handler = getHandlerInternal(request);// 將配置中的對應攔截器加入到執行鏈中,以保證這些攔截器可以有效地作用於目標物件HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);if (hasCorsConfigurationSource(handler)) {CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null);CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);config = (config != null ? config.combine(handlerConfig) : handlerConfig);executionChain = getCorsHandlerExecutionChain(request, executionChain, config);}return executionChain;}

(1) getHandlerInternal(request) 函式作用:

根據 request 資訊獲取對應的 Handler,也就是我們例子中的,通過 URL 找到匹配的 Controller 並返回。

(2) getHandlerExcetionChain 函式作用:

將適應該 URL 對應攔截器 MappedInterceptor 加入 addInterceptor() 到執行鏈 HandlerExecutionChain 中。

(3) CorsConfiguration

這個引數涉及到跨域設定,具體看下這篇文章:SpringBoot下如何配置實現跨域請求?

尋找介面卡 HandlerAdapter

前面已經找到了對應的處理器了,下一步就得找到它對應的介面卡

DispatcherServlet#getHandlerAdapter

protected getHandlerAdapter(Object handler) throws ServletException {if (this.handlerAdapters != null) {for (HandlerAdapter adapter : this.handlerAdapters) {if (adapter.supports(handler)) {return adapter;}}}}

同樣,HandlerAdapter 之間也有優先順序概念,由於第 0 位是 RequestMappingHandlerAdapter,而它的 supports 方法總是返回 true,所以毫無疑問返回了它

請求處理

通過介面卡包裝了一層,處理請求的入口如下:

RequestMappingHandlerAdapter#handleInternal

protected ModelAndView handleInternal(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ModelAndView mav;checkRequest(request);// Execute invokeHandlerMethod in synchronized block if required.if (this.synchronizeOnSession) {HttpSession session = request.getSession(false);if (session != null) {Object mutex = WebUtils.getSessionMutex(session);synchronized (mutex) {mav = invokeHandlerMethod(request, response, handlerMethod);}}else {// No HttpSession available -> no mutex necessarymav = invokeHandlerMethod(request, response, handlerMethod);}}else {// No synchronization on session demanded at all...// 執行適配中真正的方法mav = invokeHandlerMethod(request, response, handlerMethod);}if (!response.containsHeader(HEADER_CACHE_CONTROL)) {if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);}else {prepareResponse(response);}}return mav;}

通過 invokeHandlerMethod 方法,呼叫對應的 Controller 方法邏輯,包裝成 ModelAndView。

Session 程式碼塊

判斷 synchronizeOnSession 是否開啟,開啟的話,同一個 session 的請求將會序列執行(Object mutex = WebUtils.getSessionMutex(session))

自定義引數解析

解析邏輯由 RequestParamMethodArgumentResolver 完成,具體請檢視 spring-mvc

邏輯處理

InvocableHandlerMethod#invokeForRequest

public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);return doInvoke(args);}

通過給定的引數,doInvoke 使用了反射操作,執行了 Controller 方法的邏輯。

返回值解析

拿 http://localhost:8080/bookView 作為例子,經過前面的邏輯處理後,返回的只是試圖名稱 bookView,在這時,使用到了 ViewNameMethodReturnValueHandler

可以看到它實現了 HandlerMethodReturnValueHandler 介面的兩個方法

ViewNameMethodReturnValueHandler#supportsReturnType; 表示支援處理的返回型別

public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {if (returnValue instanceof CharSequence) {String viewName = returnValue.toString();mavContainer.setViewName(viewName);if (isRedirectViewName(viewName)) {mavContainer.setRedirectModelScenario(true);}}}

ViewNameMethodReturnValueHandler#handleReturnValue; 返回處理值,給 mavContainer 設定檢視名稱 viewName

public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {if (returnValue instanceof CharSequence) {String viewName = returnValue.toString();mavContainer.setViewName(viewName);if (isRedirectViewName(viewName)) {mavContainer.setRedirectModelScenario(true);}}}

最後在介面卡中包裝成了 ModelAndView 物件


檢視渲染

根據處理器執行完成後,介面卡包裝成了 ModelAndView 返回給 DispatcherServlet 繼續進行處理,來到了檢視渲染的步驟:

DispatcherServlet#processDispatchResult

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,@Nullable Exception exception) throws Exception {boolean errorView = false;// 跳過了異常判斷 =-=// Did the handler return a view to render?if (mv != null && !mv.wasCleared()) {// 如果檢視不為空並且 clear 屬性為 false, 進行檢視渲染render(mv, request, response);if (errorView) {WebUtils.clearErrorRequestAttributes(request);}}if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {// Concurrent handling started during a forwardreturn;}if (mappedHandler != null) {mappedHandler.triggerAfterCompletion(request, response, null);}}

render

還記得我們使用的是 jsp 檢視進行渲染麼,引用的依賴是 jstl,所以檢視渲染的是 JstlView 類提供的方法,以下是它的繼承體系:

渲染呼叫的是其父類的方法:

InternalResourceView#renderMergedOutputModel

在給定指定模型的情況下呈現內部資源。這包括將模型設定為請求屬性

protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {// Expose the model object as request attributes.exposeModelAsRequestAttributes(model, request);// Expose helpers as request attributes, if any.exposeHelpers(request);// Determine the path for the request dispatcher.String dispatcherPath = prepareForRendering(request, response);// Obtain a RequestDispatcher for the target resource (typically a JSP).RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);if (rd == null) {throw new ServletException("");}// If already included or response already committed, perform include, else forward.if (useInclude(request, response)) {response.setContentType(getContentType());rd.include(request, response);}else {// Note: The forwarded resource is supposed to determine the content type itself.rd.forward(request, response);}}

最後發現渲染呼叫的是第三方依賴 org.apache.catalina.core.ApplicationDispatcher 進行檢視繪製,所以不再跟蹤下去。

所以整個檢視渲染過程,就是在前面將 Model 檢視物件中的屬性設定到請求 request 中,最後通過原生(tomcat)的 ApplicationDispatcher 進行轉發,渲染成檢視。


總結

本篇比較完整的描述了 spring-mvc 的框架體系,結合 demo 和程式碼,將呼叫鏈路梳理了一遍,了解了每個環節註冊的工具類或解析器,了解了 Spring 容器和 Web 容器是如何合併使用,也了解到 mvc 初始化時載入的預設策略和請求完整的處理邏輯。

總結起來,就是我們在開頭寫下的內容:

(1) 介紹如何使用

(2) 輔助工具類 ContextLoaderContext

(3) DispatcherServlet 初始化

(4) DispatcherServlet 處理請求

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 「安卓逆向」xx招聘app登陸協議實戰簡單分析