讓你五分鐘徹底理解Spring MVC
概述
Sping MVC 正式的名字為 Spring Web MVC,是 Spring Framework 框架中的其中一個(gè)模塊,基于 Servlet API 構(gòu)建,同時(shí)使用 MVC 的架構(gòu)模式,主要用以簡化傳統(tǒng)的 Servlet + JSP 進(jìn)行 web 開發(fā)的工作。
MVC 架構(gòu)模式
Spring MVC 基于 MVC 模式,因此理解 Spring MVC 需要先對(duì) MVC 模式有所了解。
傳統(tǒng) MVC 架構(gòu)模式
MVC 即 Model-View-Controller 是軟件開發(fā)中一種常用的架構(gòu)模式,將軟件系統(tǒng)分為三層:模型(Model)、視圖(View)、控制器(Controller),各部分根據(jù)職責(zé)進(jìn)行分離,使程序的結(jié)構(gòu)更為直觀,增加了程序的可擴(kuò)展性、可維護(hù)性、可復(fù)用性。可以用如下的圖形來表示三者之間的關(guān)系。

- 模型(Model):模型封裝了數(shù)據(jù)及對(duì)數(shù)據(jù)的操作,可以直接對(duì)數(shù)據(jù)庫進(jìn)行訪問,不依賴視圖和控制器,也就是說模型并不關(guān)注數(shù)據(jù)如何展示,只負(fù)責(zé)提供數(shù)據(jù)。GUI 程序模型中數(shù)據(jù)的變化一般會(huì)通過觀察者模式通知視圖,而在 web 中則不會(huì)這樣。
- 視圖(View):視圖從模型中拉取數(shù)據(jù),只負(fù)責(zé)展示,沒有具體的程序邏輯。
- 控制器(Controller):控制器用于控制程序的流程,將模型中的數(shù)據(jù)展示到視圖中。
Java Web MVC 架構(gòu)模式
上世紀(jì) 90 年代,隨著互聯(lián)網(wǎng)的發(fā)展,基于瀏覽器的 B/S 模式隨之流行,最初瀏覽器向服務(wù)器請(qǐng)求的都是一些靜態(tài)的資源,如 HTML,CSS 等,為了支持根據(jù)用戶的請(qǐng)求動(dòng)態(tài)的獲取資源,Java 提出了 Servlet 規(guī)范。
此時(shí) Servlet 可以說是一個(gè)大雜燴,瀏覽器接收的 HTML 都是通過 Servelt 一行一行的輸出,比較繁瑣,并且寫后端代碼的程序員還要熟悉前端技術(shù),為了解決這個(gè)問題,sun 公司又借鑒 ASP 提出了 JSP。
JSP 和 HTML 相似,只是在 JSP 文件中可以嵌入 Java 代碼,減少了直接使用 Servlet 產(chǎn)生的大量冗余代碼。此時(shí) JSP 同時(shí)充當(dāng)模型、視圖、控制器的角色,為了解決前后端代碼仍然揉在一起的問題,Java Web MVC 模式后來被提出,JavaBean 充當(dāng)模型、JSP 充當(dāng)視圖,Servlet 充當(dāng)控制器,流程如下圖所示。

瀏覽器的請(qǐng)求先經(jīng)過 Servlet,Servlet 控制整個(gè)流程,使用 JavaBean 查詢并存儲(chǔ)數(shù)據(jù),然后攜帶 JavaBean 中的數(shù)據(jù)到 JSP 頁面中,這個(gè)就是 Java 中早期的 Web MVC 架構(gòu)模式了。
Spring MVC 架構(gòu)模式
Spring MVC 架構(gòu)模式對(duì) Java Web 中的 MVC 架構(gòu)模式加以擴(kuò)展,將控制器拆分為前端控制器 DispatcherServlet 和后端控制器 Controller,將 Model 拆分成業(yè)務(wù)層(Service) 和數(shù)據(jù)訪問層(Respository),并且支持不同的視圖,如 JSP、FreeMarker 等,設(shè)計(jì)更為靈活,請(qǐng)求處理流程如下。

瀏覽器的請(qǐng)求先經(jīng)過 DispatcherServlet,DispatcherServlet 負(fù)責(zé)分發(fā)請(qǐng)求,因此 DispatcherServlet 又被稱為前端控制器。DispatcherServlet 其后的 Controller 又被稱為后端控制器,Controller 可以選擇性的調(diào)用 Service、Repository 實(shí)現(xiàn)業(yè)務(wù)邏輯,DispatcherServlet 拿到 Controller 提供的模型和視圖后,進(jìn)行渲染并返回給瀏覽器。當(dāng)然了,這里只是為了方便理解 Spring MVC 描述的大概流程,具體流程會(huì)在后文介紹。
Hello,Spring MVC
雖然現(xiàn)在 SpringBoot 已經(jīng)成為主流,但是我仍然想從單純的 Spring MVC 講起,因?yàn)?SpringBoot 也只是在 Spring Framework 其上添加了一些自動(dòng)化的配置,這些自動(dòng)化的配置會(huì)讓我們忽略背后的技術(shù)原理。
幾年的 Spring 的教程中都會(huì)提出使用 Spring MVC 首先需要去 Spring 官網(wǎng)下載一大堆的依賴,而現(xiàn)在有了 maven 之后再也不必關(guān)系這些亂七八糟的依賴及其依賴關(guān)系。如果你不了解 maven,建議先去了解 maven 后再回頭看下面的內(nèi)容。
Spring MVC 依賴引入
新建 maven 項(xiàng)目,并引入 Spring MVC 的依賴,注意這里引入的版本號(hào)是 5.2.6,Spring Framework 5 開始對(duì) JDK 版本的要求是 1.8 及以上。完整的 pom 內(nèi)容如下。
<?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.zzuhkp</groupId>
<artifactId>mvc-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>mvc-demo</finalName>
</build>
</project>
DispatcherServlet 聲明
傳統(tǒng)的 Java Web 項(xiàng)目使用 Servlet 處理請(qǐng)求,Spring MVC 遵循了 Servlet 規(guī)范,提供了一個(gè)名稱為 DispatcherServlet 的 Servlet 類,使用 Spring MVC 需要先聲明這個(gè) Servlet。
DispatcherServlet 整合了 IOC 容器,所有處理 Web 請(qǐng)求的組件都存至 IOC 容器中,然后使用這些 bean 處理控制整個(gè)請(qǐng)求過程。
有兩種聲明 DispatcherServlet 的方式,第一種方式是直接在類路徑下的/WEB-INF/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">
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
第二種方式基于 Servlet 3.0 提出的 ServletContainerInitializer 接口,Servlet 容器會(huì)從類路徑中查找實(shí)現(xiàn)了這個(gè)接口的類,并在啟動(dòng)時(shí)回調(diào)這個(gè)接口中的方法,Spring MVC 已經(jīng)將這個(gè)接口實(shí)現(xiàn)為 SpringServletContainerInitializer,在其內(nèi)部調(diào)用了 WebApplicationInitializer 接口完成初始化,因此實(shí)現(xiàn) WebApplicationInitializer 接口再添加 DispatcherServlet 也可以,和上述 xml 等效的 java 代碼如下。
public class MvcXmlInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
XmlWebApplicationContext context = new XmlWebApplicationContext();
DispatcherServlet dispatcher = new DispatcherServlet(context);
Dynamic dynamic = servletContext.addServlet("dispatcher", dispatcher);
dynamic.addMapping("/");
dynamic.setLoadOnStartup(1);
}
}
除了上述用戶自定義的 WebApplicationInitializer,Spring 還自定義了一個(gè)支持注解配置的抽象實(shí)現(xiàn)類 AbstractAnnotationConfigDispatcherServletInitializer,這個(gè)類會(huì)自動(dòng)向 Servlet 上下文中注冊(cè) DispatcherServlet,實(shí)現(xiàn)這個(gè)類然后指定配置類即可。
public class MvcAnnotationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{MvcConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
這里我們使用 web.xml 配置進(jìn)行演示,我們配置的 DispatcherServlet 聲明的映射路徑是/,因此,所有的請(qǐng)求都會(huì)到達(dá) DispatcherServlet,然后再分派給不同的處理器處理。
Spring 上下文配置
Spring MVC 使用 IOC 容器存儲(chǔ)處理請(qǐng)求的組件,包括處理器在內(nèi)的所有自定義的與 Web 請(qǐng)求有關(guān)的組件都需要添加到 Spring 的配置中。
Spring 上下文配置文件指定
DispatcherServlet 初始化時(shí)默認(rèn)使用的容器是 XmlWebApplicationContext,雖然 Spring 預(yù)留了擴(kuò)展點(diǎn)用于修改容器類型,非必要情況下還是建議不要修改,這個(gè)容器默認(rèn)情況下會(huì)使用類路徑下/WEB-INF/{servlet-name}-servlet.xml文件作為容器的配置文件,我們聲明的 DispatcherServlet 名為 dispatcher,因此我們創(chuàng)建/WEB-INF/dispatcher-servlet.xml文件作為容器的配置。另外還可以使用 Servlet 的初始化參數(shù) configLocation 指定 Spring 容器配置文件路徑。
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
Spring 上下文配置文件內(nèi)容
Spring 配置文件內(nèi)容如下。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="/hellohandler" class="com.zzuhkp.mvc.handler.HelloSpringMVCHttpRequestHandler"/>
</beans>
這里聲明了一個(gè)類型為 HelloSpringMVCHttpRequestHandler 的 bean,其 id 為請(qǐng)求路徑/hellohandler,這個(gè)類的定義如下。
public class HelloSpringMVCHttpRequestHandler implements HttpRequestHandler {
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.getWriter().write("Hello,HelloSpringMVCHttpRequestHandler");
}
}
這樣配置的目的是希望當(dāng)/hellohandler請(qǐng)求到達(dá)時(shí),能夠使用我們提供的 HelloSpringMVCHttpRequestHandler 處理請(qǐng)求。
到了這里,將項(xiàng)目發(fā)布到 Tomcat,我這里使用的 Tomcat 版本號(hào)是 9.0.54,可以看到效果如下。

HandlerMapping 配置
那為什么將處理器的 bean id 配置為請(qǐng)求路徑就可以使用這個(gè)處理器進(jìn)行處理呢?Spring MVC 為了靈活的查找處理器內(nèi)部使用了 HandlerMapping 將請(qǐng)求映射到處理器,Spring 默認(rèn)情況下會(huì)使用BeanNameUrlHandlerMapping映射請(qǐng)求,這個(gè)映射器將請(qǐng)求路徑作為 id 查找處理器。除了默認(rèn)情況下使用的這個(gè)映射器,我們還可以配置 SimpleUrlHandlerMapping 映射器,和上述等效的 Spring 配置如下。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="hellohandler" class="com.zzuhkp.mvc.handler.HelloSpringMVCHttpRequestHandler"/>
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="urlMap">
<map>
<entry key="/hellohandler" value-ref="hellohandler"/>
</map>
</property>
</bean>
</beans>
處理器配置
看到這里,細(xì)心的小伙伴可能會(huì)有疑問,說好的 DispatcherServlet 將請(qǐng)求分派給 Controller 呢?這里暫時(shí)不用著急,Controller 其實(shí)是 Spring MVC 的處理器類型之一,這里的 HttpRequestHandler 同樣是 Spring MVC 的處理器。
Spring 對(duì)多種處理器進(jìn)行了支持,具體則是使用 HandlerAdapter 對(duì)處理器進(jìn)行適配,Spring MVC 內(nèi)部已經(jīng)默認(rèn)了一些適配器,HttpRequestHandler 的適配器是 HttpRequestHandlerAdapter,Controller 的適配器 SimpleControllerHandlerAdapter 也是 Spring MVC 默認(rèn)支持的。
默認(rèn)的 HandlerAdapter 已經(jīng)足夠支持日常所需,一般不會(huì)自定義 HandlerAdapter。
下面嘗試使用 Controller 作為處理器處理請(qǐng)求,定義實(shí)現(xiàn) Controller 接口的 HelloSpringMVCController 類如下。
public class HelloSpringMVCController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("/WEB-INF/view/welcome.jsp");
modelAndView.addObject("hello", "HelloSpringMVCController");
return modelAndView;
}
}
然后在 Spring 配置文件中添加這個(gè)類作為 bean。
<bean id="/hellocontroller" class="com.zzuhkp.mvc.handler.HelloSpringMVCController"/>
到了這里,終于可以看到 Controller 了,Controller 處理請(qǐng)求,返回了一個(gè)類型為 ModelAndView 的對(duì)象。
ModelAndView 包含模型和視圖,這里向模型中添加了屬性 hello,并且指定了/WEB-INF/view/welcome.jsp 文件作為視圖名,這個(gè)文件內(nèi)容如下。
<html>
<body>
<h2>Hello,${requestScope.hello}</h2>
</body>
</html>
啟動(dòng) Tomcat 訪問 /hellocontroller 效果如下。

成功將模型中的數(shù)據(jù)展示到視圖。
ViewResolver 配置
為了支持不同的視圖,如 JSP、FreeMarker 等,ModelAndView 中的視圖名稱被設(shè)計(jì)成虛擬的,具體的視圖由視圖解析器 ViewResolver 進(jìn)行解析,默認(rèn)情況下使用的視圖解析器是 InternalResourceViewResolver ,這個(gè)視圖解析器基于 URL 解析視圖。同時(shí)也可以向應(yīng)用上下文中配置自己的視圖解析器。添加自定義的 InternalResourceViewResolver 到 Spring 配置文件。
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/view"/>
<property name="suffix" value=".jsp"/>
</bean>
然后設(shè)置視圖名時(shí)就可以忽略路徑前綴/WEB-INF/view和后綴.jsp,配置前綴后綴后上述示例中 HelloSpringMVCController 就可以將視圖名從 /WEB-INF/view/welcome.jsp 簡化為 welcome。
DispatcherServlet 組件默認(rèn)配置
上述示例中使用了不少 DispatcherServlet 使用的組件,Spring MVC 默認(rèn)情況下已經(jīng)提供了一些,如果需要自定義,則將自定義的組件添加到上下文配置中接口,十分方便,那么 Spring 默認(rèn)情況下使用了哪些組件處理請(qǐng)求呢?
spring-webmvc 模塊類路徑下 org/springframework/web/servlet/DispatcherServlet.properties 文件定義了這些默認(rèn)的配置,具體如下。

基于注解的 Spring WebMVC
基于配置文件的 Spring Web MVC 項(xiàng)目在前些年確實(shí)比較流行,然而現(xiàn)在注解已經(jīng)成為 Spring 開發(fā)的主流。下面通過純注解的方式對(duì)上面的示例進(jìn)行改造。
pom 文件不需要進(jìn)行變化,首先要提供 Spring 配置類。
@ComponentScan("com.zzuhkp.mvc")
public class MvcConfig {
}
這里只添加了組件掃描能力,Spring 會(huì)將給定包下標(biāo)注了 @Component 的類作為 bean 進(jìn)行處理。然后將將這個(gè)類設(shè)置為配置類即可,這里可以參見使用上述提供的 DispatcherServlet 第二種聲明方式。
然后提供基于注解的控制器。
@Controller
public class HelloSpringMVCAnnotationController {
@GetMapping("/helloAnnotationController")
public String helloMVC(@RequestParam("hello") String hello, Model model) {
model.addAttribute("hello", hello);
return "/WEB-INF/view/welcome.jsp";
}
}
基于注解的控制器不需要實(shí)現(xiàn)特定的接口,直接在類上添加 @Controller 注解即可,這里定義了一個(gè)處理 /helloAnnotationController 路徑 GET 請(qǐng)求方式的方法,并且接收 hello 參數(shù),存放至 model 中,然后返回了視圖名。這里直接復(fù)用了上面示例中的視圖。最終效果如下。

基于注解的控制器是 Spring MVC 中設(shè)計(jì)最為靈活的地方,這里可以先考慮下,Spring 是怎么適配用戶自定義的控制器的?控制器方法中的參數(shù)如何賦值呢?如何將控制器方法的返回值解析為視圖?Spring 如何支持 RESTFUL 風(fēng)格的接口的?后面會(huì)寫幾篇文章繼續(xù)分析。
DispatcherServlet 請(qǐng)求處理流程
DispatcherServlet 請(qǐng)求處理流程已經(jīng)穿插在前面的示例中介紹,直接看前面的描述可能不是很直觀,這里總結(jié)了一張圖來梳理整個(gè)流程。

整個(gè)流程串聯(lián)起來如下。
- DispatcherServlet 處理瀏覽器發(fā)起的請(qǐng)求。
- DispatcherServlet 根據(jù)用戶或默認(rèn)的配置使用 HandlerMapping 查找可處理請(qǐng)求的處理器。
- DispatcherServlet 拿到 HandlerMapping 返回的處理器鏈 HandlerExecutionChain。整個(gè)處理器鏈包含攔截器和處理。
- DispatcherServlet 將處理器適配為 HandlerAdapter。
- DispatcherServlet 使用攔截器進(jìn)行請(qǐng)求前置處理。
- DispatcherServlet 使用處理器進(jìn)行請(qǐng)求處理。
- DispatcherServlet 使用攔截器進(jìn)行請(qǐng)求后置處理。
- DispatcherServlet 從攔截器或處理器中提取到模型及視圖 ModelAndView。
- DispatcherServlet 使用視圖解析器 ViewResolver 解析視圖出視圖 View。
- DispatcherServlet 渲染視圖,響應(yīng)請(qǐng)求。
結(jié)束語
本文先介紹 MVC 架構(gòu)模式,然后通過示例的方式對(duì) Spring MVC 的使用方式及執(zhí)行流程進(jìn)行介紹,最后還使用一個(gè)流程圖總結(jié)。
Spring MVC 中所有的擴(kuò)展都基于 DispatcherServlet 處理請(qǐng)求的這個(gè)流程,可以說理解了這個(gè)流程圖就理解了 Spring MVC 的原理,后面將會(huì)對(duì)這個(gè)流程進(jìn)行細(xì)化,繼續(xù)介紹 Spring MVC 的其他內(nèi)容。
到此這篇關(guān)于理解Spring MVC的文章就介紹到這了,更多相關(guān)理解Spring MVC內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談Arrays.asList() 和ArrayList類型區(qū)別
下面小編就為大家?guī)硪黄狝rrays.asList() 和ArrayList類型區(qū)別。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-10-10
基于SpringBoot應(yīng)用監(jiān)控Actuator安全隱患及解決方式
這篇文章主要介紹了SpringBoot應(yīng)用監(jiān)控Actuator安全隱患及解決方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
Java前后端的JSON傳輸方式(前后端JSON格式轉(zhuǎn)換)
這篇文章主要介紹了Java前后端的JSON傳輸方式(前后端JSON格式轉(zhuǎn)換),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04
微服務(wù)實(shí)戰(zhàn)之怎樣提升springboot服務(wù)吞吐量
這篇文章主要介紹了微服務(wù)實(shí)戰(zhàn)之怎樣提升springboot服務(wù)吞吐量方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08
redis?redisTemplate數(shù)據(jù)類型基礎(chǔ)操作
這篇文章主要介紹了redis?redisTemplate數(shù)據(jù)類型基礎(chǔ)操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06
如何解決org.apache.jasper.JasperException:無法為JSP編譯類詳解
這篇文章主要給大家介紹了關(guān)于如何解決org.apache.jasper.JasperException:無法為JSP編譯類的相關(guān)資料,原因可能是JSP文件的語法錯(cuò)誤、類路徑問題或其他配置問題,建議檢查JSP文件的語法、類路徑配置和其他相關(guān)配置,需要的朋友可以參考下2023-06-06

