讓你五分鐘徹底理解Spring MVC

概述

Sping MVC 正式的名字為 Spring Web MVC,是 Spring Framework 框架中的其中一個模塊,基於 Servlet API 構建,同時使用 MVC 的架構模式,主要用以簡化傳統的 Servlet + JSP 進行 web 開發的工作。

MVC 架構模式

Spring MVC 基於 MVC 模式,因此理解 Spring MVC 需要先對 MVC 模式有所瞭解。

傳統 MVC 架構模式

MVC 即 Model-View-Controller 是軟件開發中一種常用的架構模式,將軟件系統分為三層:模型(Model)、視圖(View)、控制器(Controller),各部分根據職責進行分離,使程序的結構更為直觀,增加瞭程序的可擴展性、可維護性、可復用性。可以用如下的圖形來表示三者之間的關系。

MVC 架構模式

  • 模型(Model):模型封裝瞭數據及對數據的操作,可以直接對數據庫進行訪問,不依賴視圖和控制器,也就是說模型並不關註數據如何展示,隻負責提供數據。GUI 程序模型中數據的變化一般會通過觀察者模式通知視圖,而在 web 中則不會這樣。
  • 視圖(View):視圖從模型中拉取數據,隻負責展示,沒有具體的程序邏輯。
  • 控制器(Controller):控制器用於控制程序的流程,將模型中的數據展示到視圖中。

Java Web MVC 架構模式

上世紀 90 年代,隨著互聯網的發展,基於瀏覽器的 B/S 模式隨之流行,最初瀏覽器向服務器請求的都是一些靜態的資源,如 HTML,CSS 等,為瞭支持根據用戶的請求動態的獲取資源,Java 提出瞭 Servlet 規范。

此時 Servlet 可以說是一個大雜燴,瀏覽器接收的 HTML 都是通過 Servelt 一行一行的輸出,比較繁瑣,並且寫後端代碼的程序員還要熟悉前端技術,為瞭解決這個問題,sun 公司又借鑒 ASP 提出瞭 JSP。

JSP 和 HTML 相似,隻是在 JSP 文件中可以嵌入 Java 代碼,減少瞭直接使用 Servlet 產生的大量冗餘代碼。此時 JSP 同時充當模型、視圖、控制器的角色,為瞭解決前後端代碼仍然揉在一起的問題,Java Web MVC 模式後來被提出,JavaBean 充當模型、JSP 充當視圖,Servlet 充當控制器,流程如下圖所示。

Java Web MVC

瀏覽器的請求先經過 Servlet,Servlet 控制整個流程,使用 JavaBean 查詢並存儲數據,然後攜帶 JavaBean 中的數據到 JSP 頁面中,這個就是 Java 中早期的 Web MVC 架構模式瞭。

Spring MVC 架構模式

Spring MVC 架構模式對 Java Web 中的 MVC 架構模式加以擴展,將控制器拆分為前端控制器 DispatcherServlet 和後端控制器 Controller,將 Model 拆分成業務層(Service) 和數據訪問層(Respository),並且支持不同的視圖,如 JSP、FreeMarker 等,設計更為靈活,請求處理流程如下。

Spring Web MVC

瀏覽器的請求先經過 DispatcherServlet,DispatcherServlet 負責分發請求,因此 DispatcherServlet 又被稱為前端控制器。DispatcherServlet 其後的 Controller 又被稱為後端控制器,Controller 可以選擇性的調用 Service、Repository 實現業務邏輯,DispatcherServlet 拿到 Controller 提供的模型和視圖後,進行渲染並返回給瀏覽器。當然瞭,這裡隻是為瞭方便理解 Spring MVC 描述的大概流程,具體流程會在後文介紹。

Hello,Spring MVC

雖然現在 SpringBoot 已經成為主流,但是我仍然想從單純的 Spring MVC 講起,因為 SpringBoot 也隻是在 Spring Framework 其上添加瞭一些自動化的配置,這些自動化的配置會讓我們忽略背後的技術原理。

幾年的 Spring 的教程中都會提出使用 Spring MVC 首先需要去 Spring 官網下載一大堆的依賴,而現在有瞭 maven 之後再也不必關系這些亂七八糟的依賴及其依賴關系。如果你不瞭解 maven,建議先去瞭解 maven 後再回頭看下面的內容。

Spring MVC 依賴引入

新建 maven 項目,並引入 Spring MVC 的依賴,註意這裡引入的版本號是 5.2.6,Spring Framework 5 開始對 JDK 版本的要求是 1.8 及以上。完整的 pom 內容如下。

<?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 聲明

傳統的 Java Web 項目使用 Servlet 處理請求,Spring MVC 遵循瞭 Servlet 規范,提供瞭一個名稱為 DispatcherServlet 的 Servlet 類,使用 Spring MVC 需要先聲明這個 Servlet。

DispatcherServlet 整合瞭 IOC 容器,所有處理 Web 請求的組件都存至 IOC 容器中,然後使用這些 bean 處理控制整個請求過程。

有兩種聲明 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 容器會從類路徑中查找實現瞭這個接口的類,並在啟動時回調這個接口中的方法,Spring MVC 已經將這個接口實現為 SpringServletContainerInitializer,在其內部調用瞭 WebApplicationInitializer 接口完成初始化,因此實現 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 還自定義瞭一個支持註解配置的抽象實現類 AbstractAnnotationConfigDispatcherServletInitializer,這個類會自動向 Servlet 上下文中註冊 DispatcherServlet,實現這個類然後指定配置類即可。

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 配置進行演示,我們配置的 DispatcherServlet 聲明的映射路徑是/,因此,所有的請求都會到達 DispatcherServlet,然後再分派給不同的處理器處理。

Spring 上下文配置

Spring MVC 使用 IOC 容器存儲處理請求的組件,包括處理器在內的所有自定義的與 Web 請求有關的組件都需要添加到 Spring 的配置中。

Spring 上下文配置文件指定

DispatcherServlet 初始化時默認使用的容器是 XmlWebApplicationContext,雖然 Spring 預留瞭擴展點用於修改容器類型,非必要情況下還是建議不要修改,這個容器默認情況下會使用類路徑下/WEB-INF/{servlet-name}-servlet.xml文件作為容器的配置文件,我們聲明的 DispatcherServlet 名為 dispatcher,因此我們創建/WEB-INF/dispatcher-servlet.xml文件作為容器的配置。另外還可以使用 Servlet 的初始化參數 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 上下文配置文件內容

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"/>
        
</beans>

這裡聲明瞭一個類型為 HelloSpringMVCHttpRequestHandler 的 bean,其 id 為請求路徑/hellohandler,這個類的定義如下。

public class HelloSpringMVCHttpRequestHandler implements HttpRequestHandler {


    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        response.getWriter().write("Hello,HelloSpringMVCHttpRequestHandler");
    }

}

這樣配置的目的是希望當/hellohandler請求到達時,能夠使用我們提供的 HelloSpringMVCHttpRequestHandler 處理請求。

到瞭這裡,將項目發佈到 Tomcat,我這裡使用的 Tomcat 版本號是 9.0.54,可以看到效果如下。

HelloSpringMVCRequestHandler

HandlerMapping 配置

那為什麼將處理器的 bean id 配置為請求路徑就可以使用這個處理器進行處理呢?Spring MVC 為瞭靈活的查找處理器內部使用瞭 HandlerMapping 將請求映射到處理器,Spring 默認情況下會使用BeanNameUrlHandlerMapping映射請求,這個映射器將請求路徑作為 id 查找處理器。除瞭默認情況下使用的這個映射器,我們還可以配置 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>

處理器配置

看到這裡,細心的小夥伴可能會有疑問,說好的 DispatcherServlet 將請求分派給 Controller 呢?這裡暫時不用著急,Controller 其實是 Spring MVC 的處理器類型之一,這裡的 HttpRequestHandler 同樣是 Spring MVC 的處理器。

Spring 對多種處理器進行瞭支持,具體則是使用 HandlerAdapter 對處理器進行適配,Spring MVC 內部已經默認瞭一些適配器,HttpRequestHandler 的適配器是 HttpRequestHandlerAdapter,Controller 的適配器 SimpleControllerHandlerAdapter 也是 Spring MVC 默認支持的。

默認的 HandlerAdapter 已經足夠支持日常所需,一般不會自定義 HandlerAdapter。

下面嘗試使用 Controller 作為處理器處理請求,定義實現 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 配置文件中添加這個類作為 bean。

<bean id="/hellocontroller" class="com.zzuhkp.mvc.handler.HelloSpringMVCController"/>

到瞭這裡,終於可以看到 Controller 瞭,Controller 處理請求,返回瞭一個類型為 ModelAndView 的對象。

ModelAndView 包含模型和視圖,這裡向模型中添加瞭屬性 hello,並且指定瞭/WEB-INF/view/welcome.jsp 文件作為視圖名,這個文件內容如下。

<html>
<body>
<h2>Hello,${requestScope.hello}</h2>
</body>
</html>

啟動 Tomcat 訪問 /hellocontroller 效果如下。

HelloSpringMVCController

成功將模型中的數據展示到視圖。

ViewResolver 配置

為瞭支持不同的視圖,如 JSP、FreeMarker 等,ModelAndView 中的視圖名稱被設計成虛擬的,具體的視圖由視圖解析器 ViewResolver 進行解析,默認情況下使用的視圖解析器是 InternalResourceViewResolver ,這個視圖解析器基於 URL 解析視圖。同時也可以向應用上下文中配置自己的視圖解析器。添加自定義的 InternalResourceViewResolver 到 Spring 配置文件。

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/view"/>
        <property name="suffix" value=".jsp"/>
    </bean>

然後設置視圖名時就可以忽略路徑前綴/WEB-INF/view和後綴.jsp,配置前綴後綴後上述示例中 HelloSpringMVCController 就可以將視圖名從 /WEB-INF/view/welcome.jsp 簡化為 welcome

DispatcherServlet 組件默認配置

上述示例中使用瞭不少 DispatcherServlet 使用的組件,Spring MVC 默認情況下已經提供瞭一些,如果需要自定義,則將自定義的組件添加到上下文配置中接口,十分方便,那麼 Spring 默認情況下使用瞭哪些組件處理請求呢?

spring-webmvc 模塊類路徑下 org/springframework/web/servlet/DispatcherServlet.properties 文件定義瞭這些默認的配置,具體如下。

基於註解的 Spring WebMVC

基於配置文件的 Spring Web MVC 項目在前些年確實比較流行,然而現在註解已經成為 Spring 開發的主流。下面通過純註解的方式對上面的示例進行改造。

pom 文件不需要進行變化,首先要提供 Spring 配置類。

@ComponentScan("com.zzuhkp.mvc")
public class MvcConfig {

}

這裡隻添加瞭組件掃描能力,Spring 會將給定包下標註瞭 @Component 的類作為 bean 進行處理。然後將將這個類設置為配置類即可,這裡可以參見使用上述提供的 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";
    }

}

基於註解的控制器不需要實現特定的接口,直接在類上添加 @Controller 註解即可,這裡定義瞭一個處理 /helloAnnotationController 路徑 GET 請求方式的方法,並且接收 hello 參數,存放至 model 中,然後返回瞭視圖名。這裡直接復用瞭上面示例中的視圖。最終效果如下。

基於註解的控制器是 Spring MVC 中設計最為靈活的地方,這裡可以先考慮下,Spring 是怎麼適配用戶自定義的控制器的?控制器方法中的參數如何賦值呢?如何將控制器方法的返回值解析為視圖?Spring 如何支持 RESTFUL 風格的接口的?後面會寫幾篇文章繼續分析。

DispatcherServlet 請求處理流程

DispatcherServlet 請求處理流程已經穿插在前面的示例中介紹,直接看前面的描述可能不是很直觀,這裡總結瞭一張圖來梳理整個流程。

DispatcherServlet 執行流程

整個流程串聯起來如下。

  1. DispatcherServlet 處理瀏覽器發起的請求。
  2. DispatcherServlet 根據用戶或默認的配置使用 HandlerMapping 查找可處理請求的處理器。
  3. DispatcherServlet 拿到 HandlerMapping 返回的處理器鏈 HandlerExecutionChain。整個處理器鏈包含攔截器和處理。
  4. DispatcherServlet 將處理器適配為 HandlerAdapter。
  5. DispatcherServlet 使用攔截器進行請求前置處理。
  6. DispatcherServlet 使用處理器進行請求處理。
  7. DispatcherServlet 使用攔截器進行請求後置處理。
  8. DispatcherServlet 從攔截器或處理器中提取到模型及視圖 ModelAndView。
  9. DispatcherServlet 使用視圖解析器 ViewResolver 解析視圖出視圖 View。
  10. DispatcherServlet 渲染視圖,響應請求。

結束語

本文先介紹 MVC 架構模式,然後通過示例的方式對 Spring MVC 的使用方式及執行流程進行介紹,最後還使用一個流程圖總結。

Spring MVC 中所有的擴展都基於 DispatcherServlet 處理請求的這個流程,可以說理解瞭這個流程圖就理解瞭 Spring MVC 的原理,後面將會對這個流程進行細化,繼續介紹 Spring MVC 的其他內容。

到此這篇關於理解Spring MVC的文章就介紹到這瞭,更多相關理解Spring MVC內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: