通過前面的 AOP
(面向切面編程) 和 Transaction
(事務管理),此次來到了 MVC
(Web 應用,進行請求分發和處理)php
Spring MVC 定義:css
分離了控制器(Controller)、模型(Model)、分配器(Adapter)、視圖(View)和處理程序對象(Handler,實際上調用的是 Controller 中定義的邏輯)。html
基於 Servlet 功能實現,經過實現了 Servlet 接口的 DispatcherServlet 來封裝其核心功能實現,經過將請求分派給處理程序,同時帶有可配置的處理程序映射、視圖解析、本地語言、主題解析以及上傳文件支持。前端
一樣老套路,本篇按照如下思路展開:java
(1) 介紹如何使用git
(2) 輔助工具類 ContextLoaderContext
github
(3) DispatcherServlet
初始化web
(4) DispatcherServlet
處理請求spring
代碼結構以下:(詳細代碼可在文章末尾下載)數據庫
├── 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 視圖進行渲染)
http://localhost:8080/plain/value (先後端分離的話,經常使用的是這種,最後能夠返回簡單字符或者 json 格式的對象等)
在剛纔的 web.xml
中有兩個關鍵配置,因此如今學習下這兩個配置具體是幹啥的。
做用:在啓動 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.properties
ClassPathResource 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
那樣使用。
該類是 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();
}
複製代碼
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
上下文中。
雖然有默認 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;
}
複製代碼
默認使用的是 XmlWebApplicationContext
,但若是須要配置自定義上下文,能夠在 web.xml
中的 <init-param>
標籤中修改 contextClass
屬性對應的 value
,但須要注意圖中提示:
使用該方法,用來對已經建立的 WebApplicaitonContext
進行配置以及刷新
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
// 遍歷 ApplicationContextInitializer,執行 initialize 方法
applyInitializers(wac);
// 關鍵的刷新,加載配置文件及整合 parent 到 wac
wac.refresh();
}
複製代碼
該類能夠經過 <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
須要執行。
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>
複製代碼
因爲咱們配置了 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
的初始化。
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
複製代碼
接下來看看它們各自的初始化過程以及使用場景:
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
接口定義瞭如何獲取客戶端的地區
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
的配置,想要深刻了解的能夠去細看~
主題是一組靜態資源(例如樣式表 css 和圖片 image),也能夠理解爲應用皮膚,使用 Theme
更改主題風格,改善用戶體驗。
默認註冊的 id
是 themeResolver
,類型是 FixedThemeResolver
,表示使用的是一個固定的主題,如下是它的繼承體系圖:
工做原理是經過攔截器攔截,配置對應的主題解析器,而後返回主題名稱,仍是使用上面的解析器做爲例子:
FixedThemeResolver#resolveThemeName
public String resolveThemeName(HttpServletRequest request) {
return getDefaultThemeName();
}
public String getDefaultThemeName() {
return this.defaultThemeName;
}
複製代碼
首先判斷 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 的 bean
HandlerMapping 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
套路與前面的同樣,使用的默認策略是:HttpRequestHandlerAdapter
、SimpleControllerHandlerAdapter
、 RequestMappingHandlerAdapter
和 HandlerFunctionAdapter
。
說到適配器,能夠將它理解爲,將一個類的接口適配成用戶所期待的,將兩個接口不兼容的工做類,經過適配器鏈接起來。
套路也與前面同樣,使用的默認策略是: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;
}
}
複製代碼
初始化代碼邏輯與前面同樣,使用的默認策略是:DefaultRequestToViewNameTranslator
使用場景:當 Controller
處理器方法沒有返回邏輯視圖名稱時,Spring
經過該類的約定,提供一個邏輯視圖名稱。
因爲本地測試不出來,因此引用參考資料 7 的例子:
DefaultRequestToViewNameTranslator的轉換例子:
http://localhost:8080/gamecast/display.html -> display(視圖)
套路仍是跟前面同樣,默認策略使用的是:InternalResourceViewResolver
同時,這也是 demo
中,咱們手動配置的視圖解析器
默認使用的是:SessionFlashMapManager
,經過與 FlashMap
配合使用,用於在重定向時保存/傳遞參數。
例如 Post/Redirect/Get
模式,Flash attribute
在重定向以前暫存(根據類名,能夠知道範圍是 session
級別有效),以便重定向以後還能使用。
該類做用:配合 @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
。
該類是 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>> registry
this.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
,因爲它的繼承體系與 RequestMappingHandler
相似,因此咱們直接來看它在加載時執行的方法
RequestMappingHandlerAdapter#afterPropertiesSet
public void afterPropertiesSet() {
// 首先執行這個方法,能夠添加 responseBody 切面 bean
initControllerAdviceCache();
// 參數處理器
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
視圖。
請求處理的入口定義在 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
函數展現了請求的完成處理過程:
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,進行視圖渲染和跳轉)
以 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下如何配置實現跨域請求?
前面已經找到了對應的處理器了,下一步就得找到它對應的適配器
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
。
判斷 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 boolean supportsReturnType(MethodParameter returnType) {
Class<?> paramType = returnType.getParameterType();
return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
}
複製代碼
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 forward
return;
}
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
複製代碼
還記得咱們使用的是 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
處理請求
本篇筆記寫得比以前的都要吃力,mvc
模塊基本使用了以前總結過的知識點,一邊學一邊複習以前的知識,並且因爲我的在開發環境遇到了阻塞,秉着 [本身都不能成功運行的代碼,是不能提交的] 原則,處理了挺長時間。
在跟蹤每一個知識點時,越深刻發現坑越多,想要將它描述完整,在學習理解和總結中不斷循環,因此本篇花了不少時間,同時也有不少知識點沒有去深刻學習,例如 demo
中出現的 @RequestBody
、@PathVarible
等註解是如何解析和返回結果處理,留個坑。
同時這篇筆記也是目前 Spring
源碼學習的最後一篇技術總結,指望能獲得朋友們的支持,若是寫的不對的地方或者建議,請與我聯繫,我將完善和補充~
因爲我的技術有限,若是有理解不到位或者錯誤的地方,請留下評論,我會根據朋友們的建議進行修正
Gitee 地址 https://gitee.com/vip-augus/spring-analysis-note.git
Github 地址 https://github.com/Vip-Augus/spring-analysis-note