經過前面的 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
- 如何使用
- ContextLoaderContext
- DispatcherServlet 初始化容器初始化WebApplicationContext 的初始化根容器查找根據 contextAttribute 尋找重新創建實例獲取上下文類 contextClassconfigureAndRefreshWebApplicationContextApplicationContextInitializer加載 Spring 配置注冊 mvc 解析器mvc 初始化默認策略multipartResolver 文件上傳相關LocalResolver 與國際化相關ThemeResolver 主題更換相關HandlerMapping 與匹配處理器相關HandlerAdapter 適配器HandlerExceptionResolver 處理器異常解決器RequestToViewNameTranslator 處理邏輯視圖名稱ViewResolver 視圖渲染FlashMapManager 存儲屬性RequestMappingHandlerMappingRegistryRequestMappingHandlerAdapter
- DispatcherServlet 的邏輯處理請求上下文請求分發 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 請求並進行相應的邏輯處理
<?xml version=”1.0″ encoding=”UTF-8″?> <web-app xmlns=”http://xmlns.jcp.org/xml/ns/javaee” xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:schemaLocation=”http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd” version=”3.1″> <!– 使用 ContextLoaderListener時,告訴它 Spring 配置文件地址 –> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/applicationContext.xml</param-value> </context-param>
<!– 使用監聽器加載 applicationContext 文件 –> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
<!– 配置 DispatcherServlet –> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring-mvc.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
使用 IDEA 時,盡量選擇默認條件和自動掃描加載 Web 配置文件,然後添加 tomcat 進行啓動,具體配置請查閱 idea 創建java web項目ssm-gradle
(2) 配置 applicationContext.xml
<?xml version=”1.0″ encoding=”UTF-8″?> <beans xmlns=”http://www.springframework.org/schema/beans” xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xmlns:context=”http://www.springframework.org/schema/context” xsi:schemaLocation=”http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd”>
<!–這裏比較簡單,只是通知 Spring 掃描對應包下的 bean –> <context:component-scan base-package=”web.controller”/>
</beans>
可以在這裏自定義想要加載的 bean,或者設置數據庫數據源、事務管理器等等 Spring 應用配置。
(3) 配置 spring-mvc.xml
<?xml version=”1.0″ encoding=”UTF-8″?> <beans xmlns=”http://www.springframework.org/schema/beans” xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xmlns:mvc=”http://www.springframework.org/schema/mvc” xmlns:context=”http://www.springframework.org/schema/context” xsi:schemaLocation=”http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd”>
<!–掃描包,自動注入bean–> <context:component-scan base-package=”web.controller”/> <!–使用注解開發spring mvc–> <mvc:annotation-driven/>
<!–視圖解析器–> <bean class=”org.springframework.web.servlet.view.InternalResourceViewResolver”> <property name=”prefix” value=”/WEB-INF/views/”/> <property name=”suffix” value=”.jsp”/> </bean>
</beans>
使用了 InternalResourceViewResolver,它是一個輔助 Bean,這樣配置的意圖是: 在 ModelAndView 返回的視圖名前加上 prefix 指定的前綴和 suffix 的後綴(我理解爲用來解析和返回視圖,以及將視圖層進行統一管理,放到指定路徑中)
(4) 創建 BookController
@Controller public 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”) @ResponseBody public 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/jstl compile group: ‘javax.servlet’, name: ‘jstl’, version: ‘1.2’
(7) 啓動 Tomcat 如何配置和啓動,網上也有很多例子,參考資料 3 是個不錯的例子,下面是請求處理結果:
http://localhost:8080/bookView (使用了 JSP 視圖進行渲染)
在剛才的 web.xml 中有兩個關鍵配置,所以現在學習下這兩個配置具體是幹啥的。
ContextLoaderContext
作用:在啓動 web 容器時,自動裝載 ApplicationContext 的配置信息。
下面是它的繼承體系圖:
介紹:
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 屬性加載 WebApplicationContext wac = 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(); } // 通過反射來創建 contextClass ConfigurableWebApplicationContext 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 到 wac wac.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 配置文件
<!– 配置 DispatcherServlet –> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring-mvc.xml</param-value> </init-param> </servlet>
注冊 mvc 解析器
由于我們配置了 contextConfigLocation,指定了加載資源的路徑,所以在 XmlWebApplicationContext 初始化的時候,加載的 Spring 配置文件路徑是我們指定 spring-mvc.xml:
在 spring-mvc.xml 配置中,主要配置了三項
<!–掃描包,自動注入bean–> <context:component-scan base-package=”web.controller”/> <!–使用注解開發spring mvc–> <mvc:annotation-driven/>
<!–視圖解析器–> <bean class=”org.springframework.web.servlet.view.InternalResourceViewResolver”> <property name=”prefix” value=”/WEB-INF/views/”/> <property name=”suffix” value=”.jsp”/> </bean>
同樣老套路,使用了 <mvc:annotation> 自定義注解的話,要注冊相應的解析器後,Spring 容器才能解析元素:
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.properties try { 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.AcceptHeaderLocaleResolver
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\ org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\ org.springframework.web.servlet.function.support.RouterFunctionMapping
org.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.HandlerFunctionAdapter
org.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.DefaultHandlerExceptionResolver
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
org.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 處理,需要自己設定
<!–上傳下載–> <bean id=”multipartResolver” class=”org.springframework.web.multipart.commons.CommonsMultipartResolver”/>
注冊的 id 爲 multipartResolver
LocalResolver 與國際化相關
LocalResolver 接口定義了如何獲取客戶端的地區
private void initLocaleResolver(ApplicationContext context) { try { this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class); } catch (NoSuchBeanDefinitionException ex) { // We need to use the default. this.localeResolver = getDefaultStrategy(context, LocaleResolver.class); } }
通過尋找 id 爲 localeResolver 的 bean,如果沒有的話,將會使用默認的策略進行加載 AcceptHeaderLocaleResolver,它是基于 URL 參數來控制國際化,例如使用 <a href=”?locale=zh_CN”> 來設定簡體中文,默認參數名爲 locale。
當然還有其他兩種,基于 session 和基于 cookie 的配置,想要深入了解的可以去細看~
ThemeResolver 主題更換相關
主題是一組靜態資源(例如樣式表 css 和圖片 image),也可以理解爲應用皮膚,使用 Theme 更改主題風格,改善用戶體驗。
默認注冊的 id 是 themeResolver,類型是 FixedThemeResolver,表示使用的是一個固定的主題,以下是它的繼承體系圖:
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,先來看它的繼承體系圖:
當然,父類 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 信息尋找對應的 Handler mappedHandler = 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) { // 遍曆注冊的全部 handlerMapping for (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 獲取對應的 handler Object 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 necessary mav = 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
渲染調用的是其父類的方法:
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 處理請求