前文
剛剛入職,項目大范圍的使用到了 Spring + SpringMVC + MyBatis 框架,對于一個 Java 小白直接上手理解 Spring 還是十分困難的,而且只看書,不進入代碼層面,理解并記憶 Spring 的宏大框架是在太困難了,所以用了很長時間寫了一篇破天荒長度的博客……
本篇文章時筆者這輩子寫的最累的一篇博客…… 晚上加班到十點回來后開始寫作,整整寫了兩個多星期加上一個端午節,期間差點把房子都買了…… 最后終于在端午節晚上把文章寫了出來。感謝這段時間里同組同事們,明哥芳姐龍哥磊哥的工作中的幫助 ~
這篇文章寫下來,最大的感觸是:對于初學 Spring 的人來說,理解其架構實現真的很難,包括筆者寫了這么長的文章,中間也有很多內容并沒有完全理解。對于像筆者一樣沒有使用經驗的開發者來說,一定要在一個 SpringMVC 的工程之上使用單步調試的方法,逐步深入理解 Spring 的實現,才能在腦海中構建出基本的 Spring 框架。
本文通過 IDEA 安裝 Spring MVC 項目。首先在 IDEA 官網下載適合自己電腦配置版本的 Idea,然后進行安裝,安裝過程省略。
一. IDEA 新建 Spring MVC 工程項目
1.1 新建工程
安裝 IDEA 成功后,選擇 File -> New -> Project,左邊欄中選擇 Maven,選擇 Create From archetype,然后選中 org.apache.maven.archetypes:maven-archetype-webapp,然后點擊下一步,如下圖 1.1 所示:
然后填寫 GroupId 和 ArtifactId。GroupId 一般分為多個段,這里我只說兩段,第一段為域,第二段為公司名稱。域又分為 org, com, cn 等等許多,其中 org 為非營利組織,com 為商業組織。比如我創建一個項目,我一般會將 GroupId 設置 為 com.grq,com 表示域為公司,grq 是我個人姓名縮寫,artifactId 設置為 MySpringMVC,表示你這個項目的名稱是 mySpringMVC,依照這個設置,你的包結構最好是 com.grq.mySpringMVC 打頭的,如果有個StudentDao,它的全路徑就是 com.grq.mySpringMVC.StudentDao。
設置 GroupId 和 ArtifactId 的截圖如下所示:
工程項目構建完畢后,左側的 Project 欄顯示如下圖 1.3 所示:
1.2 Maven 設置
接下來需要在 Idea 中通過設置 Maven 從網絡引入 Spring 與 SpringMVC 的依賴項。
筆者用的是 Mac OX 下的 Idea,該版本下打開設置的方法是 IntelliJ IDEA -> Preferences(Win7 版:File -> Setting)。打開設置面板后,在搜索框中輸入 "maven",就可以進行 Maven 的設置如下圖 1.4 所示:
選擇如下設置,點擊 OK。
之后就可以通過連接網絡上的 Maven 庫,下載所需的依賴庫了。
注:
如果有本地 Maven 庫的話,可以設置圖 1.4 的 User settings file, Local repository,這樣就可以實現本地依賴庫的導入)
1.3 Maven 依賴庫內容的填寫
第一步中的工程構建完畢后,Project 列表中有一個 pom.xml。其中 pom 是項目對象模型 (Project Object Model) 的簡稱,它是 Maven 項目中的文件,使用 xml 表示。它的作用類似 ant 的 build.xml 文件,功能更強大。該文件用于管理源代碼、配置文件、開發者的信息和角色、問題追蹤系統、組織信息、項目授權、項目的 url 、項目的依賴關系等等。事實上,在 Maven 世界中,project 可以什么都沒有,甚至沒有代碼,但是必須包含 pom.xml 文件。
在 pom.xml 文件中填入 Spring 與 SpringMVC 的依賴庫,最后 pom.xml 文件如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.grq.mySpringMVC</groupId>
<artifactId>mySpringMVC</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>
</dependencies>
</project>
粘貼結束后,按照順序 View -> Tool Windows -> Maven Project 打開 Maven 的管理頁,并在管理頁中點擊如下圖 1.5 的 Reimport Maven All Projects 按鈕,即可將 pom.xml 中的依賴項下載并加載進入項目中,加載成功后的 Project 視圖如下圖 1.6 所示。
1.4 添加 Tomcat 依賴庫
1.4.1 Tomcat 作用
我們通常說到的 servlet 可以理解服務器端處理數據的 java 小程序,負責管理 servlet 就是 web 容器。它幫助我們管理著servlet等,使我們只需要將重心專注于業務邏輯。servlet 沒有 main 方法,那我們如何啟動一個 servlet,如何結束一個 servlet,如何尋找一個servlet 等等,都受控于另一個 java 應用,這個應用我們就稱之為 web 容器。或者可以理解成 servlet 只是一個規范,web 容器遵照這個規范,實現支持 servlet 規范的請求和應答。
我們最常見的 Tomcat 就是一個 Web 容器。如果 Web 服務器應用得到一個指向某 servlet 的請求,此時服務器不是把 servlet 交給 servlet 本身,而是交給部署該 servlet 的容器。要有容器向 servlet 提供 http 請求和響應,而且要由容器調用 servlet 的方法,如 doPost 或者 doGet。
1.4.2 添加 Tomcat 依賴庫
點擊菜單 File -> Project Structure,彈出設置對話框。選中左側欄 Project Settings 的 Libraries,點擊上面的加號"+",選擇 "Java" 選項,如圖 1.7 所示:
彈出的 Choose Modules 窗口中選擇當前的 Module(即筆者的 mySpringMvc),OK 確認。然后可以將 Libraries 的名稱改一下,筆者將其命名為 TomcatLibs。改完確定后,在 Project 視圖下會添加 TomcatLibs 的條目。如圖 1.8 所示:
1.5 項目中添加 Web 工程
現在我們只有 Spring 的框架,但 Spring MVC 必需的 Web 工程框架還沒有搭建,所以需要向項目中添加 Web 工程。
點擊菜單 File -> Project Structure,選中左側欄 Project Settings 的 Facets,在頂部的 "+" 中選擇 Web,并在彈出的 Choose Modules 窗口中選擇當前 Module(即筆者的 mySpringMvc),Ok 確認。筆者把 Name 改為 TomcatServer,這樣的名字更加直觀。此外,將 Web Resource Directory 的 ".../web" 改為 ".../webapp",筆者這樣的操作是為了與 Tomcat 的 webapp 路徑名稱對應。如下圖 1.9 所示:
此外在右下角有一個 Create Artifact 的按鈕,點擊后進入 Artifacts 標簽欄中,將右邊區域的所有內容添加到左區域,然后點擊 Ok 完成 Web 工程的添加。如圖 1.10 所示。
二. 填充 Spring MVC 內容
2.1 View 的編寫
在 webapp/WEB-INF 路徑上右擊 -> New -> Directory,輸入名稱:static/pages,新建的文件夾用于存放靜態的 HTML 文件。
然后在 static/pages 路徑上右擊 -> New -> HTML File,隨意填寫名稱,作為我們即將使用的視圖 View(筆者這里寫的名稱是 index)。
新建完畢之后,筆者隨便填寫了一些內容如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Spring MVC Test</title>
</head>
<body>
Welcome to my Spring MVC Testing!!!
</body>
</html>
2.2 Controller 的編寫
接下來我們需要寫一個 Controller。在路徑 src/main/java 下建包:右擊 src/main/java -> New -> Package,填寫合適的包名。筆者這里填寫的包名是 com.grq.springMvcTrain.controller。
在新建的 controller 包下再新建一個 java 文件,筆者將其命名為 MvcController.java。然后在 MvcController.java 中填寫內容如下:
package com.grq.springMvcTrain.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class MvcController {
@RequestMapping("/")
public String index() {
return "index";
}
}
2.3 添加父子容器的 xml 文件
接下來我們為父子容器添加 xml 文件。在這里,父容器指 Spring 容器,子容器指 SpringMVC 容器。
關于父子容器相關的內容,可以參考《spring的啟動過程——spring和springMVC父子容器的原理》,
《Spring和SpringMVC父子容器關系初窺》。
xml 文件都添加到 src/resources 路徑下,可將父容器命名為 application-context.xml, 子容器命名為 application-context-mvc.xml。其中 application-context.xml 為業務層 Spring 容器,application-context-mvc.xml 為 Web 容器。
2.3.1 application-context.xml
在 src/resources 路徑下右擊 -> New -> XML Configuration File -> Spring Config,命名為 application-context.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">
<!-- 自動掃描該包下的 Bean 并裝載 -->
<context:component-scan base-package="com.grq.springMvcTrain"/>
</beans>
2.3.2 application-context-mvc.xml
在 src/resources 路徑下右擊 -> New -> XML Configuration File -> Spring Config,命名為 application-context-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">
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
<context:component-scan base-package="com.grq.springMvcTrain.controller"/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/static/pages/"/>
<property name="suffix" value=".html"/>
<property name="order" value="1"/>
</bean>
</beans>
<mvc:annotation-driven>:
上面代碼中,<mvc:annotation-driven> 會自動注冊 RequestMappingHandlerMapping 與 RequestMappingHandlerAdapter 兩個 Bean,這兩個是 Spring MVC 為 @Controller 分發請求所必需的,并且提供了數據綁定支持。
<mvc:default-servlet-handler />:
在 application-context-mvc.xml 中配置 <mvc:default-servlet-handler />后,會在Spring MVC上下文中定義一個 org.springframework.web.servlet.resource 包下的 DefaultServletHttpRequestHandler,它的作用類似于一個檢查員,對進入 DispatcherServlet 的 URL 進行篩查。如果發現是靜態資源的請求,就將該請求轉由 Web 應用服務器默認的 Servlet 處理;如果不是靜態資源的請求,才由 DispatcherServlet 繼續處理。
一般 Web 應用服務器默認的 Servlet 名稱是 "default",因此DefaultServletHttpRequestHandler 可以找到它。如果你所有的 Web 應用服務器的默認 Servlet 名稱不是 "default",則需要通過 default-servlet-name 屬性顯示指定為:
<mvc:default-servlet-handler default-servlet-name="所使用的Web服務器默認使用的Servlet名稱" />
suffix, prefix:
對于視圖解析器 InternalResourceViewResolver,suffix, prefix 是很重要的屬性,它是邏輯視圖名的前綴與后綴。例如:
- 有 URL 地址:/WEB-INF/static/pages/index.html
- 邏輯視圖名:index
- 前綴:/WEB-INF/static/pages/
- 后綴:.html
4. web.xml 填寫
<?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_4_0.xsd"
version="4.0">
<!--業務層與模型層的 Spring 配置文件,配置文件被父容器使用-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:application-context.xml</param-value>
</context-param>
<!--聲明 Servlet-->
<servlet>
<servlet-name>mySpringMvcServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--對 DispatcherServlet 進行配置-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:application-context-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<!--DispatcherServlet 的 URL 模式-->
<servlet-mapping>
<servlet-name>mySpringMvcServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextCleanupListener</listener-class>
</listener>
</web-app>
web.xml 文件是用來初始化配置信息:比如 Welcome 頁面, servlet, servlet-mapping, filter, listener, 啟動加載級別等。對于一個 Web 項目,是可以沒有 web.xml 文件的。當你的 Web 工程沒用到這些時,你可以不用 web.xml 文件來配置你的 Application。也就是說,web.xml 文件并不是 Web 工程必須的。
web.xml 的模式文件中定義的標簽并不是定死的,模式文件也是可以改變的。一般來說,隨著 web.xml 模式文件的版本升級,里面定義的功能會越來越復雜,標簽元素的種類肯定也會越來越多,但有些不是很常用的,我們只需記住一些常用的并知道怎么配置就可以了。
web.xml 相關內容參考:《springmvc配置文件web.xml詳解各方總結。》
5. 配置 Tomcat
配置 Tomcat 要通過 Edit Configuration 進行。選項的外形如下圖 2.1 所示:
進入 Edit Configuration 后,點擊 "+",選擇 Tomcat Server -> Local。在命名框中隨意命名,筆者此處命名為 TomcatServer。
然后再 Deployment 標簽頁點擊 "+",選擇添加 Artifact,將之前的 Web 工程加入,選擇 OK。
配置 Tomcat ,將路徑配置正確。如下圖 2.3 所示:
三. Spring MVC 容器初始化
參考網址:
《springMVC的容器初始化過程》
《Spring之SpringMVC(源碼)啟動初始化過程分析》
《第二章 Spring MVC入門 —— 跟開濤學SpringMVC》
下面通過單步調試的方法,詳細解釋 SpringMVC 的初始化運行步驟。
Spring MVC 的核心在于 DispatcherServlet,觀察 DispatcherServlet 的繼承結構如下圖 3.1 所示:
可以從圖 3.1 看到,DispatcherServlet 依次繼承了 GenericServlet, HttpServlet, HttpServletBean, FrameworkServlet。由于 DispatcherServlet 是繼承了 HttpServlet,所以它的初始化入口應該是 HttpServlet 的 init() 方法。
1. HttpServlet.init()
Web 容器啟動時將調用它的 init 方法。源碼如下:
public final void init() throws ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Initializing servlet '" + getServletName() + "'");
}
// 從初始化參數中設置 Bean 屬性,讀取 web.xml 文件獲取 DispatcherServlet 基本信息
try {
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
throw ex;
}
// 子類調用初始化
initServletBean();
if (logger.isDebugEnabled()) {
logger.debug("Servlet '" + getServletName() + "' configured successfully");
}
}
可以注意到,init() 方法是 final,不能夠被覆蓋,它位于 HttpServletBean 中。它完成的功能有兩個,第一個將 Servlet 初始化參數設置到該 Servlet 中。在該部分中,可以讀取 web.xml 文件中的 DispatcherServlet 的相關信息,其中初始化內容包括上下文信息所在路徑,即 web.xml 中的 classpath:*/application-context-mvc.xml。
第二個調用子類的初始化。完成該步驟的是 HttpServletBean 中的 initServletBean() 方法。
2. HttpServletBean.initServletBean()
HttpServletBean 是 FrameworkServlet 的子類。接下來就關注一下調用子類初始化的 FrameworkServlet 的 initServletBean() 方法:
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
if (this.logger.isInfoEnabled()) {
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
}
long startTime = System.currentTimeMillis();
try {
// 內容 1: 完成了 Web 上下文的初始化工作;
// ContextLoaderListener 加載了上下文將作為根上下文(DispatcherServlet 的父容器)
this.webApplicationContext = initWebApplicationContext();
// 內容 2: 提供給子類進行初始化的擴展點。行容器的一些初始化,這個方法由子類實現,來進行擴展;
initFrameworkServlet();
}
catch (ServletException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
catch (RuntimeException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
if (this.logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
elapsedTime + " ms");
}
}
在 try 代碼塊中的兩行代碼即為 initServletBean() 方法的主要內容,分為兩個功能:一是完成 Web 上下文的初始化工作,這是主要內容。二是提供給子類,進行初始化。
再接下來,需要進入 HttpServletBean 的 initWebApplicationContext()。
3. HttpServletBean.initWebApplicationContext()
initWebApplicationContext() 方法的主要作用,就是從 web.xml 中讀取關于 Web 容器上下文的相關信息。該部分主要代碼如下:
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
// 步驟 1. 在創建的時候注入根上下文
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent -> set
// the root application context (if any; may be null) as the parent
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
// 步驟 2. 如果經過步驟 1,沒有注入上下文,則尋找上下文
if (wac == null) {
wac = findWebApplicationContext();
}
// 步驟 3. 如果沒有找到相應的上下文,則手動創建一個,并指定父親為其 ContextLoaderListner
if (wac == null) {
wac = createWebApplicationContext(rootContext);
}
// 步驟 4. 刷新上下文
if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
onRefresh(wac);
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
"' as ServletContext attribute with name [" + attrName + "]");
}
}
return wac;
}
該過程中,經歷了四個步驟:
- 在創建時注入根上下文;
- 如果此時沒有注入上下文,則開始尋找上下文;
- 如果此時沒有注入上下文,則手動創建一個,并指定其父親為其 ContextLoaderListenr;
- 刷新上下文;
四個步驟中,最重要的是手動創建上下文部分。
4. FrameworkServlet.createWebApplicationContext
該部分可以手動創建一個上下文。主要源碼如下:
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
if (this.contextId != null) {
wac.setId(this.contextId);
} else {
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(this.getServletContext().getContextPath()) + '/' + this.getServletName());
}
}
// 設置上下文參數
wac.setServletContext(this.getServletContext());
wac.setServletConfig(this.getServletConfig());
wac.setNamespace(this.getNamespace());
wac.addApplicationListener(new SourceFilteringListener(wac, new FrameworkServlet.ContextRefreshListener()));
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment)env).initPropertySources(this.getServletContext(), this.getServletConfig());
}
this.postProcessWebApplicationContext(wac);
this.applyInitializers(wac);
wac.refresh();
}
設置上下文參數的部分,獲取了所有 WebApplicationContext 相關的值,并將其注入了 WebApplicationContext 中。
5. DispatcherServlet.onRefresh()
最后調用 onRefresh() 方法。onRefresh() 方法是抽象基類 AbstractApplicationContext 的方法,實際運行時被 DispatcherServlet 所覆蓋,它在內部調用了 initStrategies() 方法,作用是刷新上下文。
protected void onRefresh(ApplicationContext context) {
this.initStrategies(context);
}
6. DispatcherServlet.initStrategies
initStrategies 方法源碼如下:
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
}
進入 DispatcherServlet 的 initStrategies,此時所有的 bean 都已經加載好了;程序運行到這里,就已經實現了 DispatcherServlet 初始化的工作。后面進入 DispatcherServlet 處理用戶響應的過程。
四. SpringMVC 響應 —— doDispatch 的運行流程
參考網址:
運行流程:
《第二章 Spring MVC入門 —— 跟開濤學SpringMVC》攔截器相關:《SpringMVC源碼總結(十一)mvc:interceptors攔截器介紹》
《第五章 處理器攔截器詳解——跟著開濤學SpringMVC 》
運行到上一步,DispathcerServlet 已經在 Web 容器中運行,程序等待瀏覽器客戶端的響應。前面設定的端口號為 8080,此時在任意一個瀏覽器輸入地址:
http://localhost:8080
此時就相當于從客戶端向該工程的 Servlet 發送了一個請求 request,程序也就進入了 Servlet 的 doService() 方法。觀察到 DispatcherServelt 的 doService() 方法部分源碼如下所示:
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
......
try {
this.doDispatch(request, response);
} finally {
......
}
}
由上面的源碼可以看出,DispatcherServlet 的核心是調用 doDispatch 方法。doDispatch 方法的源碼如下:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 步驟 1: 檢查是否為 multipart
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 步驟 2: 請求到處理器 (DispatcherServlet) 的映射,通過 HandlerMapping 進行映射
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 步驟 3: 處理器適配
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 如果程序支持,則處理最后修改的頭部
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
// 步驟 4: 預處理
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 步驟 5: 由適配器執行處理器
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// 步驟 6: 后處理
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 步驟 7: 解析、渲染 View
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
// 步驟 8: 完成后處理
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
下面按照上述源碼中標注的步驟,進入 DispatcherServlet 的 doDispatch 方法并分析:
注:下面的步驟 3 至步驟 8 是一個核心運行流程,并將該核心流程的總結放在最后。
1. 步驟 1: 檢查是否為 multipart
由于筆者是 SpringMVC 的新手,所以該步驟筆者并不能完全理解。主要作用是檢查 request 是否為多部分 request (checkMultipart),比如檢測是否要用于文件上傳。
2. 步驟 2: 請求到 DispatcherServlet 的映射
通過 HandleMapping 映射,請求到 DispatcherServlet 的映射。該部分源碼如下:
// 步驟 2
mappedHandler = getHandler(processedRequest, false);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
進入 getHandler 方法源碼,可以觀察到,getHandler 內部遍歷了 DispatcherServlet 的 HandlerMapping 集合,直到訪問到了 Handler,就將其作為 HandlerExecutionChain。
HandlerExecutionChain 即 Handler 執行鏈,它包含一個處理器 (HandlerMethod),若干個攔截器 (HandlerInterceptor)。結構如下圖 4.1 所示:
HandlerExecutionChain 的主要功能是通過若干 HandlerInterceptor 實現的。HandlerInterceptor 主要方法如下:
- boolean preHandle(...):該方法是一個前置方法,請求到達 Handler 之前,先執行該前置處理方法。如果該方法返回 false,則請求直接返回;如果返回 true,才會傳遞給下一個處理節點。
- void postHandle(...):在請求被 HandlerAdapter 執行之后,執行該后置處理方法。
如果遍歷過程中找到 Handler,則將當前 handler 作為 HandlerExecutionChain 并返回到 DispatcherServlet,否則判斷退出。
在步驟 2 中,經過了 handler = hm.getHandler(request) 語句后(此時的 hm 類型為 HandlerMapping 的子類 RequestMappingHandlerMapping),此時 mapperdHandler = com.grq.example.controller.UserController.index()。
3. 步驟 3: 處理器適配
// 步驟 3: 處理器適配
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
該步驟中,核心方法是 getHandlerAdapter,它將我們的 Handler 包裝成相應適配器 HandlerAdapter。該方法的代碼如下:
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
Iterator var2 = this.handlerAdapters.iterator();
HandlerAdapter ha;
do {
if (!var2.hasNext()) {
throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
ha = (HandlerAdapter)var2.next();
if (this.logger.isTraceEnabled()) {
this.logger.trace("Testing handler adapter [" + ha + "]");
}
} while(!ha.supports(handler));
return ha;
}
由代碼段可知,getHandlerAdapter 方法遍歷眾多的 HandlerApapter,并分別調用它們的 support(handler) 方法,直到 support 方法返回值為 true 為止(此時 HandlerAdapter 的類型為 RequestMappingHandlerAdapter)
4. 步驟 4: 預處理
// 預處理
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
該部分只有一個 applyPreHandle 方法,即為 HandlerExecutionChain 執行鏈中若干攔截器的作用部分。代碼如下所示:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
HandlerInterceptor interceptor = interceptors[i];
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
}
return true;
}
若干攔截器使用 preHandle 方法層層攔截,若存在某個攔截器的 preHandle 返回 false,則該方法返回 false;此外只要有一個攔截器返回 false,相應的 doDispatch 方法也將返回結束。
5. 步驟 5: 由適配器執行處理器
// 步驟 5: 由適配器執行處理器
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
該步驟中,核心方法是 handle 方法,它是 HandlerAdapter 的接口方法,具體需要按照不同的 HandlerAdapter 類型進行實現。
此前提到運行到這里,此時的 HandlerAdapter 類型是 RequestMappingHandlerAdapter。在 RequestMappingHandlerAdapter 中進行一些步驟的跳轉,會調用 AbstractHandlerMethodAdapter 抽象類的 handleInternal 方法,并用其子類的具體實現。
RequestMappingHandlerAdapter 的 handleInternal 方法代碼如下:
protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
this.checkRequest(request);
ModelAndView mav;
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized(mutex) {
mav = this.invokeHandlerMethod(request, response, handlerMethod);
}
} else {
mav = this.invokeHandlerMethod(request, response, handlerMethod);
}
} else {
mav = this.invokeHandlerMethod(request, response, handlerMethod);
}
if (!response.containsHeader("Cache-Control")) {
if (this.getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
this.applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
} else {
this.prepareResponse(response);
}
}
return mav;
}
可以看出,該方法的核心語句是 mav = this.invokeHandlerMethod(request, response, handlerMethod),它的輸入參數 handlerMethod 中包含先前獲取到的方法相關信息(包括方法名 method 與參數 parameters),在該方法中調用 invokeHandlerMethod,返回值即為 ModelAndView 類型的數據。
6. 步驟 6: 后處理
mappedHandler.applyPostHandle(processedRequest, response, mv);
該部分只有一個 applyPostHandle 方法,即為 HandlerExecutionChain 執行鏈中若干攔截器的作用部分。代碼如下所示:
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) throws Exception {
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for(int i = interceptors.length - 1; i >= 0; --i) {
HandlerInterceptor interceptor = interceptors[i];
interceptor.postHandle(request, response, this.handler, mv);
}
}
}
若干攔截器使用 preHandle 方法層層攔截,若存在某個攔截器的 postHandle 返回 false,則該方法返回 false;此外只要有一個攔截器返回 false,相應的 doDispatch 方法也將返回結束。
7. 步驟 7: 解析、渲染 View
步驟 5 中獲得 ModelAndView 參數后,doDispatch 方法使用 processDispatchResult 方法解析 View。processDispatchResult 方法源碼如下:
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
boolean errorView = false;
// 判斷異常
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
this.logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException)exception).getModelAndView();
} else {
Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
mv = this.processHandlerException(request, response, handler, exception);
errorView = mv != null;
}
}
// 渲染 View
if (mv != null && !mv.wasCleared()) {
// 核心源碼,解析 View
this.render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
} else if (this.logger.isDebugEnabled()) {
this.logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + this.getServletName() + "': assuming HandlerAdapter completed request handling");
}
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, (Exception)null);
}
}
}
上面的源碼中,核心的源碼在于 render 方法,它用來解析、渲染 View。render 方法的源碼如下:
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
Locale locale = this.localeResolver.resolveLocale(request);
response.setLocale(locale);
// 解析 View
View view;
if (mv.isReference()) {
// We need to resolve the view name.
view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException(
"Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" +
getServletName() + "'");
}
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// Delegate to the View object for rendering.
if (logger.isDebugEnabled()) {
logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
}
// 渲染 View
try {
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '"
+ getServletName() + "'", ex);
}
throw ex;
}
}
解析視圖:
解析視圖的功能由方法 resolveViewName 實現。具體解析的方法,是在容器中查找所有配置好的 List 類型的視圖解析器 (ViewResolver),然后進行遍歷,只要存在一個視圖解析器,就能解析出視圖。調用該視圖解析器的方法對 View 進行解析,最后返回該 View 值。方法源碼如下:
// org.springframework.web.servlet.DispatcherServlet # resolveViewName
protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
HttpServletRequest request) throws Exception {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
return null;
}
渲染視圖:
render 是一個接口方法,具體需要由實現了 View 接口的類具體實現。接口方法如下:
void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
View 接口主要由 AbstractView 抽象類繼承,在 AbstractView 中的 render 方法實現如下:
// org.springframework.web.servlet.view.AbstractView # render
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("Rendering view with name '" + this.beanName + "' with model " + model +
" and static attributes " + this.staticAttributes);
}
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
prepareResponse(request, response);
// 核心源碼,進行實時渲染
renderMergedOutputModel(mergedModel, request, response);
}
render 方法為指定的模型指定視圖,如果有必要的話,在 createMergedOutputModel 方法中合并它靜態的屬性和 RequestContext 中的屬性,最后在核心源碼 renderMergedOutputModel() 中執行實際的渲染。
renderMergedOutputModel 也是一個抽象方法,由具體的視圖解析器具體實現。以 InternalResourceView 的 renderMergedOutputModel() 方法為例,源碼如下:
@Override
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine which request handle to expose to the RequestDispatcher.
HttpServletRequest requestToExpose = getRequestToExpose(request);
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, requestToExpose);
// Expose helpers as request attributes, if any.
exposeHelpers(requestToExpose);
// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(requestToExpose, response);
// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = getRequestDispatcher(requestToExpose, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
"]: Check that the corresponding file exists within your web application archive!");
}
// If already included or response already committed, perform include, else forward.
if (useInclude(requestToExpose, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.include(requestToExpose, response);
}
else {
// Note: The forwarded resource is supposed to determine the content type itself.
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.forward(requestToExpose, response);
}
}
之前的步驟都是向該方法中的 RequestDispatcher 中填充數據,獲取了 RequestDispatcher 后,最后通過 include 或 forward 方法轉發,正常狀況下,運行到這里我們就會在瀏覽器上看到了頁面內容,如下圖 4.2 所示。
關于 include 與 forward 的區別,可參考:《SpringMVC——使用RequestDispatcher.include()和HttpServletResponseWrapper動態獲取jsp輸出內容》
8. 步驟 8: 完成后處理
// 步驟 8: 完成后處理
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
整個請求處理完畢,即在視圖渲染完畢時回調 applyAfterConcurrentHandlingStarted 方法。該方法的核心在于 afterCompletion 方法,該方法無論視圖渲染成功,都會調用,但僅調用處理器執行鏈中 preHandle 返回 true 的攔截器的 afterCompletion。
該方法在初學時一般不會在意,但依舊有很大的應用空間,如性能監控中我們可以在此記錄結束時間并輸出消耗時間;另外還可以進行一些資源清理,類似于 try-catch-finally 中的 finally。
9. 流程結束
上述步驟 3 至 步驟 8,是一個正常結束的 doDispatch 流程,即攔截器返回值全部為 true。它的流程順序如下圖 4.3 所示:
當然也存在 doDispatch 的中斷流程,該部分的具體細節,可以參閱博客《第五章 處理器攔截器詳解——跟著開濤學SpringMVC 》。
至此,返回控制權給 DispatcherServlet,由 DispatcherServlet 返回相應給用戶,至此一個流程結束。
后記
紙上得來終覺淺,絕知此事要躬行。
筆者入職之后看了好幾天的關于 Spring 的各類各樣的書,書上講的倒還好,但是自己就是記不住。畢竟如果只是看書,幾乎是不能記住如此抽象的知識內容的。所以,面對 Spring 與 Spring MVC 這種體系龐大的框架,一定要通過單步調試的方法慢慢體會并總結,這樣才有可能將整個流程較為穩妥的記在心里。
但筆者對 Spring 的學習也尚未滿一個月,所以本文的解釋不夠詳細,也許會有些啰嗦不知所以然,請各位讀者諒解,并希望各位能夠在閱讀后提出寶貴意見,可以使我們共同進步。