Spring 源碼學習(十) Spring mvc


通過前面的 AOP(面向切面編程) 和 Transaction(事務管理),此次來到了 MVC(Web 應用,進行請求分發和處理)php

Spring MVC 定義:css

分離了控制器(Controller)、模型(Model)、分配器(Adapter)、視圖(View)和處理程序對象(Handler,實際上調用的是 Controller 中定義的邏輯)。html

基於 Servlet 功能實現,經過實現了 Servlet 接口的 DispatcherServlet 來封裝其核心功能實現,經過將請求分派給處理程序,同時帶有可配置的處理程序映射、視圖解析、本地語言、主題解析以及上傳文件支持。前端

一樣老套路,本篇按照如下思路展開:java

(1) 介紹如何使用git

(2) 輔助工具類 ContextLoaderContextgithub

(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 :使 WebSpring 的配置文件相結合的關鍵配置

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 中有兩個關鍵配置,因此如今學習下這兩個配置具體是幹啥的。


ContextLoaderContext

做用:在啓動 web 容器時,自動裝載 ApplicationContext 的配置信息。

下面是它的繼承體系圖:

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

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

public void contextInitialized(ServletContextEvent event) {
	initWebApplicationContext(event.getServletContext());
}
複製代碼

每一個 Web 應用都會有一個 ServletContext 貫穿生命週期(在應用啓動時建立,關閉時銷燬),跟 SpringApplicationContext 相似,在全局範圍內有效。

實際上初始化的工做,是由父類 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 那樣使用。


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 屬性加載 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

xml_web_applicaiton_context_refresh_diagram

從圖中可以看出,刷新方法的代碼邏輯與以前同樣,經過父類 AbstractApplicationContextrefresh 方法,進行了配置文件的加載。

在例子中的 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 進行了重寫,主要用來刷新 SpringWeb 功能實現中所必須用到的全局變量:

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 的關鍵所在,先來大體介紹一下初始化的套路:

  1. 尋找用戶自定義配置
  2. 沒有找到,使用默認配置

顯然,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"/>
複製代碼

註冊的 idmultipartResolver


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);
    }
}
複製代碼

經過尋找 idlocaleResolverbean,若是沒有的話,將會使用默認的策略進行加載 AcceptHeaderLocaleResolver,它是基於 URL 參數來控制國際化,例如使用 <a href="?locale=zh_CN"> 來設定簡體中文,默認參數名爲 locale

固然還有其餘兩種,基於 session 和基於 cookie 的配置,想要深刻了解的能夠去細看~


ThemeResolver 主題更換相關

主題是一組靜態資源(例如樣式表 css 和圖片 image),也能夠理解爲應用皮膚,使用 Theme 更改主題風格,改善用戶體驗。

默認註冊的 idthemeResolver,類型是 FixedThemeResolver,表示使用的是一個固定的主題,如下是它的繼承體系圖:

工做原理是經過攔截器攔截,配置對應的主題解析器,而後返回主題名稱,仍是使用上面的解析器做爲例子:

FixedThemeResolver#resolveThemeName

public String resolveThemeName(HttpServletRequest request) {
	return getDefaultThemeName();
}

public String getDefaultThemeName() {
	return this.defaultThemeName;
}
複製代碼

HandlerMapping 與匹配處理器相關

首先判斷 detectAllHandlerMappings 變量是否爲 true,表示是否須要加載容器中全部的 HandlerMappingfalse 將會加載用戶配置的。

如註釋所說,至少得保證有一個 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 配置時,就已經注入了 RequestMappingHandlerMappingBeanNameUrlHandlerMapping


HandlerAdapter 適配器

套路與前面的同樣,使用的默認策略是:HttpRequestHandlerAdapterSimpleControllerHandlerAdapterRequestMappingHandlerAdapterHandlerFunctionAdapter

說到適配器,能夠將它理解爲,將一個類的接口適配成用戶所期待的,將兩個接口不兼容的工做類,經過適配器鏈接起來。


HandlerExceptionResolver 處理器異常解決器

套路也與前面同樣,使用的默認策略是:ExceptionHandlerExceptionResolverResponseStatusExceptionResolverDefaultHandlerExceptionResolver

實現了 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,先來看它的繼承體系圖:

request_mapping_handler_mapping_diagram

從繼承圖中看到,它實現了 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 的內部類,是個工具類,用來保存全部 Mappinghandler 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。它是一個泛型的 Mapkey 類型是 RequestMappingInfo,保存了 @RequestMapping 各類屬性的集合,value 類型是 AbstractHandlerMethodMapping,保存的是咱們的映射關係。

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


RequestMappingHandlerAdapter

而另外一個重要的配置就是處理器適配器 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);
	}
}
複製代碼

因此看到這個適配器中,初始化了不少工具變量,用來處理 @ControllerAdviceInitBinder 等註解和參數。不過核心仍是待會要講到的 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);
}
複製代碼

能夠看到 doGetdoPost 這些方法,底層調用的都是 processRequest 方法進行處理,關鍵方法是委託給子類 DispatcherServletdoServie() 方法

DispatcherServlet#doService

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

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


請求上下文

processRequestdoServie() 方法執行前,主要作了這如下準備工做:

(1) 爲了保證當前線程的 LocaleContext 以及 RequestAttributes 能夠在當前請求後還能恢復,提取當前線程的兩個屬性。 (2) 根據當前 request 建立對應的 LocaleContext 以及 RequestAttributes,綁定到當前線程 (3) 往 request 對象中設置以前加載過的 localeResolverflashMapManager 等輔助工具變量


請求分發 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

view_name_method_return_value_handler_diagram

能夠看到它實現了 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);
	}
}
複製代碼

render

還記得咱們使用的是 jsp 視圖進行渲染麼,引用的依賴是 jstl,因此視圖渲染的是 JstlView 類提供的方法,如下是它的繼承體系:

jstl_diagram

渲染調用的是其父類的方法:

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


參考資料

  1. idea中web.xml報錯 Servlet should have a mapping
  2. SpringMVC 使用總結
  3. idea 建立java web項目ssm-gradle
  4. Spring 源碼深度解析 / 郝佳編著. -- 北京 : 人民郵電出版社
  5. Spring MVC【入門】就這一篇!
  6. DispatcherServlet
  7. spring-mvc
  8. SpringBoot下如何配置實現跨域請求?

傳送門:

相關文章
相關標籤/搜索