1. 程式人生 > >手碼兩萬餘字,SpringMVC 包教包會

手碼兩萬餘字,SpringMVC 包教包會

1. SpringMVC 簡介

1.1 Spring Web MVC是什麼

Spring Web MVC 是一種基於 Java 的實現了 Web MVC 設計模式的請求驅動型別的輕量級 Web 框架,即使用了 MVC 架構模式的思想,將 web 層進行職責解耦,基於請求驅動指的就是使用請求-響應模型,框架的目的就是幫助我們簡化開發,Spring Web MVC 也是要簡化我們日常 Web 開發的。在 傳統的 Jsp/Servlet 技術體系中,如果要開發介面,一個介面對應一個 Servlet,會導致我們開發出許多 Servlet,使用 SpringMVC 可以有效的簡化這一步驟。

Spring Web MVC 也是服務到工作者模式的實現,但進行可優化。前端控制器是 DispatcherServlet;應用控制器可以拆為處理器對映器(Handler Mapping)進行處理器管理和檢視解析器(View Resolver)進行檢視管理;頁面控制器/動作/處理器為 Controller 介面(僅包含 ModelAndView handleRequest(request, response) 方法,也有人稱作 Handler)的實現(也可以是任何的 POJO 類);支援本地化(Locale)解析、主題(Theme)解析及檔案上傳等;提供了非常靈活的資料驗證、格式化和資料繫結機制;提供了強大的約定大於配置(慣例優先原則)的契約式程式設計支援。

1.2 Spring Web MVC能幫我們做什麼

  • 讓我們能非常簡單的設計出乾淨的 Web 層和薄薄的 Web 層;
  • 進行更簡潔的 Web 層的開發;
  • 天生與 Spring 框架整合(如 IoC 容器、AOP 等);
  • 提供強大的約定大於配置的契約式程式設計支援;
  • 能簡單的進行 Web 層的單元測試;
  • 支援靈活的 URL 到頁面控制器的對映;
  • 非常容易與其他檢視技術整合,如 Velocity、FreeMarker 等等,因為模型資料不放在特定的 API 裡,而是放在一個 Model 裡(Map 資料結構實現,因此很容易被其他框架使用);
  • 非常靈活的資料驗證、格式化和資料繫結機制,能使用任何物件進行資料繫結,不必實現特定框架的 API;
  • 提供一套強大的 JSP 標籤庫,簡化 JSP 開發;
  • 支援靈活的本地化、主題等解析;
  • 更加簡單的異常處理;
  • 對靜態資源的支援;
  • 支援 RESTful 風格

2. HelloWorld

接下來,通過一個簡單的例子來感受一下 SpringMVC。

1.利用 Maven 建立一個 web 工程(參考 Maven 教程)。
2.在 pom.xml 檔案中,新增 spring-webmvc 的依賴:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>RELEASE</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.1</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>javax.servlet.jsp-api</artifactId>
        <version>2.3.3</version>
    </dependency>
</dependencies>

添加了 spring-webmvc 依賴之後,其他的 spring-web、spring-aop、spring-context 等等就全部都加入進來了。

3.準備一個 Controller,即一個處理瀏覽器請求的介面。

public class MyController implements Controller {
    /**
     * 這就是一個請求處理介面
     * @param req 這就是前端傳送來的請求
     * @param resp 這就是服務端給前端的響應
     * @return 返回值是一個 ModelAndView,Model 相當於是我們的資料模型,View 是我們的檢視
     * @throws Exception
     */
    public ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        ModelAndView mv = new ModelAndView("hello");
        mv.addObject("name", "javaboy");
        return mv;
    }
}

這裡我們我們創建出來的 Controller 就是前端請求處理介面。

4.建立檢視

這裡我們就採用 jsp 作為檢視,在 webapp 目錄下建立 hello.jsp 檔案,內容如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<h1>hello ${name}!</h1>
</body>
</html>

5.在 resources 目錄下,建立一個名為 spring-servlet.xml 的 springmvc 的配置檔案,這裡,我們先寫一個簡單的 demo ,因此可以先不用新增 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 class="org.javaboy.helloworld.MyController" name="/hello"/>
    <!--這個是處理器對映器,這種方式,請求地址其實就是一個 Bean 的名字,然後根據這個 bean 的名字查詢對應的處理器-->
    <bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id="handlerMapping">
        <property name="beanName" value="/hello"/>
    </bean>
    <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" id="handlerAdapter"/>
    
    <!--檢視解析器-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
        <property name="prefix" value="/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

6.載入 springmvc 配置檔案

在 web 專案啟動時,載入 springmvc 配置檔案,這個配置是在 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>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-servlet.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

所有請求都將自動攔截下來,攔截下來後,請求交給 DispatcherServlet 去處理,在載入 DispatcherServlet 時,還需要指定配置檔案路徑。這裡有一個預設的規則,如果配置檔案放在 webapp/WEB-INF/ 目錄下,並且配置檔案的名字等於 DispatcherServlet 的名字+ -servlet(即這裡的配置檔案路徑是 webapp/WEB-INF/springmvc-servlet.xml),如果是這樣的話,可以不用新增 init-param 引數,即不用手動配置 springmvc 的配置檔案,框架會自動載入。

7.配置並啟動專案(參考 Maven 教程)

8.專案啟動成功後,瀏覽器輸入 http://localhost:8080/hello 就可以看到如下頁面:

3. SpringMVC 工作流程

面試時,關於 SpringMVC 的問題,超過 99% 都是這個問題。

4. SpringMVC 中的元件

1.DispatcherServlet:前端控制器

使用者請求到達前端控制器,它就相當於 mvc 模式中的c,DispatcherServlet 是整個流程控制的中心,相當於是 SpringMVC 的大腦,由它呼叫其它元件處理使用者的請求,DispatcherServlet 的存在降低了元件之間的耦合性。

2.HandlerMapping:處理器對映器

HandlerMapping 負責根據使用者請求找到 Handler 即處理器(也就是我們所說的 Controller),SpringMVC 提供了不同的對映器實現不同的對映方式,例如:配置檔案方式,實現介面方式,註解方式等,在實際開發中,我們常用的方式是註解方式。

3.Handler:處理器

Handler 是繼 DispatcherServlet 前端控制器的後端控制器,在DispatcherServlet 的控制下 Handler 對具體的使用者請求進行處理。由於 Handler 涉及到具體的使用者業務請求,所以一般情況需要程式設計師根據業務需求開發 Handler。(這裡所說的 Handler 就是指我們的 Controller)

4.HandlAdapter:處理器介面卡

通過 HandlerAdapter 對處理器進行執行,這是介面卡模式的應用,通過擴充套件介面卡可以對更多型別的處理器進行執行。

5.ViewResolver:檢視解析器

ViewResolver 負責將處理結果生成 View 檢視,ViewResolver 首先根據邏輯檢視名解析成物理檢視名即具體的頁面地址,再生成 View 檢視物件,最後對 View 進行渲染將處理結果通過頁面展示給使用者。 SpringMVC 框架提供了很多的 View 檢視型別,包括:jstlView、freemarkerView、pdfView 等。一般情況下需要通過頁面標籤或頁面模版技術將模型資料通過頁面展示給使用者,需要由程式設計師根據業務需求開發具體的頁面。

5. DispatcherServlet

5.1 DispatcherServlet作用

DispatcherServlet 是前端控制器設計模式的實現,提供 Spring Web MVC 的集中訪問點,而且負責職責的分派,而且與 Spring IoC 容器無縫整合,從而可以獲得 Spring 的所有好處。DispatcherServlet 主要用作職責排程工作,本身主要用於控制流程,主要職責如下:

  1. 檔案上傳解析,如果請求型別是 multipart 將通過 MultipartResolver 進行檔案上傳解析;
  2. 通過 HandlerMapping,將請求對映到處理器(返回一個 HandlerExecutionChain,它包括一個處理器、多個 HandlerInterceptor 攔截器);
  3. 通過 HandlerAdapter 支援多種型別的處理器(HandlerExecutionChain 中的處理器);
  4. 通過 ViewResolver 解析邏輯檢視名到具體檢視實現;
  5. 本地化解析;
  6. 渲染具體的檢視等;
  7. 如果執行過程中遇到異常將交給 HandlerExceptionResolver 來解析

5.2 DispathcherServlet配置詳解

<servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>springmvc</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>
  • load-on-startup:表示啟動容器時初始化該 Servlet;
  • url-pattern:表示哪些請求交給 Spring Web MVC 處理, "/" 是用來定義預設 servlet 對映的。也可以如 *.html 表示攔截所有以 html 為副檔名的請求
  • contextConfigLocation:表示 SpringMVC 配置檔案的路徑

其他的引數配置:

引數 描述
contextClass 實現WebApplicationContext介面的類,當前的servlet用它來建立上下文。如果這個引數沒有指定, 預設使用XmlWebApplicationContext。
contextConfigLocation 傳給上下文例項(由contextClass指定)的字串,用來指定上下文的位置。這個字串可以被分成多個字串(使用逗號作為分隔符) 來支援多個上下文(在多上下文的情況下,如果同一個bean被定義兩次,後面一個優先)。
namespace WebApplicationContext名稱空間。預設值是[server-name]-servlet。

5.3 Spring 配置

之前的案例中,只有 SpringMVC,沒有 Spring,Web 專案也是可以執行的。在實際開發中,Spring 和 SpringMVC 是分開配置的,所以我們對上面的專案繼續進行完善,新增 Spring 相關配置。

首先,專案新增一個 service 包,提供一個 HelloService 類,如下:

@Service
public class HelloService {
    public String hello(String name) {
        return "hello " + name;
    }
}

現在,假設我需要將 HelloService 注入到 Spring 容器中並使用它,這個是屬於 Spring 層的 Bean,所以我們一般將除了 Controller 之外的所有 Bean 註冊到 Spring 容器中,而將 Controller 註冊到 SpringMVC 容器中,現在,在 resources 目錄下新增 applicationContext.xml 作為 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"
       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 https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.javaboy" use-default-filters="true">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
</beans>

但是,這個配置檔案,預設情況下,並不會被自動載入,所以,需要我們在 web.xml 中對其進行配置:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

首先通過 context-param 指定 Spring 配置檔案的位置,這個配置檔案也有一些預設規則,它的配置檔名預設就叫 applicationContext.xml ,並且,如果你將這個配置檔案放在 WEB-INF 目錄下,那麼這裡就可以不用指定配置檔案位置了,只需要指定監聽器就可以了。這段配置是 Spring 整合 Web 環境的通用配置;一般用於載入除 Web 層的 Bean(如DAO、Service 等),以便於與其他任何Web框架整合。

  • contextConfigLocation:表示用於載入 Bean 的配置檔案;
  • contextClass:表示用於載入 Bean 的 ApplicationContext 實現類,預設 WebApplicationContext。

配置完成之後,還需要修改 MyController,在 MyController 中注入 HelloSerivce:

@org.springframework.stereotype.Controller("/hello")
public class MyController implements Controller {
    @Autowired
    HelloService helloService;
    /**
     * 這就是一個請求處理介面
     * @param req 這就是前端傳送來的請求
     * @param resp 這就是服務端給前端的響應
     * @return 返回值是一個 ModelAndView,Model 相當於是我們的資料模型,View 是我們的檢視
     * @throws Exception
     */
    public ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        System.out.println(helloService.hello("javaboy"));
        ModelAndView mv = new ModelAndView("hello");
        mv.addObject("name", "javaboy");
        return mv;
    }
}

注意

為了在 SpringMVC 容器中能夠掃描到 MyController ,這裡給 MyController 添加了 @Controller 註解,同時,由於我們目前採用的 HandlerMapping 是 BeanNameUrlHandlerMapping(意味著請求地址就是處理器 Bean 的名字),所以,還需要手動指定 MyController 的名字。

最後,修改 SpringMVC 的配置檔案,將 Bean 配置為掃描形式:

<context:component-scan base-package="org.javaboy.helloworld" use-default-filters="false">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--這個是處理器對映器,這種方式,請求地址其實就是一個 Bean 的名字,然後根據這個 bean 的名字查詢對應的處理器-->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id="handlerMapping">
    <property name="beanName" value="/hello"/>
</bean>
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" id="handlerAdapter"/>
<!--檢視解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
    <property name="prefix" value="/jsp/"/>
    <property name="suffix" value=".jsp"/>
</bean>

配置完成後,再次啟動專案,Spring 容器也將會被建立。訪問 /hello 介面,HelloService 中的 hello 方法就會自動被呼叫。

5.4 兩個容器

當 Spring 和 SpringMVC 同時出現,我們的專案中將存在兩個容器,一個是 Spring 容器,另一個是 SpringMVC 容器,Spring 容器通過 ContextLoaderListener 來載入,SpringMVC 容器則通過 DispatcherServlet 來載入,這兩個容器不一樣:

從圖中可以看出:

  • ContextLoaderListener 初始化的上下文載入的 Bean 是對於整個應用程式共享的,不管是使用什麼表現層技術,一般如 DAO 層、Service 層 Bean;
  • DispatcherServlet 初始化的上下文載入的 Bean 是隻對 Spring Web MVC 有效的 Bean,如 Controller、HandlerMapping、HandlerAdapter 等等,該初始化上下文應該只加載 Web相關元件。
  1. 為什麼不在 Spring 容器中掃描所有 Bean?

這個是不可能的。因為請求達到服務端後,找 DispatcherServlet 去處理,只會去 SpringMVC 容器中找,這就意味著 Controller 必須在 SpringMVC 容器中掃描。

2.為什麼不在 SpringMVC 容器中掃描所有 Bean?

這個是可以的,可以在 SpringMVC 容器中掃描所有 Bean。不寫在一起,有兩個方面的原因:

  1. 為了方便配置檔案的管理
  2. 在 Spring+SpringMVC+Hibernate 組合中,實際上也不支援這種寫法

6. 處理器詳解

6.1 HandlerMapping

注意,下文所說的處理器即我們平時所見到的 Controller

HandlerMapping ,中文譯作處理器對映器,在 SpringMVC 中,系統提供了很多 HandlerMapping:

HandlerMapping 是負責根據 request 請求找到對應的 Handler 處理器及 Interceptor 攔截器,將它們封裝在 HandlerExecutionChain 物件中返回給前端控制器。

  • BeanNameUrlHandlerMapping

BeanNameUrl 處理器對映器,根據請求的 url 與 Spring 容器中定義的 bean 的 name 進行匹配,從而從 Spring 容器中找到 bean 例項,就是說,請求的 Url 地址就是處理器 Bean 的名字。

這個 HandlerMapping 配置如下:

<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id="handlerMapping">
    <property name="beanName" value="/hello"/>
</bean>
  • SimpleUrlHandlerMapping

SimpleUrlHandlerMapping 是 BeanNameUrlHandlerMapping 的增強版本,它可以將 url 和處理器 bean 的 id 進行統一對映配置:

<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping" id="handlerMapping">
    <property name="mappings">
        <props>
            <prop key="/hello">myController</prop>
            <prop key="/hello2">myController2</prop>
        </props>
    </property>
</bean>

注意,在 props 中,可以配置多個請求路徑和處理器例項的對映關係。

6.2 HandlerAdapter

HandlerAdapter,中文譯作處理器介面卡。

HandlerAdapter 會根據介面卡介面對後端控制器進行包裝(適配),包裝後即可對處理器進行執行,通過擴充套件處理器介面卡可以執行多種型別的處理器,這裡使用了介面卡設計模式。

在 SpringMVC 中,HandlerAdapter 也有諸多實現類:

  • SimpleControllerHandlerAdapter

SimpleControllerHandlerAdapter 簡單控制器處理器介面卡,所有實現了 org.springframework.web.servlet.mvc.Controller 介面的 Bean 通過此介面卡進行適配、執行,也就是說,如果我們開發的介面是通過實現 Controller 介面來完成的(不是通過註解開發的介面),那麼 HandlerAdapter 必須是 SimpleControllerHandlerAdapter。

<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />
  • HttpRequestHandlerAdapter

HttpRequestHandlerAdapter,http 請求處理器介面卡,所有實現了 org.springframework.web.HttpRequestHandler 介面的 Bean 通過此介面卡進行適配、執行。

例如存在如下介面:

@Controller
public class MyController2 implements HttpRequestHandler {
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("-----MyController2-----");
    }
}
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping" id="handlerMapping">
    <property name="mappings">
        <props>
            <prop key="/hello2">myController2</prop>
        </props>
    </property>
</bean>
<bean class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter" id="handlerAdapter"/>

6.3 最佳實踐

各種情況都大概瞭解了,我們看下專案中的具體實踐。

  • 元件自動掃描

web 開發中,我們基本上不再通過 XML 或者 Java 配置來建立一個 Bean 的例項,而是直接通過元件掃描來實現 Bean 的配置,如果要掃描多個包,多個包之間用 , 隔開即可:

<context:component-scan base-package="org.sang"/>
  • HandlerMapping

正常情況下,我們在專案中使用的是 RequestMappingHandlerMapping,這個是根據處理器中的註解,來匹配請求(即 @RequestMapping 註解中的 url 屬性)。因為在上面我們都是通過實現類來開發介面的,相當於還是一個類一個介面,所以,我們可以通過 RequestMappingHandlerMapping 來做處理器對映器,這樣我們可以在一個類中開發出多個介面。

  • HandlerAdapter

對於上面提到的通過 @RequestMapping 註解所定義出來的介面方法,這些方法的呼叫都是要通過 RequestMappingHandlerAdapter 這個介面卡來實現。

例如我們開發一個介面:

@Controller
public class MyController3 {
    @RequestMapping("/hello3")
    public ModelAndView hello() {
        return new ModelAndView("hello3");
    }
}

要能夠訪問到這個介面,我們需要 RequestMappingHandlerMapping 才能定位到需要執行的方法,需要 RequestMappingHandlerAdapter,才能執行定位到的方法,修改 springmvc 的配置檔案如下:

<?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 https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.javaboy.helloworld"/>

    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" id="handlerMapping"/>
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" id="handlerAdapter"/>
    <!--檢視解析器-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
        <property name="prefix" value="/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

然後,啟動專案,訪問 /hello3 介面,就可以看到相應的頁面了。

  • 繼續優化

由於開發中,我們常用的是 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter ,這兩個有一個簡化的寫法,如下:

<mvc:annotation-driven>

可以用這一行配置,代替 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter 的兩行配置。

<?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"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <context:component-scan base-package="org.javaboy.helloworld"/>

    <mvc:annotation-driven/>
    <!--檢視解析器-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
        <property name="prefix" value="/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

訪問效果和上一步的效果一樣。這是我們實際開發中,最終配置的形態。

7.1 @RequestMapping

這個註解用來標記一個介面,這算是我們在介面開發中,使用最多的註解之一。

7.1.1 請求 URL

標記請求 URL 很簡單,只需要在相應的方法上新增該註解即可:

@Controller
public class HelloController {
    @RequestMapping("/hello")
    public ModelAndView hello() {
        return new ModelAndView("hello");
    }
}

這裡 @RequestMapping("/hello") 表示當請求地址為 /hello 的時候,這個方法會被觸發。其中,地址可以是多個,就是可以多個地址對映到同一個方法。

@Controller
public class HelloController {
    @RequestMapping({"/hello","/hello2"})
    public ModelAndView hello() {
        return new ModelAndView("hello");
    }
}

這個配置,表示 /hello 和 /hello2 都可以訪問到該方法。

7.1.2 請求窄化

同一個專案中,會存在多個介面,例如訂單相關的介面都是 /order/xxx 格式的,使用者相關的介面都是 /user/xxx 格式的。為了方便處理,這裡的字首(就是 /order、/user)可以統一在 Controller 上面處理。

@Controller
@RequestMapping("/user")
public class HelloController {
    @RequestMapping({"/hello","/hello2"})
    public ModelAndView hello() {
        return new ModelAndView("hello");
    }
}

當類上加了 @RequestMapping 註解之後,此時,要想訪問到 hello ,地址就應該是 /user/hello 或者 /user/hello2

7.1.3 請求方法限定

預設情況下,使用 @RequestMapping 註解定義好的方法,可以被 GET 請求訪問到,也可以被 POST 請求訪問到,但是 DELETE 請求以及 PUT 請求不可以訪問到。

當然,我們也可以指定具體的訪問方法:

@Controller
@RequestMapping("/user")
public class HelloController {
    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    public ModelAndView hello() {
        return new ModelAndView("hello");
    }
}

通過 @RequestMapping 註解,指定了該介面只能被 GET 請求訪問到,此時,該介面就不可以被 POST 以及請求請求訪問到了。強行訪問會報如下錯誤:

當然,限定的方法也可以有多個:

@Controller
@RequestMapping("/user")
public class HelloController {
    @RequestMapping(value = "/hello",method = {RequestMethod.GET,RequestMethod.POST,RequestMethod.PUT,RequestMethod.DELETE})
    public ModelAndView hello() {
        return new ModelAndView("hello");
    }
}

此時,這個介面就可以被 GET、POST、PUT、以及 DELETE 訪問到了。但是,由於 JSP 支支援 GET、POST 以及 HEAD ,所以這個測試,不能使用 JSP 做頁面模板。可以講檢視換成其他的,或者返回 JSON,這裡就不影響了。

7.2 Controller 方法的返回值

7.2.1 返回 ModelAndView

如果是前後端不分的開發,大部分情況下,我們返回 ModelAndView,即資料模型+檢視:

@Controller
@RequestMapping("/user")
public class HelloController {
    @RequestMapping("/hello")
    public ModelAndView hello() {
        ModelAndView mv = new ModelAndView("hello");
        mv.addObject("username", "javaboy");
        return mv;
    }
}

Model 中,放我們的資料,然後在 ModelAndView 中指定檢視名稱。

7.2.2 返回 Void

沒有返回值。沒有返回值,並不一定真的沒有返回值,只是方法的返回值為 void,我們可以通過其他方式給前端返回。實際上,這種方式也可以理解為 Servlet 中的那一套方案。

注意,由於預設的 Maven 專案沒有 Servlet,因此這裡需要額外新增一個依賴:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
</dependency>
  • 通過 HttpServletRequest 做服務端跳轉
@RequestMapping("/hello2")
public void hello2(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    req.getRequestDispatcher("/jsp/hello.jsp").forward(req,resp);//伺服器端跳轉
}
  • 通過 HttpServletResponse 做重定向
@RequestMapping("/hello3")
public void hello3(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    resp.sendRedirect("/hello.jsp");
}

也可以自己手動指定響應頭去實現重定向:

@RequestMapping("/hello3")
public void hello3(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    resp.setStatus(302);
    resp.addHeader("Location", "/jsp/hello.jsp");
}
  • 通過 HttpServletResponse 給出響應
@RequestMapping("/hello4")
public void hello4(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    resp.setContentType("text/html;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write("hello javaboy!");
    out.flush();
    out.close();
}

這種方式,既可以返回 JSON,也可以返回普通字串。

7.2.3 返回字串

  • 返回邏輯檢視名

前面的 ModelAndView 可以拆分為兩部分,Model 和 View,在 SpringMVC 中,Model 我們可以直接在引數中指定,然後返回值是邏輯檢視名:

@RequestMapping("/hello5")
public String hello5(Model model) {
    model.addAttribute("username", "javaboy");//這是資料模型
    return "hello";//表示去查詢一個名為 hello 的檢視
}
  • 服務端跳轉
@RequestMapping("/hello5")
public String hello5() {
    return "forward:/jsp/hello.jsp";
}

forward 後面跟上跳轉的路徑。

  • 客戶端跳轉
@RequestMapping("/hello5")
public String hello5() {
    return "redirect:/user/hello";
}

這種,本質上就是瀏覽器重定向。

  • 真的返回一個字串

上面三個返回的字串,都是由特殊含義的,如果一定要返回一個字串,需要額外新增一個注意:@ResponseBody ,這個註解表示當前方法的返回值就是要展示出來返回值,沒有特殊含義。

@RequestMapping("/hello5")
@ResponseBody
public String hello5() {
    return "redirect:/user/hello";
}

上面程式碼表示就是想返回一段內容為 redirect:/user/hello 的字串,他沒有特殊含義。注意,這裡如果單純的返回一箇中文字串,是會亂碼的,可以在 @RequestMapping 中新增 produces 屬性來解決:

@RequestMapping(value = "/hello5",produces = "text/html;charset=utf-8")
@ResponseBody
public String hello5() {
    return "Java 語言程式設計";
}

7.3 引數繫結

7.3.1 預設支援的引數型別

預設支援的引數型別,就是可以直接寫在 @RequestMapping 所註解的方法中的引數型別,一共有四類:

  • HttpServletRequest
  • HttpServletResponse
  • HttpSession
  • Model/ModelMap

這幾個例子可以參考上一小節。

在請求的方法中,預設的引數就是這幾個,如果在方法中,剛好需要這幾個引數,那麼就可以把這幾個引數加入到方法中。

7.3.2 簡單資料型別

Integer、Boolean、Double 等等簡單資料型別也都是支援的。例如新增一本書:

首先,在 /jsp/ 目錄下建立 add book.jsp 作為圖書新增頁面:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<form action="/doAdd" method="post">
    <table>
        <tr>
            <td>書名:</td>
            <td><input type="text" name="name"></td>
        </tr>
        <tr>
            <td>作者:</td>
            <td><input type="text" name="author"></td>
        </tr>
        <tr>
            <td>價格:</td>
            <td><input type="text" name="price"></td>
        </tr>
        <tr>
            <td>是否上架:</td>
            <td>
                <input type="radio" value="true" name="ispublic">是
                <input type="radio" value="false" name="ispublic">否
            </td>
        </tr>
        <tr>
           <td colspan="2">
               <input type="submit" value="新增">
           </td>
        </tr>
    </table>
</form>
</body>
</html>

建立控制器,控制器提供兩個功能,一個是訪問 jsp 頁面,另一個是提供新增介面:

@Controller
public class BookController {
    @RequestMapping("/book")
    public String addBook() {
        return "addbook";
    }

    @RequestMapping(value = "/doAdd",method = RequestMethod.POST)
    @ResponseBody
    public void doAdd(String name,String author,Double price,Boolean ispublic) {
        System.out.println(name);
        System.out.println(author);
        System.out.println(price);
        System.out.println(ispublic);
    }
}

注意,由於 doAdd 方法確實不想返回任何值,所以需要給該方法新增 @ResponseBody 註解,表示這個方法到此為止,不用再去查詢相關檢視了。另外, POST 請求傳上來的中文會亂碼,所以,我們在 web.xml 中再額外新增一個編碼過濾器:

<filter>
    <filter-name>encoding</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceRequestEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
    <init-param>
        <param-name>forceResponseEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>encoding</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

最後,瀏覽器中輸入 http://localhost:8080/book ,就可以執行新增操作,服務端會打印出來相應的日誌。

在上面的繫結中,有一個要求,表單中欄位的 name 屬性要和介面中的變數名一一對應,才能對映成功,否則服務端接收不到前端傳來的資料。有一些特殊情況,我們的服務端的介面變數名可能和前端不一致,這個時候我們可以通過 @RequestParam 註解來解決。

  • @RequestParam

這個註解的的功能主要有三方面:

  1. 給變數取別名
  2. 設定變數是否必填
  3. 給變數設定預設值

如下:

@RequestMapping(value = "/doAdd",method = RequestMethod.POST)
@ResponseBody
public void doAdd(@RequestParam("name") String bookname, String author, Double price, Boolean ispublic) {
    System.out.println(bookname);
    System.out.println(author);
    System.out.println(price);
    System.out.println(ispublic);
}

註解中的 “name” 表示給 bookname 這個變數取的別名,也就是說,bookname 將接收前端傳來的 name 這個變數的值。在這個註解中,還可以新增 required 屬性和 defaultValue 屬性,如下:

@RequestMapping(value = "/doAdd",method = RequestMethod.POST)
@ResponseBody
public void doAdd(@RequestParam(value = "name",required = true,defaultValue = "三國演義") String bookname, String author, Double price, Boolean ispublic) {
    System.out.println(bookname);
    System.out.println(author);
    System.out.println(price);
    System.out.println(ispublic);
}

required 屬性預設為 true,即只要添加了 @RequestParam 註解,這個引數預設就是必填的,如果不填,請求無法提交,會報 400 錯誤,如果這個引數不是必填項,可以手動把 required 屬性設定為 false。但是,如果同時設定了 defaultValue,這個時候,前端不傳該引數到後端,即使 required 屬性為 true,它也不會報錯。

7.3.3 實體類

引數除了是簡單資料型別之外,也可以是實體類。實際上,在開發中,大部分情況下,都是實體類。

還是上面的例子,我們改用一個 Book 物件來接收前端傳來的資料:

public class Book {
    private String name;
    private String author;
    private Double price;
    private Boolean ispublic;

    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", author='" + author + '\'' +
                ", price=" + price +
                ", ispublic=" + ispublic +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    public Boolean getIspublic() {
        return ispublic;
    }

    public void setIspublic(Boolean ispublic) {
        this.ispublic = ispublic;
    }
}

服務端接收資料方式如下:

@RequestMapping(value = "/doAdd",method = RequestMethod.POST)
@ResponseBody
public void doAdd(Book book) {
    System.out.println(book);
}

前端頁面傳值的時候和上面的一樣,只需要寫屬性名就可以了,不需要寫 book 物件名。

當然,物件中可能還有物件。例如如下物件:

public class Book {
    private String name;
    private Double price;
    private Boolean ispublic;
    private Author author;

    public void setAuthor(Author author) {
        this.author = author;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", price=" + price +
                ", ispublic=" + ispublic +
                ", author=" + author +
                '}';
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    public Boolean getIspublic() {
        return ispublic;
    }

    public void setIspublic(Boolean ispublic) {
        this.ispublic = ispublic;
    }
}
public class Author {
    private String name;
    private Integer age;

    @Override
    public String toString() {
        return "Author{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Book 物件中,有一個 Author 屬性,如何給 Author 屬性傳值呢?前端寫法如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<form action="/doAdd" method="post">
    <table>
        <tr>
            <td>書名:</td>
            <td><input type="text" name="name"></td>
        </tr>
        <tr>
            <td>作者姓名:</td>
            <td><input type="text" name="author.name"></td>
        </tr>
        <tr>
            <td>作者年齡:</td>
            <td><input type="text" name="author.age"></td>
        </tr>
        <tr>
            <td>價格:</td>
            <td><input type="text" name="price"></td>
        </tr>
        <tr>
            <td>是否上架:</td>
            <td>
                <input type="radio" value="true" name="ispublic">是
                <input type="radio" value="false" name="ispublic">否
            </td>
        </tr>
        <tr>
           <td colspan="2">
               <input type="submit" value="新增">
           </td>
        </tr>
    </table>
</form>
</body>
</html>

這樣在後端直接用 Book 物件就可以接收到所有資料了。

7.3.4 自定義引數繫結

前面的轉換,都是系統自動轉換的,這種轉換僅限於基本資料型別。特殊的資料型別,系統無法自動轉換,例如日期。例如前端傳一個日期到後端,後端不是用字串接收,而是使用一個 Date 物件接收,這個時候就會出現引數型別轉換失敗。這個時候,需要我們手動定義引數型別轉換器,將日期字串手動轉為一個 Date 物件。

@Component
public class DateConverter implements Converter<String, Date> {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    public Date convert(String source) {
        try {
            return sdf.parse(source);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }
}

在自定義的引數型別轉換器中,將一個 String 轉為 Date 物件,同時,將這個轉換器註冊為一個 Bean。

接下來,在 SpringMVC 的配置檔案中,配置該 Bean,使之生效。

<mvc:annotation-driven conversion-service="conversionService"/>
<bean class="org.springframework.format.support.FormattingConversionServiceFactoryBean" id="conversionService">
    <property name="converters">
        <set>
            <ref bean="dateConverter"/>
        </set>
    </property>
</bean>

配置完成後,在服務端就可以接收前端傳來的日期引數了。

7.3.5 集合類的引數

  • String 陣列

String 陣列可以直接用陣列去接收,前端傳遞的時候,陣列的傳遞其實就多相同的 key,這種一般用在 checkbox 中較多。

例如前端增加興趣愛好一項:

<form action="/doAdd" method="post">
    <table>
        <tr>
            <td>書名:</td>
            <td><input type="text" name="name"></td>
        </tr>
        <tr>
            <td>作者姓名:</td>
            <td><input type="text" name="author.name"></td>
        </tr>
        <tr>
            <td>作者年齡:</td>
            <td><input type="text" name="author.age"></td>
        </tr>
        <tr>
            <td>出生日期:</td>
            <td><input type="date" name="author.birthday"></td>
        </tr>
        <tr>
            <td>興趣愛好:</td>
            <td>
                <input type="checkbox" name="favorites" value="足球">足球
                <input type="checkbox" name="favorites" value="籃球">籃球
                <input type="checkbox" name="favorites" value="乒乓球">乒乓球
            </td>
        </tr>
        <tr>
            <td>價格:</td>
            <td><input type="text" name="price"></td>
        </tr>
        <tr>
            <td>是否上架:</td>
            <td>
                <input type="radio" value="true" name="ispublic">是
                <input type="radio" value="false" name="ispublic">否
            </td>
        </tr>
        <tr>
           <td colspan="2">
               <input type="submit" value="新增">
           </td>
        </tr>
    </table>
</form>

在服務端用一個數組去接收 favorites 物件:

@RequestMapping(value = "/doAdd",method = RequestMethod.POST)
@ResponseBody
public void doAdd(Book book,String[] favorites) {
    System.out.println(Arrays.toString(favorites));
    System.out.println(book);
}

注意,前端傳來的陣列物件,服務端不可以使用 List 集合去接收。

  • List 集合

如果需要使用 List 集合接收前端傳來的資料,List 集合本身需要放在一個封裝物件中,這個時候,List 中,可以是基本資料型別,也可以是物件。例如有一個班級類,班級裡邊有學生,學生有多個:

public class MyClass {
    private Integer id;
    private List<Student> students;

    @Override
    public String toString() {
        return "MyClass{" +
                "id=" + id +
                ", students=" + students +
                '}';
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public List<Student> getStudents() {
        return students;
    }

    public void setStudents(List<Student> students) {
        this.students = students;
    }
}
public class Student {
    private Integer id;
    private String name;

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

新增班級的時候,可以傳遞多個 Student,前端頁面寫法如下:

<form action="/addclass" method="post">
    <table>
        <tr>
            <td>班級編號:</td>
            <td><input type="text" name="id"></td>
        </tr>
        <tr>
            <td>學生編號:</td>
            <td><input type="text" name="students[0].id"></td>
        </tr>
        <tr>
            <td>學生姓名:</td>
            <td><input type="text" name="students[0].name"></td>
        </tr>
        <tr>
            <td>學生編號:</td>
            <td><input type="text" name="students[1].id"></td>
        </tr>
        <tr>
            <td>學生姓名:</td>
            <td><input type="text" name="students[1].name"></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="提交">
            </td>
        </tr>
    </table>
</form>

服務端直接接收資料即可:

@RequestMapping("/addclass")
@ResponseBody
public void addClass(MyClass myClass) {
    System.out.println(myClass);
}
  • Map

相對於實體類而言,Map 是一種比較靈活的方案,但是,Map 可維護性比較差,因此一般不推薦使用。

例如給上面的班級類新增其他屬性資訊:

public class MyClass {
    private Integer id;
    private List<Student> students;
    private Map<String, Object> info;

    @Override
    public String toString() {
        return "MyClass{" +
                "id=" + id +
                ", students=" + students +
                ", info=" + info +
                '}';
    }

    public Map<String, Object> getInfo() {
        return info;
    }

    public void setInfo(Map<String, Object> info) {
        this.info = info;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public List<Student> getStudents() {
        return students;
    }

    public void setStudents(List<Student> students) {
        this.students = students;
    }
}

在前端,通過如下方式給 info 這個 Map 賦值。

<form action="/addclass" method="post">
    <table>
        <tr>
            <td>班級編號:</td>
            <td><input type="text" name="id"></td>
        </tr>
        <tr>
            <td>班級名稱:</td>
            <td><input type="text" name="info['name']"></td>
        </tr>
        <tr>
            <td>班級位置:</td>
            <td><input type="text" name="info['pos']"></td>
        </tr>
        <tr>
            <td>學生編號:</td>
            <td><input type="text" name="students[0].id"></td>
        </tr>
        <tr>
            <td>學生姓名:</td>
            <td><input type="text" name="students[0].name"></td>
        </tr>
        <tr>
            <td>學生編號:</td>
            <td><input type="text" name="students[1].id"></td>
        </tr>
        <tr>
            <td>學生姓名:</td>
            <td><input type="text" name="students[1].name"></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="提交">
            </td>
        </tr>
    </table>
</form>

8. 檔案上傳

SpringMVC 中對檔案上傳做了封裝,我們可以更加方便的實現檔案上傳。從 Spring3.1 開始,對於檔案上傳,提供了兩個處理器:

  • CommonsMultipartResolver
  • StandardServletMultipartResolver

第一個處理器相容性較好,可以相容 Servlet3.0 之前的版本,但是它依賴了 commons-fileupload 這個第三方工具,所以如果使用這個,一定要新增 commons-fileupload 依賴。

第二個處理器相容性較差,它適用於 Servlet3.0 之後的版本,它不依賴第三方工具,使用它,可以直接做檔案上傳。

8.1 CommonsMultipartResolver

使用 CommonsMultipartResolver 做檔案上傳,需要首先新增 commons-fileupload 依賴,如下:

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>

然後,在 SpringMVC 的配置檔案中,配置 MultipartResolver:

<bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver" id="multipartResolver"/>

注意,這個 Bean 一定要有 id,並且 id 必須是 multipartResolver

接下來,建立 jsp 頁面:

<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file">
    <input type="submit" value="上傳">
</form>

注意檔案上傳請求是 POST 請求,enctype 一定是 multipart/form-data

然後,開發檔案上傳介面:

@Controller
public class FileUploadController {
    SimpleDateFormat sdf = new SimpleDateFormat("/yyyy/MM/dd/");

    @RequestMapping("/upload")
    @ResponseBody
    public String upload(MultipartFile file, HttpServletRequest req) {
        String format = sdf.format(new Date());
        String realPath = req.getServletContext().getRealPath("/img") + format;
        File folder = new File(realPath);
        if (!folder.exists()) {
            folder.mkdirs();
        }
        String oldName = file.getOriginalFilename();
        String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
        try {
            file.transferTo(new File(folder, newName));
            String url = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/img" + format + newName;
            return url;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "failed";
    }
}

這個檔案上傳方法中,一共做了四件事:

  1. 解決檔案儲存路徑,這裡是儲存在專案執行目錄下的 img 目錄下,然後利用日期繼續寧分類
  2. 處理檔名問題,使用 UUID 做新的檔名,用來代替舊的檔名,可以有效防止檔名衝突
  3. 儲存檔案
  4. 生成檔案訪問路徑

這裡還有一個小問題,在 SpringMVC 中,靜態資源預設都是被自動攔截的,無法訪問,意味著上傳成功的圖片無法訪問,因此,還需要我們在 SpringMVC 的配置檔案中,再新增如下配置:

<mvc:resources mapping="/**" location="/"/>

完成之後,就可以訪問 jsp 頁面,做檔案上傳了。

當然,預設的配置不一定滿足我們的需求,我們還可以自己手動配置檔案上傳大小等:

<bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver" id="multipartResolver">
    <!--預設的編碼-->
    <property name="defaultEncoding" value="UTF-8"/>
    <!--上傳的總檔案大小-->
    <property name="maxUploadSize" value="1048576"/>
    <!--上傳的單個檔案大小-->
    <property name="maxUploadSizePerFile" value="1048576"/>
    <!--記憶體中最大的資料量,超過這個資料量,資料就要開始往硬碟中寫了-->
    <property name="maxInMemorySize" value="4096"/>
    <!--臨時目錄,超過 maxInMemorySize 配置的大小後,資料開始往臨時目錄寫,等全部上傳完成後,再將資料合併到正式的檔案上傳目錄-->
    <property name="uploadTempDir" value="file:///E:\\tmp"/>
</bean>

8.2 StandardServletMultipartResolver

這種檔案上傳方式,不需要依賴第三方 jar(主要是不需要新增 commons-fileupload 這個依賴),但是也不支援 Servlet3.0 之前的版本。

使用 StandardServletMultipartResolver ,那我們首先在 SpringMVC 的配置檔案中,配置這個 Bean:

<bean class="org.springframework.web.multipart.support.StandardServletMultipartResolver" id="multipartResolver">
</bean>

注意,這裡 Bean 的名字依然叫 multipartResolver

配置完成後,注意,這個 Bean 無法直接配置上傳檔案大小等限制。需要在 web.xml 中進行配置(這裡,即使不需要限制檔案上傳大小,也需要在 web.xml 中配置 multipart-config):

<servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-servlet.xml</param-value>
    </init-param>
    <multipart-config>
        <!--檔案儲存的臨時目錄,這個目錄系統不會主動建立-->
        <location>E:\\temp</location>
        <!--上傳的單個檔案大小-->
        <max-file-size>1048576</max-file-size>
        <!--上傳的總檔案大小-->
        <max-request-size>1048576</max-request-size>
        <!--這個就是記憶體中儲存的檔案最大大小-->
        <file-size-threshold>4096</file-size-threshold>
    </multipart-config>
</servlet>
<servlet-mapping>
    <servlet-name>springmvc</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

配置完成後,就可以測試檔案上傳了,測試方式和上面一樣。

8.3 多檔案上傳

多檔案上傳分為兩種,一種是 key 相同的檔案,另一種是 key 不同的檔案。

8.3.1 key 相同的檔案

這種上傳,前端頁面一般如下:

<form action="/upload2" method="post" enctype="multipart/form-data">
    <input type="file" name="files" multiple>
    <input type="submit" value="上傳">
</form>

主要是 input 節點中多了 multiple 屬性。後端用一個數組來接收檔案即可:

@RequestMapping("/upload2")
@ResponseBody
public void upload2(MultipartFile[] files, HttpServletRequest req) {
    String format = sdf.format(new Date());
    String realPath = req.getServletContext().getRealPath("/img") + format;
    File folder = new File(realPath);
    if (!folder.exists()) {
        folder.mkdirs();
    }
    try {
        for (MultipartFile file : files) {
            String oldName = file.getOriginalFilename();
            String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
            file.transferTo(new File(folder, newName));
            String url = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/img" + format + newName;
            System.out.println(url);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

8.3.2 key 不同的檔案

key 不同的,一般前端定義如下:

<form action="/upload3" method="post" enctype="multipart/form-data">
    <input type="file" name="file1">
    <input type="file" name="file2">
    <input type="submit" value="上傳">
</form>

這種