小白文-SpringMVC-解讀DispatcherServlet源碼

SpringMVC
學習完Spring框架技術之後,差不多會出現兩批人:
- 一批是聽得雲里霧裡,依然不明白這個東西是幹嘛的;
- 還有一批就是差不多理解了核心思想,但是不知道這些東西該如何去發揮它的作用。
在SpringMVC階段,你就能逐漸夠體會到Spring框架為我們帶來的便捷之處了。
此階段,我們將再次回到Tomcat的Web應用程式開發中,去感受SpringMVC為我們帶來的巨大便捷。
MVC理論基礎
在之前,我們給大家講解了三層架構,包括:

每一層都有著各自的職責,其中最關鍵的當屬表示層,因為它相當於就是直接與用戶的瀏覽器打交道的一層,並且所有的請求都會經過它進行解析,然後再告知業務層進行處理,任何頁面的返回和數據填充也全靠表示層來完成,因此它實際上是整個三層架構中最關鍵的一層。
我們編寫了大量的Servlet(也就是表示層實現)來處理來自瀏覽器的各種請求,但是我們發現,僅僅是幾個很小的功能,以及幾個很基本的頁面,我們都要編寫將近十個Servlet,如果是更加大型的網站系統,比如淘寶、B站,光是一個頁面中可能就包含了幾十甚至上百個功能,想想那樣的話寫起來得多恐怖。
因此,SpringMVC正是為了解決這種問題而生的,它是一個非常優秀的表示層框架(在此之前還有一個叫做Struts2的框架,但是現階段貌似快涼透了),採用MVC思想設計實現。
MVC解釋如下:
- M是指業務模型(Model):通俗的講就是我們之前用於封裝數據傳遞的實體類。
- V是指用戶介面(View):一般指的是前端頁面。
- C則是控制器(Controller):控制器就相當於Servlet的基本功能,處理請求,返迴響應。

SpringMVC正是希望這三者之間進行解耦,實現各干各的,更加精細地劃分對應的職責。
最後再將View和Model進行渲染,得到最終的頁面並返回給前端(就像之前使用Thymeleaf那樣,把實體數據對象和前端頁面都給到Thymeleaf,然後它會將其進行整合渲染得到最終有數據的頁面。)
配置環境並搭建項目
下載地址://tomcat.apache.org/download-90.cgi
添加SpringMVC的依賴:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.13</version>
</dependency>
接著我們需要配置一下web.xml,將DispatcherServlet替換掉Tomcat自帶的Servlet,這裡url-pattern需要寫為/,即可完成替換:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="//xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="//xmlns.jcp.org/xml/ns/javaee //xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>mvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>mvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
接著需要為整個Web應用程式配置一個Spring上下文環境(也就是容器),
因為SpringMVC是基於Spring開發的,它直接利用Spring提供的容器來實現各種功能,這裡我們直接使用註解方式進行配置,不再使用XML配置文件:
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.example.config.MvcConfiguration</param-value>
</init-param>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
如果還是想使用XML配置文件進行配置,那麼可以直接這樣寫:
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>配置文件名稱</param-value>
</init-param>
如果你希望完完全全丟棄配置文件,可以直接添加一個類,Tomcat會在類路徑中查找實現。
ServletContainerInitializer 介面的類,如果發現的話,就用它來配置Servlet容器,Spring提供了這個介面的實現類 SpringServletContainerInitializer , 通過@HandlesTypes(WebApplicationInitializer.class)設置,這個類反過來會查找實現WebApplicationInitializer 的類,並將配置的任務交給他們來完成,因此直接實現介面即可:
public class MainInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{MainConfiguration.class}; //基本的Spring配置類,一般用於業務層配置
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[0]; //配置DispatcherServlet的配置類、主要用於Controller等配置
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"}; //匹配路徑,與上面一致
}
}
順便編寫一下最基本的配置類:
@Configuration
public class MainConfiguration {
}
後面我們都採用無XML配置方式進行講解。

這樣,就完成最基本的配置了,現在任何請求都會優先經過DispatcherServlet進行集中處理,下面我們會詳細講解如何使用它。
Controller控制器
有了SpringMVC之後,我們不必再像之前那樣一個請求地址創建一個Servlet了,它使用DispatcherServlet替代Tomcat為我們提供的默認的靜態資源Servlet。
也就是說,現在所有的請求(除了jsp,因為Tomcat還提供了一個jsp的Servlet)都會經過DispatcherServlet進行處理。
那麼DispatcherServlet會幫助我們做什麼呢?

根據圖片我們可以了解,我們的請求到達Tomcat伺服器之後,會交給當前的Web應用程式進行處理,而SpringMVC使用DispatcherServlet來處理所有的請求,也就是說它被作為一個統一的訪問點,所有的請求全部由它來進行調度。
當一個請求經過DispatcherServlet之後,會先走HandlerMapping。
它會將請求映射為HandlerExecutionChain,依次經過HandlerInterceptor有點類似於之前我們所學的過濾器。
不過在SpringMVC中我們使用的是攔截器,然後再交給HandlerAdapter,根據請求的路徑選擇合適的控制器進行處理,控制器處理完成之後,會返回一個ModelAndView對象,包括數據模型和視圖,通俗的講就是頁面中數據和頁面本身(只包含視圖名稱即可)。
返回ModelAndView之後,會交給ViewResolver(視圖解析器)進行處理,視圖解析器會對整個視圖頁面進行解析,SpringMVC自帶了一些視圖解析器,但是只適用於JSP頁面,我們也可以像之前一樣使用Thymeleaf作為視圖解析器,這樣我們就可以根據給定的視圖名稱,直接讀取HTML編寫的頁面,解析為一個真正的View。
解析完成後,就需要將頁面中的數據全部渲染到View中,最後返回給DispatcherServlet一個包含所有數據的成形頁面,再響應給瀏覽器,完成整個過程。
因此,實際上整個過程我們只需要編寫對應請求路徑的的Controller以及配置好我們需要的ViewResolver即可,之後還可以繼續補充添加攔截器,而其他的流程已經由SpringMVC幫助我們完成了。
配置視圖解析器和控制器
首先我們需要實現最基本的頁面解析並返回,第一步就是配置視圖解析器,這裡我們使用Thymeleaf為我們提供的視圖解析器,導入需要的依賴:
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.12.RELEASE</version>
</dependency>
配置視圖解析器非常簡單,我們只需要將對應的ViewResolver註冊為Bean即可,這裡我們直接在配置類中編寫:
@ComponentScan("com.example.controller")
@Configuration
@EnableWebMvc
public class WebConfiguration {
//我們需要使用ThymeleafViewResolver作為視圖解析器,並解析我們的HTML頁面
@Bean
public ThymeleafViewResolver thymeleafViewResolver(@Autowired SpringTemplateEngine springTemplateEngine){
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setOrder(1); //可以存在多個視圖解析器,並且可以為他們設定解析順序
resolver.setCharacterEncoding("UTF-8"); //編碼格式是重中之重
resolver.setTemplateEngine(springTemplateEngine); //和之前JavaWeb階段一樣,需要使用模板引擎進行解析,所以這裡也需要設定一下模板引擎
return resolver;
}
//配置模板解析器
@Bean
public SpringResourceTemplateResolver templateResolver(){
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setSuffix(".html"); //需要解析的後綴名稱
resolver.setPrefix("/"); //需要解析的HTML頁面文件存放的位置
return resolver;
}
//配置模板引擎Bean
@Bean
public SpringTemplateEngine springTemplateEngine(@Autowired ITemplateResolver resolver){
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setTemplateResolver(resolver); //模板解析器,默認即可
return engine;
}
}
別忘了在Initializer中添加此類作為配置:
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{MvcConfiguration.class};
}
現在我們就完成了視圖解析器的配置,我們接著來創建一個Controller,創建Controller也非常簡單,只需在一個類上添加一個@Controller註解即可,它會被Spring掃描並自動註冊為Controller類型的Bean,然後我們只需要在類中編寫方法用於處理對應地址的請求即可:
@Controller //直接添加註解即可
public class MainController {
@RequestMapping("/index") //直接填寫訪問路徑
public ModelAndView index(){
return new ModelAndView("index"); //返回ModelAndView對象,這裡填入了視圖的名稱
//返回後會經過視圖解析器進行處理
}
}
我們會發現,打開瀏覽器之後就可以直接訪問我們的HTML頁面了。
而頁面中的數據我們可以直接向Model進行提供:
@RequestMapping(value = "/index")
public ModelAndView index(){
ModelAndView modelAndView = new ModelAndView("index");
modelAndView.getModel().put("name", "啊這");
return modelAndView;
}
這樣Thymeleaf就能收到我們傳遞的數據進行解析:
<!DOCTYPE html>
<html lang="en" xmlns:th="//www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="static/test.js"></script>
</head>
<body>
HelloWorld!
<div th:text="${name}"></div>
</body>
</html>
當然,如果僅僅是傳遞一個頁面不需要任何的附加屬性,我們可以直接返回View名稱,SpringMVC會將其自動包裝為ModelAndView對象:
@RequestMapping(value = "/index")
public String index(){
return "index";
}
還可以單獨添加一個Model作為形參進行設置,SpringMVC會自動幫助我們傳遞實例對象:
@RequestMapping(value = "/index")
public String index(Model model){ //這裡不僅僅可以是Model,還可以是Map、ModelMap
model.addAttribute("name", "yyds");
return "index";
}
注意,一定要保證視圖名稱下面出現橫線並且按住Ctrl可以跳轉,配置才是正確的(最新版IDEA)
我們的頁面中可能還會包含一些靜態資源,比如js、css,因此這裡我們還需要配置一下,讓靜態資源通過Tomcat提供的默認Servlet進行解析,我們需要讓配置類實現一下WebMvcConfigurer介面,這樣在Web應用程式啟動時,會根據我們重寫方法裡面的內容進行進一步的配置:
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable(); //開啟默認的Servlet
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("/WEB-INF/static/");
//配置靜態資源的訪問路徑
}
我們編寫一下前端內容:
<!DOCTYPE html>
<html lang="en" xmlns:th="//www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- 引用靜態資源,這裡使用Thymeleaf的網址鏈接表達式,Thymeleaf會自動添加web應用程式的名稱到鏈接前面 -->
<script th:src="@{/static/test.js}"></script>
</head>
<body>
HelloWorld!
</body>
</html>
創建test.js並編寫如下內容:
window.alert("歡迎來到GayHub,全球最大同性交友網站")
最後訪問頁面,頁面在載入時就會顯示一個彈窗,這樣我們就完成了最基本的頁面配置。相比之前的方式,這樣就簡單很多了,直接避免了編寫大量的Servlet來處理請求。
@RequestMapping詳解
前面我們已經了解了如何創建一個控制器來處理我們的請求,接著我們只需要在控制器添加一個方法用於處理對應的請求即可,之前我們需要完整地編寫一個Servlet來實現,而現在我們只需要添加一個@RequestMapping即可實現,其實從它的名字我們也能得知,此註解就是將請求和處理請求的方法建立一個映射關係,當收到請求時就可以根據映射關係調用對應的請求處理方法,那麼我們就來先聊聊@RequestMapping吧,註解定義如下:
@Mapping
public @interface RequestMapping {
String name() default "";
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
RequestMethod[] method() default {};
String[] params() default {};
String[] headers() default {};
String[] consumes() default {};
String[] produces() default {};
}
其中最關鍵的是path屬性(等價於value),它決定了當前方法處理的請求路徑,注意路徑必須全局唯一,任何路徑只能有一個方法進行處理,它是一個數組,也就是說此方法不僅僅可以只用於處理某一個請求路徑,我們可以使用此方法處理多個請求路徑:
@RequestMapping({"/index", "/test"})
public ModelAndView index(){
return new ModelAndView("index");
}
現在我們訪問/index或是/test都會經過此方法進行處理。
我們也可以直接將@RequestMapping添加到類名上,表示為此類中的所有請求映射添加一個路徑前綴,比如:
@Controller
@RequestMapping("/yyds")
public class MainController {
@RequestMapping({"/index", "/test"})
public ModelAndView index(){
return new ModelAndView("index");
}
}
那麼現在我們需要訪問/yyds/index或是/yyds/test才可以得到此頁面。我們可以直接在IDEA下方的端點板塊中查看當前Web應用程式定義的所有請求映射,並且可以通過IDEA為我們提供的內置Web客戶端直接訪問某個路徑。
路徑還支援使用通配符進行匹配:
- ?:表示任意一個字元,比如
@RequestMapping("/index/x?")可以匹配/index/xa、/index/xb等等。 - *:表示任意0-n個字元,比如
@RequestMapping("/index/*")可以匹配/index/lbwnb、/index/yyds等。 - **:表示當前目錄或基於當前目錄的多級目錄,比如
@RequestMapping("/index/**")可以匹配/index、/index/xxx等。
我們接著來看下一個method屬性,顧名思義,它就是請求的方法類型,我們可以限定請求方式,比如:
@RequestMapping(value = "/index", method = RequestMethod.POST)
public ModelAndView index(){
return new ModelAndView("index");
}
現在我們如果直接使用瀏覽器訪問此頁面,會顯示405方法不支援,因為瀏覽器默認是直接使用GET方法獲取頁面,而我們這裡指定為POST方法訪問此地址,所以訪問失敗,我們現在再去端點中用POST方式去訪問,成功得到頁面。
我們也可以使用衍生註解直接設定為指定類型的請求映射:
@PostMapping(value = "/index")
public ModelAndView index(){
return new ModelAndView("index");
}
這裡使用了@PostMapping直接指定為POST請求類型的請求映射,同樣的,還有@GetMapping可以直接指定為GET請求方式,這裡就不一一列舉了。
我們可以使用params屬性來指定請求必須攜帶哪些請求參數,比如:
@RequestMapping(value = "/index", params = {"username", "password"})
public ModelAndView index(){
return new ModelAndView("index");
}
比如這裡我們要求請求中必須攜帶username和password屬性,否則無法訪問。它還支援表達式,比如我們可以這樣編寫:
@RequestMapping(value = "/index", params = {"!username", "password"})
public ModelAndView index(){
return new ModelAndView("index");
}
在username之前添加一個感嘆號表示請求的不允許攜帶此參數,否則無法訪問,我們甚至可以直接設定一個固定值:
@RequestMapping(value = "/index", params = {"username!=test", "password=123"})
public ModelAndView index(){
return new ModelAndView("index");
}
這樣,請求參數username不允許為test,並且password必須為123,否則無法訪問。
header屬性用法與params一致,但是它要求的是請求頭中需要攜帶什麼內容,比如:
@RequestMapping(value = "/index", headers = "!Connection")
public ModelAndView index(){
return new ModelAndView("index");
}
那麼,如果請求頭中攜帶了Connection屬性,將無法訪問。其他兩個屬性:
- consumes: 指定處理請求的提交內容類型(Content-Type),例如application/json, text/html;
- produces: 指定返回的內容類型,僅當request請求頭中的(Accept)類型中包含該指定類型才返回;
@RequestParam和@RequestHeader詳解
我們接著來看,如何獲取到請求中的參數。
我們只需要為方法添加一個形式參數,並在形式參數前面添加@RequestParam註解即可:
@RequestMapping(value = "/index")
public ModelAndView index(@RequestParam("username") String username){
System.out.println("接受到請求參數:"+username);
return new ModelAndView("index");
}
我們需要在@RequestParam中填寫參數名稱,參數的值會自動傳遞給形式參數,我們可以直接在方法中使用,注意,如果參數名稱與形式參數名稱相同,即使不添加@RequestParam也能獲取到參數值。
一旦添加@RequestParam,那麼此請求必須攜帶指定參數,我們也可以將require屬性設定為false來將屬性設定為非必須:
@RequestMapping(value = "/index")
public ModelAndView index(@RequestParam(value = "username", required = false) String username){
System.out.println("接受到請求參數:"+username);
return new ModelAndView("index");
}
我們還可以直接設定一個默認值,當請求參數缺失時,可以直接使用默認值:
@RequestMapping(value = "/index")
public ModelAndView index(@RequestParam(value = "username", required = false, defaultValue = "傘兵一號") String username){
System.out.println("接受到請求參數:"+username);
return new ModelAndView("index");
}
如果需要使用Servlet原本的一些類,比如:
@RequestMapping(value = "/index")
public ModelAndView index(HttpServletRequest request){
System.out.println("接受到請求參數:"+request.getParameterMap().keySet());
return new ModelAndView("index");
}
直接添加HttpServletRequest為形式參數即可,SpringMVC會自動傳遞該請求原本的HttpServletRequest對象,同理,我們也可以添加HttpServletResponse作為形式參數,甚至可以直接將HttpSession也作為參數傳遞:
@RequestMapping(value = "/index")
public ModelAndView index(HttpSession session){
System.out.println(session.getAttribute("test"));
session.setAttribute("test", "雞你太美");
return new ModelAndView("index");
}
我們還可以直接將請求參數傳遞給一個實體類:
@Data
public class User {
String username;
String password;
}
注意必須攜帶set方法或是構造方法中包含所有參數,請求參數會自動根據類中的欄位名稱進行匹配:
@RequestMapping(value = "/index")
public ModelAndView index(User user){
System.out.println("獲取到cookie值為:"+user);
return new ModelAndView("index");
}
@RequestHeader與@RequestParam用法一致,不過它是用於獲取請求頭參數的,這裡就不再演示了。
@CookieValue和@SessionAttrbutie
通過使用@CookieValue註解,我們也可以快速獲取請求攜帶的Cookie資訊:
@RequestMapping(value = "/index")
public ModelAndView index(HttpServletResponse response,
@CookieValue(value = "test", required = false) String test){
System.out.println("獲取到cookie值為:"+test);
response.addCookie(new Cookie("test", "lbwnb"));
return new ModelAndView("index");
}
同樣的,Session也能使用註解快速獲取:
@RequestMapping(value = "/index")
public ModelAndView index(@SessionAttribute(value = "test", required = false) String test,
HttpSession session){
session.setAttribute("test", "xxxx");
System.out.println(test);
return new ModelAndView("index");
}
可以發現,通過使用SpringMVC框架,整個Web應用程式的開發變得非常簡單,大部分功能只需要一個註解就可以搞定了,正是得益於Spring框架,SpringMVC才能大顯身手。
重定向和請求轉發
重定向和請求轉發也非常簡單,我們只需要在視圖名稱前面添加一個前綴即可,比如重定向:
@RequestMapping("/index")
public String index(){
return "redirect:home";
}
@RequestMapping("/home")
public String home(){
return "home";
}
通過添加redirect:前綴,就可以很方便地實現重定向,那麼請求轉發呢,其實也是一樣的,使用forward:前綴表示轉發給其他請求映射:
@RequestMapping("/index")
public String index(){
return "forward:home";
}
@RequestMapping("/home")
public String home(){
return "home";
}
使用SpringMVC,只需要一個前綴就可以實現重定向和請求轉發,非常方便。
Bean的Web作用域
在學習Spring時我們講解了Bean的作用域,包括singleton和prototype,Bean分別會以單例和多例模式進行創建,而在SpringMVC中,它的作用域被繼續細分:
- request:對於每次HTTP請求,使用request作用域定義的Bean都將產生一個新實例,請求結束後Bean也消失。
- session:對於每一個會話,使用session作用域定義的Bean都將產生一個新實例,會話過期後Bean也消失。
- global session:不常用,不做講解。
這裡我們創建一個測試類來試試看:
public class TestBean {
}
接著將其註冊為Bean,注意這裡需要添加@RequestScope或是@SessionScope表示此Bean的Web作用域:
@Bean
@RequestScope
public TestBean testBean(){
return new TestBean();
}
接著我們將其自動注入到Controller中:
@Controller
public class MainController {
@Resource
TestBean bean;
@RequestMapping(value = "/index")
public ModelAndView index(){
System.out.println(bean);
return new ModelAndView("index");
}
}
我們發現,每次發起得到的Bean實例都不同,接著我們將其作用域修改為@SessionScope,這樣作用域就上升到Session,只要清理瀏覽器的Cookie,那麼都會被認為是同一個會話,只要是同一個會話,那麼Bean實例始終不變。
實際上,它也是通過代理實現的,我們調用Bean中的方法會被轉發到真正的Bean對象去執行。
RestFul風格
中文釋義為「表現層狀態轉換」(名字挺高大上的),它不是一種標準,而是一種設計風格。
它的主要作用是充分並正確利用HTTP協議的特性,規範資源獲取的URI路徑。
通俗的講,RESTful風格的設計允許將參數通過URL拼接傳到服務端,目的是讓URL看起來更簡潔實用,並且我們可以充分使用多種HTTP請求方式(POST/GET/PUT/DELETE),來執行相同請求地址的不同類型操作。
因此,這種風格的連接,我們就可以直接從請求路徑中讀取參數,比如:
//localhost:8080/mvc/index/123456
我們可以直接將index的下一級路徑作為請求參數進行處理,也就是說現在的請求參數包含在了請求路徑中:
@RequestMapping("/index/{str}")
public String index(@PathVariable String str) {
System.out.println(str);
return "index";
}
注意請求路徑我們可以手動添加類似佔位符一樣的資訊,這樣佔位符位置的所有內容都會被作為請求參數,而方法的形參列表中必須包括一個與佔位符同名的並且添加了@PathVariable註解的參數,或是由@PathVariable註解指定為佔位符名稱:
@RequestMapping("/index/{str}")
public String index(@PathVariable("str") String text){
System.out.println(text);
return "index";
}
如果沒有配置正確,方法名稱上會出現黃線。
我們可以按照不同功能進行劃分:
- POST //localhost:8080/mvc/index - 添加用戶資訊,攜帶表單數據
- GET //localhost:8080/mvc/index/{id} - 獲取用戶資訊,id直接放在請求路徑中
- PUT //localhost:8080/mvc/index - 修改用戶資訊,攜帶表單數據
- DELETE //localhost:8080/mvc/index/{id} - 刪除用戶資訊,id直接放在請求路徑中
我們分別編寫四個請求映射:
@Controller
public class MainController {
@RequestMapping(value = "/index/{id}", method = RequestMethod.GET)
public String get(@PathVariable("id") String text){
System.out.println("獲取用戶:"+text);
return "index";
}
@RequestMapping(value = "/index", method = RequestMethod.POST)
public String post(String username){
System.out.println("添加用戶:"+username);
return "index";
}
@RequestMapping(value = "/index/{id}", method = RequestMethod.DELETE)
public String delete(@PathVariable("id") String text){
System.out.println("刪除用戶:"+text);
return "index";
}
@RequestMapping(value = "/index", method = RequestMethod.PUT)
public String put(String username){
System.out.println("修改用戶:"+username);
return "index";
}
}
這只是一種設計風格而已,各位小夥伴了解即可。
Interceptor攔截器
攔截器是整個SpringMVC的一個重要內容,攔截器與過濾器類似,都是用於攔截一些非法請求,但是我們之前講解的過濾器是作用於Servlet之前,只有經過層層的攔截器才可以成功到達Servlet。
而攔截器並不是在Servlet之前,它在Servlet與RequestMapping之間,相當於DispatcherServlet在將請求交給對應Controller中的方法之前進行攔截處理,它只會攔截所有Controller中定義的請求映射對應的請求(不會攔截靜態資源),這裡一定要區分兩者的不同。

創建攔截器
創建一個攔截器我們需要實現一個HandlerInterceptor介面:
public class MainInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("我是處理之前!");
return true; //只有返回true才會繼續,否則直接結束
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("我是處理之後!");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("我是完成之後!");
}
}
接著我們需要在配置類中進行註冊:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MainInterceptor())
.addPathPatterns("/**") //添加攔截器的匹配路徑,只要匹配一律攔截
.excludePathPatterns("/home"); //攔截器不進行攔截的路徑
}
現在我們在瀏覽器中訪問index頁面,攔截器已經生效。
得到整理攔截器的執行順序:
我是處理之前!
我是處理!
我是處理之後!
我是完成之後!
也就是說,處理前和處理後,包含了真正的請求映射的處理,在整個流程結束後還執行了一次afterCompletion方法,其實整個過程與我們之前所認識的Filter類似,不過在處理前,我們只需要返回true或是false表示是否被攔截即可,而不是再去使用FilterChain進行向下傳遞。
那麼我們就來看看,如果處理前返回false,會怎麼樣:
我是處理之前!
通過結果發現一旦返回false,之後的所有流程全部取消,那麼如果是在處理中發生異常了呢?
@RequestMapping("/index")
public String index(){
System.out.println("我是處理!");
if(true) throw new RuntimeException("");
return "index";
}
結果為:
我是處理之前!
我是處理!
我是完成之後!
我們發現如果處理過程中拋出異常,那麼久不會執行處理後postHandle方法,但是會執行afterCompletion方法,我們可以在此方法中獲取到拋出的異常。
多級攔截器
前面介紹了僅僅只有一個攔截器的情況,我們接著來看如果存在多個攔截器會如何執行,我們以同樣的方式創建二號攔截器:
public class SubInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("二號攔截器:我是處理之前!");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("二號攔截器:我是處理之後!");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("二號攔截器:我是完成之後!");
}
}
註冊二號攔截器:
@Override
public void addInterceptors(InterceptorRegistry registry) {
//一號攔截器
registry.addInterceptor(new MainInterceptor()).addPathPatterns("/**").excludePathPatterns("/home");
//二號攔截器
registry.addInterceptor(new SubInterceptor()).addPathPatterns("/**");
}
注意攔截順序就是註冊的順序,因此攔截器會根據註冊順序依次執行,我們可以打開瀏覽器運行一次:
一號攔截器:我是處理之前!
二號攔截器:我是處理之前!
我是處理!
二號攔截器:我是處理之後!
一號攔截器:我是處理之後!
二號攔截器:我是完成之後!
一號攔截器:我是完成之後!
和多級Filter相同,在處理之前,是按照順序從前向後進行攔截的,但是處理完成之後,就按照倒序執行處理後方法,而完成後是在所有的postHandle執行之後再同樣的以倒序方式執行。
那麼如果這時一號攔截器在處理前就返回了false呢?
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("一號攔截器:我是處理之前!");
return false;
}
得到結果如下:
一號攔截器:我是處理之前!
我們發現,與單個攔截器的情況一樣,一旦攔截器返回false,那麼之後無論有無攔截器,都不再繼續。
異常處理
當我們的請求映射方法中出現異常時,會直接展示在前端頁面,這是因為SpringMVC為我們提供了默認的異常處理頁面,當出現異常時,我們的請求會被直接轉交給專門用於異常處理的控制器進行處理。
我們可以自定義一個異常處理控制器,一旦出現指定異常,就會轉接到此控制器執行:
@ControllerAdvice
public class ErrorController {
@ExceptionHandler(Exception.class)
public String error(Exception e, Model model){ //可以直接添加形參來獲取異常
e.printStackTrace();
model.addAttribute("e", e);
return "500";
}
}
接著我們編寫一個專門顯示異常的頁面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
500 - 伺服器出現了一個內部錯誤QAQ
<div th:text="${e}"></div>
</body>
</html>
接著修改:
@RequestMapping("/index")
public String index(){
System.out.println("我是處理!");
if(true) throw new RuntimeException("無法訪問!");
return "index";
}
訪問後,我們發現控制台會輸出異常資訊,同時頁面也是我們自定義的一個頁面。
JSON數據格式與AJAX請求
JSON (JavaScript Object Notation, JS 對象簡譜) 是一種輕量級的數據交換格式。
我們現在推崇的是前後端分離的開發模式,而不是所有的內容全部交給後端渲染再發送給瀏覽器。(當然,我個人不認為所有的項目都適合前後分離。)
也就是說,整個Web頁面的內容在一開始就編寫完成了,而其中的數據由前端執行JS程式碼來向伺服器動態獲取,再到前端進行渲染(填充),這樣可以大幅度減少後端的壓力,並且後端只需要傳輸關鍵數據即可(在即將到來的SpringBoot階段,我們將完全採用前後端分離的開發模式)
JSON數據格式
既然要實現前後端分離,那麼我們就必須約定一種更加高效的數據傳輸模式,來向前端頁面傳輸後端提供的數據。因此JSON橫空出世,它非常容易理解,並且與前端的兼容性極好,因此現在比較主流的數據傳輸方式則是通過JSON格式承載的。
一個JSON格式的數據長這樣,以學生對象為例:
{"name": "傑哥", "age": 18}
多個學生可以以數組的形式表示:
[{"name": "傑哥", "age": 18}, {"name": "阿偉", "age": 18}]
嵌套關係可以表示為:
{"studentList": [{"name": "傑哥", "age": 18}, {"name": "阿偉", "age": 18}], "count": 2}
它直接包括了屬性的名稱和屬性的值,與JavaScript的對象極為相似,它到達前端後,可以直接轉換為對象,以對象的形式進行操作和內容的讀取,相當於以字元串形式表示了一個JS對象,我們可以直接在控制台窗口中測試:
let obj = JSON.parse('{"studentList": [{"name": "傑哥", "age": 18}, {"name": "阿偉", "age": 18}], "count": 2}')
//將JSON格式字元串轉換為JS對象
obj.studentList[0].name //直接訪問第一個學生的名稱
我們也可以將JS對象轉換為JSON字元串:
JSON.stringify(obj)
我們後端就可以以JSON字元串的形式向前端返回數據,這樣前端在拿到數據之後,就可以快速獲取,非常方便。
那麼後端如何快速創建一個JSON格式的數據呢?我們首先需要導入以下依賴:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
JSON解析框架有很多種,比較常用的是Jackson和FastJSON,這裡我們使用阿里巴巴的FastJSON進行解析。
首先要介紹的是JSONObject,它和Map的使用方法一樣(實現了Map介面),比如我們向其中存放幾個數據:
@RequestMapping(value = "/index")
public String index(){
JSONObject object = new JSONObject();
object.put("name", "傑哥");
object.put("age", 18);
System.out.println(object.toJSONString()); //以JSON格式輸出JSONObject字元串
return "index";
}
最後我們得到的結果為:
{"name": "傑哥", "age": 18}
實際上JSONObject就是對JSON數據的一種對象表示。同樣的還有JSONArray,它表示一個數組,用法和List一樣,數組中可以嵌套其他的JSONObject或是JSONArray:
@RequestMapping(value = "/index")
public String index(){
JSONObject object = new JSONObject();
object.put("name", "傑哥");
object.put("age", 18);
JSONArray array = new JSONArray();
array.add(object);
System.out.println(array.toJSONString());
return "index";
}
得到的結果為:
[{"name": "傑哥", "age": 18}]
當出現循環引用時,會按照以下語法來解析:
我們可以也直接創建一個實體類,將實體類轉換為JSON格式的數據:
@RequestMapping(value = "/index", produces = "application/json")
@ResponseBody
public String data(){
Student student = new Student();
student.setName("傑哥");
student.setAge(18);
return JSON.toJSONString(student);
}
這裡我們修改了produces的值,將返回的內容類型設定為application/json,表示伺服器端返回了一個JSON格式的數據(當然不設置也行,也能展示,這樣是為了規範)然後我們在方法上添加一個@ResponseBody表示方法返回(也可以在類上添加@RestController表示此Controller默認返回的是字元串數據)的結果不是視圖名稱而是直接需要返回一個字元串作為頁面數據,這樣,返回給瀏覽器的就是我們直接返回的字元串內容。
接著我們使用JSON工具類將其轉換為JSON格式的字元串,打開瀏覽器,得到JSON格式數據。
SpringMVC非常智慧,我們可以直接返回一個對象類型,它會被自動轉換為JSON字元串格式:
@RequestMapping(value = "/data", produces = "application/json")
@ResponseBody
public Student data(){
Student student = new Student();
student.setName("傑哥");
student.setAge(18);
return student;
}
注意需要在配置類中添加一下FastJSON轉換器(默認只支援JackSon):
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new FastJsonHttpMessageConverter());
}
AJAX請求
前面我們講解了如何向瀏覽器發送一個JSON格式的數據,那麼我們現在來看看如何向伺服器請求數據。

Ajax即Asynchronous Javascript And XML(非同步JavaScript和XML),它的目標就是實現頁面中的數據動態更新,而不是直接刷新整個頁面,它是一個概念。
它在JQuery框架中有實現,因此我們直接導入JQuery(JQuery極大地簡化了JS的開發,封裝了很多內容,感興趣的可以了解一下):
<script src="//code.jquery.com/jquery-3.1.1.min.js"></script>
接著我們就可以直接使用了,首先修改一下前端頁面:
<!DOCTYPE html>
<html lang="en" xmlns:th="//www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="//code.jquery.com/jquery-3.1.1.min.js"></script>
<script th:src="@{/static/test.js}"></script>
</head>
<body>
你好,
<span id="username"></span>
您的年齡是:
<span id="age"></span>
<button onclick="updateData()">點我更新頁面數據</button>
</body>
</html>
現在我們希望用戶名稱和年齡需要在我們點擊按鈕之後才會更新,我們接著來編寫一下JS:
function updateData() {
//美元符.的方式來使用Ajax請求,這裡使用的是get方式,第一個參數為請求的地址(注意需要帶上Web應用程式名稱),第二個參數為成功獲取到數據的方法,data就是返回的數據內容
$.get("/mvc/data", function (data) { //獲取成功執行的方法
window.alert('接受到非同步請求數據:'+JSON.stringify(data)) //彈窗展示數據
$("#username").text(data.name) //這裡使用了JQuery提供的選擇器,直接選擇id為username的元素,更新數據
$("#age").text(data.age)
})
}
使用JQuery非常方便,我們直接通過JQuery的選擇器就可以快速獲取頁面中的元素,注意這裡獲取的元素是被JQuery封裝過的元素,需要使用JQuery提供的方法來進行操作。
這樣,我們就實現了從服務端獲取數據並更新到頁面中(實際上之前,我們在JavaWeb階段使用XHR請求也演示過,不過當時是純粹的數據)
那麼我們接著來看,如何向服務端發送一個JS對象數據並進行解析:
function submitData() {
$.post("/mvc/submit", { //這裡使用POST方法發送請求
name: "測試", //第二個參數是要傳遞的對象,會以表單數據的方式發送
age: 18
}, function (data) {
window.alert(JSON.stringify(data)) //發送成功執行的方法
})
}
伺服器端只需要在請求參數位置添加一個對象接收即可(和前面是一樣的,因為這裡也是提交的表單數據):
@RequestMapping("/submit")
@ResponseBody
public String submit(Student student){
System.out.println("接收到前端數據:"+student);
return "{\"success\": true}";
}
我們也可以將js對象轉換為JSON字元串的形式進行傳輸,這裡需要使用ajax方法來處理:
function submitData() {
$.ajax({ //最基本的請求方式,需要自己設定一些參數
type: 'POST', //設定請求方法
url: "/mvc/submit", //請求地址
data: JSON.stringify({name: "測試", age: 18}), //轉換為JSON字元串進行發送
success: function (data) {
window.alert(JSON.stringify(data))
},
contentType: "application/json" //請求頭Content-Type一定要設定為JSON格式
})
}
如果我們需要讀取前端發送給我們的JSON格式數據,那麼這個時候就需要添加@RequestBody註解:
@RequestMapping("/submit")
@ResponseBody
public String submit(@RequestBody JSONObject object){
System.out.println("接收到前端數據:"+object);
return "{\"success\": true}";
}
這樣,我們就實現了前後端使用JSON字元串進行通訊。
實現文件上傳和下載
利用SpringMVC,我們可以很輕鬆地實現文件上傳和下載,同樣的,我們只需要配置一個Resolver:
@Bean("multipartResolver") //注意這裡Bean的名稱是固定的,必須是multipartResolver
public CommonsMultipartResolver commonsMultipartResolver(){
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
resolver.setMaxUploadSize(1024 * 1024 * 10); //最大10MB大小
resolver.setDefaultEncoding("UTF-8"); //默認編碼格式
return resolver;
}
接著我們直接編寫Controller即可:
@RequestMapping(value = "/upload", method = RequestMethod.POST)
@ResponseBody
public String upload(@RequestParam CommonsMultipartFile file) throws IOException {
File fileObj = new File("test.html");
file.transferTo(fileObj);
System.out.println("用戶上傳的文件已保存到:"+fileObj.getAbsolutePath());
return "文件上傳成功!";
}
使用CommonsMultipartFile對象來接收用戶上傳的文件。它是基於Apache的Commons-fileupload框架實現的,我們還需要導入一個依賴:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
最後在前端添加一個文件的上傳點:
<div>
<form action="upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit">
</form>
</div>
這樣,點擊提交之後,文件就會上傳到伺服器了。
下載其實和我們之前的寫法大致一樣,直接使用HttpServletResponse,並向輸出流中傳輸數據即可。
@RequestMapping(value = "/download", method = RequestMethod.GET)
@ResponseBody
public void download(HttpServletResponse response){
response.setContentType("multipart/form-data");
try(OutputStream stream = response.getOutputStream();
InputStream inputStream = new FileInputStream("test.html")){
IOUtils.copy(inputStream, stream);
}catch (IOException e){
e.printStackTrace();
}
}
在前端頁面中添加一個下載點:
<a href="download" download="test.html">下載最新資源</a>
解讀DispatcherServlet源碼
首先我們需要找到DispatcherServlet的最頂層HttpServletBean,在這裡直接繼承的HttpServlet,那麼我們首先來看一下,它在初始化方法中做了什麼:
public final void init() throws ServletException {
//讀取配置參數,並進行配置
PropertyValues pvs = new HttpServletBean.ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));
this.initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
} catch (BeansException var4) {
if (this.logger.isErrorEnabled()) {
this.logger.error("Failed to set bean properties on servlet '" + this.getServletName() + "'", var4);
}
throw var4;
}
}
//此初始化階段由子類實現,
this.initServletBean();
}
我們接著來看initServletBean()方法是如何實現的,它是在子類FrameworkServlet中定義的:
protected final void initServletBean() throws ServletException {
this.getServletContext().log("Initializing Spring " + this.getClass().getSimpleName() + " '" + this.getServletName() + "'");
if (this.logger.isInfoEnabled()) {
this.logger.info("Initializing Servlet '" + this.getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
//注意:我們在一開始說了SpringMVC有兩個容器,一個是Web容器一個是根容器
//Web容器只負責Controller等表現層內容
//根容器就是Spring容器,它負責Service、Dao等,並且它是Web容器的父容器。
//初始化WebApplicationContext,這個階段會為根容器和Web容器進行父子關係建立
this.webApplicationContext = this.initWebApplicationContext();
this.initFrameworkServlet();
} catch (RuntimeException | ServletException var4) {
//...以下內容全是列印日誌
}

我們來看看initWebApplicationContext是如何進行初始化的:
protected WebApplicationContext initWebApplicationContext() {
//這裡獲取的是根容器,一般用於配置Service、數據源等
WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
//如果webApplicationContext在之前已經存在,則直接給到wac
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
//設定根容器為Web容器的父容器
cwac.setParent(rootContext);
}
this.configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
//如果webApplicationContext是空,那麼就從ServletContext找一下有沒有初始化上下文
wac = this.findWebApplicationContext();
}
if (wac == null) {
//如果還是找不到,直接創個新的,並直接將根容器作為父容器
wac = this.createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
synchronized(this.onRefreshMonitor) {
//此方法由DispatcherServlet實現
this.onRefresh(wac);
}
}
if (this.publishContext) {
String attrName = this.getServletContextAttributeName();
//把Web容器丟進ServletContext
this.getServletContext().setAttribute(attrName, wac);
}
return wac;
}
我們接著來看DispatcherServlet中實現的onRefresh()方法:
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
//初始化各種解析器
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
//在容器中查找所有的HandlerMapping,放入集合中
//HandlerMapping保存了所有的請求映射資訊(Controller中定義的),它可以根據請求找到處理器Handler,但並不是簡單的返回處理器,而是將處理器和攔截器封裝,形成一個處理器執行鏈(類似於之前的Filter)
initHandlerMappings(context);
//在容器中查找所有的HandlerAdapter,它用於處理請求並返回ModelAndView對象
//默認有三種實現HttpRequestHandlerAdapter,SimpleControllerHandlerAdapter和AnnotationMethodHandlerAdapter
//當HandlerMapping找到處理請求的Controller之後,會選擇一個合適的HandlerAdapter處理請求
//比如我們之前使用的是註解方式配置Controller,現在有一個請求攜帶了一個參數,那麼HandlerAdapter會對請求的數據進行解析,並傳入方法作為實參,最後根據方法的返回值將其封裝為ModelAndView對象
initHandlerAdapters(context);
//其他的內容
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
DispatcherServlet初始化過程我們已經了解了,那麼我們接著來看DispatcherServlet是如何進行調度的,首先我們的請求肯定會經過HttpServlet,然後其交給對應的doGet、doPost等方法進行處理,而在FrameworkServlet中,這些方法都被重寫,並且使用processRequest來進行處理:
protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.processRequest(request, response);
}
protected final void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.processRequest(request, response);
}
我們來看看processRequest做了什麼:
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//前期準備工作
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = this.buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());
this.initContextHolders(request, localeContext, requestAttributes);
try {
//重點在這裡,這裡進行了Service的執行,不過是在DispatcherServlet中定義的
this.doService(request, response);
} catch (IOException | ServletException var16) {
//...
}
請各位一定要耐心,這些大型框架的底層一般都是層層套娃,因為這樣寫起來層次會更加清晰,那麼我們來看看DispatcherServlet中是如何實現的:
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
//...
try {
//重點在這裡,這才是整個處理過程中最核心的部分
this.doDispatch(request, response);
} finally {
//...
}
終於找到最核心的部分了:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
//在HandlerMapping集合中尋找可以處理當前請求的HandlerMapping
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
//找不到HandlerMapping則無法進行處理
return;
}
//根據HandlerMapping提供的資訊,找到可以處理的HandlerAdapter
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}
//執行所有攔截器的preHandle()方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
//使用HandlerAdapter進行處理(我們編寫的請求映射方法在這個位置才真正地執行了)
//HandlerAdapter會幫助我們將請求的數據進行處理,再來調用我們編寫的請求映射方法
//最後HandlerAdapter會將結果封裝為ModelAndView返回給mv
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
//執行所有攔截器的postHandle()方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
//最後處理結果,對視圖進行渲染等,如果拋出異常會出現錯誤頁面
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
所以,根據以上源碼分析得出最終的流程圖:


