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

摘自:https://www.cnblogs.com/lenve/p/12100698.html

手碼兩萬餘字,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>

這種,在後端用不同的變量來接收就行了:

@RequestMapping("/upload3")
@ResponseBody
public void upload3(MultipartFile file1, MultipartFile file2, 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 {
        String oldName = file1.getOriginalFilename();
        String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
        file1.transferTo(new File(folder, newName));
        String url1 = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/img" + format + newName;
        System.out.println(url1);
        String oldName2 = file2.getOriginalFilename();
        String newName2 = UUID.randomUUID().toString() + oldName2.substring(oldName2.lastIndexOf("."));
        file2.transferTo(new File(folder, newName2));
        String url2 = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/img" + format + newName2;
        System.out.println(url2);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

9. 全局異常處理

項目中,可能會拋出多個異常,我們不可以直接將異常的堆棧信息展示給用戶,有兩個原因:

  1. 用戶體驗不好
  2. 非常不安全

所以,針對異常,我們可以自定義異常處理,SpringMVC 中,針對全局異常也提供了相應的解決方案,主要是通過 @ControllerAdvice 和 @ExceptionHandler 兩個註解來處理的。

以第八節的文件上傳大小超出限制爲例,自定義異常,只需要提供一個異常處理類即可:

@ControllerAdvice//表示這是一個增強版的 Controller,主要用來做全局數據處理
public class MyException {
    @ExceptionHandler(Exception.class)
    public ModelAndView fileuploadException(Exception e) {
        ModelAndView error = new ModelAndView("error");
        error.addObject("error", e.getMessage());
        return error;
    }
}

在這裏:

  • @ControllerAdvice 表示這是一個增強版的 Controller,主要用來做全局數據處理
  • @ExceptionHandler 表示這是一個異常處理方法,這個註解的參數,表示需要攔截的異常,參數爲 Exception 表示攔截所有異常,這裏也可以具體到某一個異常,如果具體到某一個異常,那麼發生了其他異常則不會被攔截到。
  • 異常方法的定義,和 Controller 中方法的定義一樣,可以返回 ModelAndview,也可以返回 String 或者 void

例如如下代碼,指揮攔截文件上傳異常,其他異常和它沒關係,不會進入到自定義異常處理的方法中來。

@ControllerAdvice//表示這是一個增強版的 Controller,主要用來做全局數據處理
public class MyException {
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ModelAndView fileuploadException(MaxUploadSizeExceededException e) {
        ModelAndView error = new ModelAndView("error");
        error.addObject("error", e.getMessage());
        return error;
    }
}

10. 服務端數據校驗

B/S 系統中對 http 請求數據的校驗多數在客戶端進行,這也是出於簡單及用戶體驗性上考慮,但是在一些安全性要求高的系統中服務端校驗是不可缺少的,實際上,幾乎所有的系統,凡是涉及到數據校驗,都需要在服務端進行二次校驗。爲什麼要在服務端進行二次校驗呢?這需要理解客戶端校驗和服務端校驗各自的目的。

  1. 客戶端校驗,我們主要是爲了提高用戶體驗,例如用戶輸入一個郵箱地址,要校驗這個郵箱地址是否合法,沒有必要發送到服務端進行校驗,直接在前端用 js 進行校驗即可。但是大家需要明白的是,前端校驗無法代替後端校驗,前端校驗可以有效的提高用戶體驗,但是無法確保數據完整性,因爲在 B/S 架構中,用戶可以方便的拿到請求地址,然後直接發送請求,傳遞非法參數。
  2. 服務端校驗,雖然用戶體驗不好,但是可以有效的保證數據安全與完整性。
  3. 綜上,實際項目中,兩個一起用。

Spring 支持 JSR-303 驗證框架,JSR-303 是 JAVA EE 6 中的一項子規範,叫做 Bean Validation,官方參考實現是 Hibernate Validator(與Hibernate ORM 沒有關係),JSR-303 用於對 Java Bean 中的字段的值進行驗證。

10.1 普通校驗

普通校驗,是這裏最基本的用法。

首先,我們需要加入校驗需要的依賴:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.1.0.Final</version>
</dependency>

接下來,在 SpringMVC 的配置文件中配置校驗的 Bean:

<bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" id="validatorFactoryBean">
    <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
</bean>
<mvc:annotation-driven validator="validatorFactoryBean"/>

配置時,提供一個 LocalValidatorFactoryBean 的實例,然後 Bean 的校驗使用 HibernateValidator。

這樣,配置就算完成了。

接下來,我們提供一個添加學生的頁面:

<form action="/addstudent" method="post">
    <table>
        <tr>
            <td>學生編號:</td>
            <td><input type="text" name="id"></td>
        </tr>
        <tr>
            <td>學生姓名:</td>
            <td><input type="text" name="name"></td>
        </tr>
        <tr>
            <td>學生郵箱:</td>
            <td><input type="text" name="email"></td>
        </tr>
        <tr>
            <td>學生年齡:</td>
            <td><input type="text" name="age"></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="提交">
            </td>
        </tr>
    </table>
</form>

在這裏需要提交的數據中,假設學生編號不能爲空,學生姓名長度不能超過 10 且不能爲空,郵箱地址要合法,年齡不能超過 150。那麼在定義實體類的時候,就可以加入這個判斷條件了。

public class Student {
    @NotNull
    private Integer id;
    @NotNull
    @Size(min = 2,max = 10)
    private String name;
    @Email
    private String email;
    @Max(150)
    private Integer age;

    public String getEmail() {
        return email;
    }

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

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getAge() {
        return age;
    }

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

    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;
    }
}

在這裏:

  • @NotNull 表示這個字段不能爲空
  • @Size 中描述了這個字符串長度的限制
  • @Email 表示這個字段的值必須是一個郵箱地址
  • @Max 表示這個字段的最大值

定義完成後,接下來,在 Controller 中定義接口:

@Controller
public class StudentController {
    @RequestMapping("/addstudent")
    @ResponseBody
    public void addStudent(@Validated Student student, BindingResult result) {
        if (result != null) {
            //校驗未通過,獲取所有的異常信息並展示出來
            List<ObjectError> allErrors = result.getAllErrors();
            for (ObjectError allError : allErrors) {
                System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
            }
        }
    }
}

在這裏:

  • @Validated 表示 Student 中定義的校驗規則將會生效
  • BindingResult 表示出錯信息,如果這個變量不爲空,表示有錯誤,否則校驗通過。

接下來就可以啓動項目了。訪問 jsp 頁面,然後添加 Student,查看校驗規則是否生效。

默認情況下,打印出來的錯誤信息時系統默認的錯誤信息,這個錯誤信息,我們也可以自定義。自定義方式如下:

由於 properties 文件中的中文會亂碼,所以需要我們先修改一下 IDEA 配置,點 File-->Settings->Editor-->File Encodings,如下:

然後定義錯誤提示文本,在 resources 目錄下新建一個 MyMessage.properties 文件,內容如下:

student.id.notnull=id 不能爲空
student.name.notnull=name 不能爲空
student.name.length=name 最小長度爲 2 ,最大長度爲 10
student.email.error=email 地址非法
student.age.error=年齡不能超過 150

接下來,在 SpringMVC 配置中,加載這個配置文件:

<bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" id="validatorFactoryBean">
    <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
    <property name="validationMessageSource" ref="bundleMessageSource"/>
</bean>
<bean class="org.springframework.context.support.ReloadableResourceBundleMessageSource" id="bundleMessageSource">
    <property name="basenames">
        <list>
            <value>classpath:MyMessage</value>
        </list>
    </property>
    <property name="defaultEncoding" value="UTF-8"/>
    <property name="cacheSeconds" value="300"/>
</bean>
<mvc:annotation-driven validator="validatorFactoryBean"/>

最後,在實體類上的註解中,加上校驗出錯時的信息:

public class Student {
    @NotNull(message = "{student.id.notnull}")
    private Integer id;
    @NotNull(message = "{student.name.notnull}")
    @Size(min = 2,max = 10,message = "{student.name.length}")
    private String name;
    @Email(message = "{student.email.error}")
    private String email;
    @Max(value = 150,message = "{student.age.error}")
    private Integer age;

    public String getEmail() {
        return email;
    }

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

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getAge() {
        return age;
    }

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

    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;
    }
}

配置完成後,如果校驗再出錯,就會展示我們自己的出錯信息了。

10.2 分組校驗

由於校驗規則都是定義在實體類上面的,但是,在不同的數據提交環境下,校驗規則可能不一樣。例如,用戶的 id 是自增長的,添加的時候,可以不用傳遞用戶 id,但是修改的時候則必須傳遞用戶 id,這種情況下,就需要使用分組校驗。

分組校驗,首先需要定義校驗組,所謂的校驗組,其實就是空接口:

public interface ValidationGroup1 {
}
public interface ValidationGroup2 {
}

然後,在實體類中,指定每一個校驗規則所屬的組:

public class Student {
    @NotNull(message = "{student.id.notnull}",groups = ValidationGroup1.class)
    private Integer id;
    @NotNull(message = "{student.name.notnull}",groups = {ValidationGroup1.class, ValidationGroup2.class})
    @Size(min = 2,max = 10,message = "{student.name.length}",groups = {ValidationGroup1.class, ValidationGroup2.class})
    private String name;
    @Email(message = "{student.email.error}",groups = {ValidationGroup1.class, ValidationGroup2.class})
    private String email;
    @Max(value = 150,message = "{student.age.error}",groups = {ValidationGroup2.class})
    private Integer age;

    public String getEmail() {
        return email;
    }

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

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getAge() {
        return age;
    }

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

    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;
    }
}

在 group 中指定每一個校驗規則所屬的組,一個規則可以屬於一個組,也可以屬於多個組。

最後,在接收參數的地方,指定校驗組:

@Controller
public class StudentController {
    @RequestMapping("/addstudent")
    @ResponseBody
    public void addStudent(@Validated(ValidationGroup2.class) Student student, BindingResult result) {
        if (result != null) {
            //校驗未通過,獲取所有的異常信息並展示出來
            List<ObjectError> allErrors = result.getAllErrors();
            for (ObjectError allError : allErrors) {
                System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
            }
        }
    }
}

配置完成後,屬於 ValidationGroup2 這個組的校驗規則,纔會生效。

10.3 校驗註解

校驗註解,主要有如下幾種:

  • @Null 被註解的元素必須爲 null
  • @NotNull 被註解的元素必須不爲 null
  • @AssertTrue 被註解的元素必須爲 true
  • @AssertFalse 被註解的元素必須爲 false
  • @Min(value) 被註解的元素必須是一個數字,其值必須大於等於指定的最小值
  • @Max(value) 被註解的元素必須是一個數字,其值必須小於等於指定的最大值
  • @DecimalMin(value) 被註解的元素必須是一個數字,其值必須大於等於指定的最小值
  • @DecimalMax(value) 被註解的元素必須是一個數字,其值必須小於等於指定的最大值
  • @Size(max=, min=) 被註解的元素的大小必須在指定的範圍內
  • @Digits (integer, fraction) 被註解的元素必須是一個數字,其值必須在可接受的範圍內
  • @Past 被註解的元素必須是一個過去的日期
  • @Future 被註解的元素必須是一個將來的日期
  • @Pattern(regex=,flag=) 被註解的元素必須符合指定的正則表達式
  • @NotBlank(message =) 驗證字符串非 null,且長度必須大於0
  • @Email 被註解的元素必須是電子郵箱地址
  • @Length(min=,max=) 被註解的字符串的大小必須在指定的範圍內
  • @NotEmpty 被註解的字符串的必須非空
  • @Range(min=,max=,message=) 被註解的元素必須在合適的範圍內

11.1 數據回顯基本用法

數據回顯就是當用戶數據提交失敗時,自動填充好已經輸入的數據。一般來說,如果使用 Ajax 來做數據提交,基本上是沒有數據回顯這個需求的,但是如果是通過表單做數據提交,那麼數據回顯就非常有必要了。

11.1.1 簡單數據類型

簡單數據類型,實際上框架在這裏沒有提供任何形式的支持,就是我們自己手動配置。我們繼續在第 10 小節的例子上演示 Demo。加入提交的 Student 數據不符合要求,那麼重新回到添加 Student 頁面,並且預設之前已經填好的數據。

首先我們先來改造一下 student.jsp 頁面:

<form action="/addstudent" method="post">
    <table>
        <tr>
            <td>學生編號:</td>
            <td><input type="text" name="id" value="${id}"></td>
        </tr>
        <tr>
            <td>學生姓名:</td>
            <td><input type="text" name="name" value="${name}"></td>
        </tr>
        <tr>
            <td>學生郵箱:</td>
            <td><input type="text" name="email" value="${email}"></td>
        </tr>
        <tr>
            <td>學生年齡:</td>
            <td><input type="text" name="age" value="${age}"></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="提交">
            </td>
        </tr>
    </table>
</form>

在接收數據時,使用簡單數據類型去接收:

@RequestMapping("/addstudent")
public String addStudent2(Integer id, String name, String email, Integer age, Model model) {
    model.addAttribute("id", id);
    model.addAttribute("name", name);
    model.addAttribute("email", email);
    model.addAttribute("age", age);
    return "student";
}

這種方式,相當於框架沒有做任何工作,就是我們手動做數據回顯的。此時訪問頁面,服務端會再次定位到該頁面,而且數據已經預填好。

11.1.2 實體類

上面這種簡單數據類型的回顯,實際上非常麻煩,因爲需要開發者在服務端一個一個手動設置。如果使用對象的話,就沒有這麼麻煩了,因爲 SpringMVC 在頁面跳轉時,會自動將對象填充進返回的數據中。

此時,首先修改一下 student.jsp 頁面:

<form action="/addstudent" method="post">
    <table>
        <tr>
            <td>學生編號:</td>
            <td><input type="text" name="id" value="${student.id}"></td>
        </tr>
        <tr>
            <td>學生姓名:</td>
            <td><input type="text" name="name" value="${student.name}"></td>
        </tr>
        <tr>
            <td>學生郵箱:</td>
            <td><input type="text" name="email" value="${student.email}"></td>
        </tr>
        <tr>
            <td>學生年齡:</td>
            <td><input type="text" name="age" value="${student.age}"></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="提交">
            </td>
        </tr>
    </table>
</form>

注意,在預填數據中,多了一個 student. 前綴。這 student 就是服務端接收數據的變量名,服務端的變量名和這裏的 student 要保持一直。服務端定義如下:

@RequestMapping("/addstudent")
public String addStudent(@Validated(ValidationGroup2.class) Student student, BindingResult result) {
    if (result != null) {
        //校驗未通過,獲取所有的異常信息並展示出來
        List<ObjectError> allErrors = result.getAllErrors();
        for (ObjectError allError : allErrors) {
            System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
        }
        return "student";
    }
    return "hello";
}

注意,服務端什麼都不用做,就說要返回的頁面就行了,student 這個變量會被自動填充到返回的 Model 中。變量名就是填充時候的 key。如果想自定義這個 key,可以在參數中寫出來 Model,然後手動加入 Student 對象,就像簡單數據類型回顯那樣。

另一種定義回顯變量別名的方式,就是使用 @ModelAttribute 註解。

11.2 @ModelAttribute

@ModelAttribute 這個註解,主要有兩方面的功能:

  1. 在數據回顯時,給變量定義別名
  2. 定義全局數據

11.2.1 定義別名

在數據回顯時,給變量定義別名,非常容易,直接加這個註解即可:

@RequestMapping("/addstudent")
public String addStudent(@ModelAttribute("s") @Validated(ValidationGroup2.class) Student student, BindingResult result) {
    if (result != null) {
        //校驗未通過,獲取所有的異常信息並展示出來
        List<ObjectError> allErrors = result.getAllErrors();
        for (ObjectError allError : allErrors) {
            System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
        }
        return "student";
    }
    return "hello";
}

這樣定義完成後,在前端再次訪問回顯的變量時,變量名稱就不是 student 了,而是 s:

<form action="/addstudent" method="post">
    <table>
        <tr>
            <td>學生編號:</td>
            <td><input type="text" name="id" value="${s.id}"></td>
        </tr>
        <tr>
            <td>學生姓名:</td>
            <td><input type="text" name="name" value="${s.name}"></td>
        </tr>
        <tr>
            <td>學生郵箱:</td>
            <td><input type="text" name="email" value="${s.email}"></td>
        </tr>
        <tr>
            <td>學生年齡:</td>
            <td><input type="text" name="age" value="${s.age}"></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="提交">
            </td>
        </tr>
    </table>
</form>

11.2.2 定義全局數據

假設有一個 Controller 中有很多方法,每個方法都會返回數據給前端,但是每個方法返回給前端的數據又不太一樣,雖然不太一樣,但是沒有方法的返回值又有一些公共的部分。可以將這些公共的部分提取出來單獨封裝成一個方法,用 @ModelAttribute 註解來標記。

例如在一個 Controller 中 ,添加如下代碼:

@ModelAttribute("info")
public Map<String,Object> info() {
    Map<String, Object> map = new HashMap<>();
    map.put("username", "javaboy");
    map.put("address", "www.javaboy.org");
    return map;
}

當用戶訪問當前 Controller 中的任意一個方法,在返回數據時,都會將添加了 @ModelAttribute 註解的方法的返回值,一起返回給前端。@ModelAttribute 註解中的 info 表示返回數據的 key。

12.1 返回 JSON

目前主流的 JSON 處理工具主要有三種:

  • jackson
  • gson
  • fastjson

在 SpringMVC 中,對 jackson 和 gson 都提供了相應的支持,就是如果使用這兩個作爲 JSON 轉換器,只需要添加對應的依賴就可以了,返回的對象和返回的集合、Map 等都會自動轉爲 JSON,但是,如果使用 fastjson,除了添加相應的依賴之外,還需要自己手動配置 HttpMessageConverter 轉換器。其實前兩個也是使用 HttpMessageConverter 轉換器,但是是 SpringMVC 自動提供的,SpringMVC 沒有給 fastjson 提供相應的轉換器。

12.1.1 jackson

jackson 是一個使用比較多,時間也比較長的 JSON 處理工具,在 SpringMVC 中使用 jackson ,只需要添加 jackson 的依賴即可:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.10.1</version>
</dependency>

依賴添加成功後,凡是在接口中直接返回的對象,集合等等,都會自動轉爲 JSON。如下:

public class Book {
    private Integer id;
    private String name;
    private String author;

    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 Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}
@RequestMapping("/book")
@ResponseBody
public Book getBookById() {
    Book book = new Book();
    book.setId(1);
    book.setName("三國演義");
    book.setAuthor("羅貫中");
    return book;
}

這裏返回一個對象,但是在前端接收到的則是一個 JSON 字符串,這個對象會通過 HttpMessageConverter 自動轉爲 JSON 字符串。

如果想返回一個 JSON 數組,寫法如下:

@RequestMapping("/books")
@ResponseBody
public List<Book> getAllBooks() {
    List<Book> list = new ArrayList<Book>();
    for (int i = 0; i < 10; i++) {
        Book book = new Book();
        book.setId(i);
        book.setName("三國演義:" + i);
        book.setAuthor("羅貫中:" + i);
        list.add(book);
    }
    return list;
}

添加了 jackson ,就能夠自動返回 JSON,這個依賴於一個名爲 HttpMessageConverter 的類,這本身是一個接口,從名字上就可以看出,它的作用是 Http 消息轉換器,既然是消息轉換器,它提供了兩方面的功能:

  1. 將返回的對象轉爲 JSON
  2. 將前端提交上來的 JSON 轉爲對象

但是,HttpMessageConverter 只是一個接口,由各個 JSON 工具提供相應的實現,在 jackson 中,實現的名字叫做 MappingJackson2HttpMessageConverter,而這個東西的初始化,則由 SpringMVC 來完成。除非自己有一些自定義配置的需求,否則一般來說不需要自己提供 MappingJackson2HttpMessageConverter。

舉一個簡單的應用場景,例如每一本書,都有一個出版日期,修改 Book 類如下:

public class Book {
    private Integer id;
    private String name;
    private String author;
    private Date publish;


    public Date getPublish() {
        return publish;
    }

    public void setPublish(Date publish) {
        this.publish = publish;
    }

    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 Integer getId() {
        return id;
    }

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

然後在構造 Book 時添加日期屬性:

@RequestMapping("/book")
@ResponseBody
public Book getBookById() {
    Book book = new Book();
    book.setId(1);
    book.setName("三國演義");
    book.setAuthor("羅貫中");
    book.setPublish(new Date());
    return book;
}

訪問 /book 接口,返回的 json 格式如下:

如果我們想自己定製返回日期的格式,簡單的辦法,可以通過添加註解來實現:

public class Book {
    private Integer id;
    private String name;
    private String author;
    @JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai")
    private Date publish;

注意這裏一定要設置時區。

這樣,就可以定製返回的日期格式了。

但是,這種方式有一個弊端,這個註解可以加在屬性上,也可以加在類上,也就說,最大可以作用到一個類中的所有日期屬性上。如果項目中有很多實體類都需要做日期格式化,使用這種方式就比較麻煩了,這個時候,我們可以自己提供一個 jackson 的 HttpMesageConverter 實例,在這個實例中,自己去配置相關屬性,這裏的配置將是一個全局配置。

在 SpringMVC 配置文件中,添加如下配置:

<mvc:annotation-driven>
    <mvc:message-converters>
        <ref bean="httpMessageConverter"/>
    </mvc:message-converters>
</mvc:annotation-driven>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" id="httpMessageConverter">
    <property name="objectMapper">
        <bean class="com.fasterxml.jackson.databind.ObjectMapper">
            <property name="dateFormat">
                <bean class="java.text.SimpleDateFormat">
                    <constructor-arg name="pattern" value="yyyy-MM-dd HH:mm:ss"/>
                </bean>
            </property>
            <property name="timeZone" value="Asia/Shanghai"/>
        </bean>
    </property>
</bean>

添加完成後,去掉 Book 實體類中日期格式化的註解,再進行測試,結果如下:

12.1.2 gson

gson 是 Google 推出的一個 JSON 解析器,主要在 Android 開發中使用較多,不過,Web 開發中也是支持這個的,而且 SpringMVC 還針對 Gson 提供了相關的自動化配置,以致我們在項目中只要添加 gson 依賴,就可以直接使用 gson 來做 JSON 解析了。

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.6</version>
</dependency>

如果項目中,同時存在 jackson 和 gson 的話,那麼默認使用的是 jackson,爲社麼呢?在 org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter 類的構造方法中,加載順序就是先加載 jackson 的 HttpMessageConverter,後加載 gson 的 HttpMessageConverter。

加完依賴之後,就可以直接返回 JSON 字符串了。使用 Gson 時,如果想做自定義配置,則需要自定義 HttpMessageConverter。

<mvc:annotation-driven>
    <mvc:message-converters>
        <ref bean="httpMessageConverter"/>
    </mvc:message-converters>
</mvc:annotation-driven>
<bean class="org.springframework.http.converter.json.GsonHttpMessageConverter" id="httpMessageConverter">
    <property name="gson">
        <bean class="com.google.gson.Gson" factory-bean="gsonBuilder" factory-method="create"/>
    </property>
</bean>
<bean class="com.google.gson.GsonBuilder" id="gsonBuilder">
    <property name="dateFormat" value="yyyy-MM-dd"/>
</bean>

12.1.3 fastjson

fastjson 號稱最快的 JSON 解析器,但是也是這三個中 BUG 最多的一個。在 SpringMVC 並沒針對 fastjson 提供相應的 HttpMessageConverter,所以,fastjson 在使用時,一定要自己手動配置 HttpMessageConverter(前面兩個如果沒有特殊需要,直接添加依賴就可以了)。

使用 fastjson,我們首先添加 fastjson 依賴:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.60</version>
</dependency>

然後在 SpringMVC 的配置文件中配置 HttpMessageConverter:

<mvc:annotation-driven>
    <mvc:message-converters>
        <ref bean="httpMessageConverter"/>
    </mvc:message-converters>
</mvc:annotation-driven>
<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" id="httpMessageConverter">
    <property name="fastJsonConfig">
        <bean class="com.alibaba.fastjson.support.config.FastJsonConfig">
            <property name="dateFormat" value="yyyy-MM-dd"/>
        </bean>
    </property>
</bean>

fastjson 默認中文亂碼,添加如下配置解決:

<mvc:annotation-driven>
    <mvc:message-converters>
        <ref bean="httpMessageConverter"/>
    </mvc:message-converters>
</mvc:annotation-driven>
<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" id="httpMessageConverter">
    <property name="fastJsonConfig">
        <bean class="com.alibaba.fastjson.support.config.FastJsonConfig">
            <property name="dateFormat" value="yyyy-MM-dd"/>
        </bean>
    </property>
    <property name="supportedMediaTypes">
        <list>
            <value>application/json;charset=utf-8</value>
        </list>
    </property>
</bean>

12.2 接收 JSON

瀏覽器傳來的參數,可以是 key/value 形式的,也可以是一個 JSON 字符串。在 Jsp/Servlet 中,我們接收 key/value 形式的參數,一般是通過 getParameter 方法。如果客戶端商戶慘的是 JSON 數據,我們可以通過如下格式進行解析:

@RequestMapping("/addbook2")
@ResponseBody
public void addBook2(HttpServletRequest req) throws IOException {
    ObjectMapper om = new ObjectMapper();
    Book book = om.readValue(req.getInputStream(), Book.class);
    System.out.println(book);
}

但是這種解析方式有點麻煩,在 SpringMVC 中,我們可以通過一個註解來快速的將一個 JSON 字符串轉爲一個對象:

@RequestMapping("/addbook3")
@ResponseBody
public void addBook3(@RequestBody Book book) {
    System.out.println(book);
}

這樣就可以直接收到前端傳來的 JSON 字符串了。這也是 HttpMessageConverter 提供的第二個功能。

13. RESTful

本小節選自外部博客,原文鏈接:https://www.ruanyifeng.com/blog/2011/09/restful.html

越來越多的人開始意識到,網站即軟件,而且是一種新型的軟件。這種"互聯網軟件"採用客戶端/服務器模式,建立在分佈式體系上,通過互聯網通信,具有高延時(high latency)、高併發等特點。網站開發,完全可以採用軟件開發的模式。但是傳統上,軟件和網絡是兩個不同的領域,很少有交集;軟件開發主要針對單機環境,網絡則主要研究系統之間的通信。互聯網的興起,使得這兩個領域開始融合,現在我們必須考慮,如何開發在互聯網環境中使用的軟件。

RESTful 架構,就是目前最流行的一種互聯網軟件架構。它結構清晰、符合標準、易於理解、擴展方便,所以正得到越來越多網站的採用。

但是,到底什麼是 RESTful 架構,並不是一個容易說清楚的問題。下面,我就談談我理解的 RESTful 架構。、

RESTful 它不是一個具體的架構,不是一個軟件,不是一個框架,而是一種規範。在移動互聯網興起之前,我們都很少提及 RESTful,主要是因爲用的少,移動互聯網興起後,RESTful 得到了非常廣泛的應用,因爲在移動互聯網興起之後,我們再開發後端應用,就不僅僅只是開發一個網站了,還對應了多個前端(Android、iOS、HTML5 等等),這個時候,我們在設計後端接口是,就需要考慮接口的形式,格式,參數的傳遞等等諸多問題了。

13.1 起源

REST 這個詞,是 Roy Thomas Fielding 在他 2000 年的博士論文中提出的。

Fielding 是一個非常重要的人,他是 HTTP 協議(1.0版和1.1版)的主要設計者、Apache 服務器軟件的作者之一、Apache 基金會的第一任主席。所以,他的這篇論文一經發表,就引起了關注,並且立即對互聯網開發產生了深遠的影響。

他這樣介紹論文的寫作目的:

"本文研究計算機科學兩大前沿----軟件和網絡----的交叉點。長期以來,軟件研究主要關注軟件設計的分類、設計方法的演化,很少客觀地評估不同的設計選擇對系統行爲的影響。而相反地,網絡研究主要關注系統之間通信行爲的細節、如何改進特定通信機制的表現,常常忽視了一個事實,那就是改變應用程序的互動風格比改變互動協議,對整體表現有更大的影響。我這篇文章的寫作目的,就是想在符合架構原理的前提下,理解和評估以網絡爲基礎的應用軟件的架構設計,得到一個功能強、性能好、適宜通信的架構。"

13.2 名稱

Fielding 將他對互聯網軟件的架構原則,定名爲REST,即 Representational State Transfer 的縮寫。我對這個詞組的翻譯是"表現層狀態轉化"。

如果一個架構符合 REST 原則,就稱它爲 RESTful 架構。

要理解 RESTful 架構,最好的方法就是去理解 Representational State Transfer 這個詞組到底是什麼意思,它的每一個詞代表了什麼涵義。如果你把這個名稱搞懂了,也就不難體會 REST 是一種什麼樣的設計。

13.3 資源(Resources)

REST 的名稱"表現層狀態轉化"中,省略了主語。"表現層"其實指的是"資源"(Resources)的"表現層"。

所謂"資源",就是網絡上的一個實體,或者說是網絡上的一個具體信息。它可以是一段文本、一張圖片、一首歌曲、一種服務,總之就是一個具體的實在。你可以用一個 URI (統一資源定位符)指向它,每種資源對應一個特定的 URI。要獲取這個資源,訪問它的 URI 就可以,因此 URI 就成了每一個資源的地址或獨一無二的識別符。

所謂"上網",就是與互聯網上一系列的"資源"互動,調用它的 URI。

在 RESTful 風格的應用中,每一個 URI 都代表了一個資源。

13.4 表現層(Representation)

"資源"是一種信息實體,它可以有多種外在表現形式。我們把"資源"具體呈現出來的形式,叫做它的"表現層"(Representation)。

比如,文本可以用 txt 格式表現,也可以用 HTML 格式、XML 格式、JSON 格式表現,甚至可以採用二進制格式;圖片可以用 JPG 格式表現,也可以用 PNG 格式表現。

URI 只代表資源的實體,不代表它的形式。嚴格地說,有些網址最後的 ".html" 後綴名是不必要的,因爲這個後綴名錶示格式,屬於 "表現層" 範疇,而 URI 應該只代表"資源"的位置。它的具體表現形式,應該在 HTTP 請求的頭信息中用 Accept 和 Content-Type 字段指定,這兩個字段纔是對"表現層"的描述。

13.5 狀態轉化(State Transfer)

訪問一個網站,就代表了客戶端和服務器的一個互動過程。在這個過程中,勢必涉及到數據和狀態的變化。

互聯網通信協議 HTTP 協議,是一個無狀態協議。這意味着,所有的狀態都保存在服務器端。因此,如果客戶端想要操作服務器,必須通過某種手段,讓服務器端發生"狀態轉化"(State Transfer)。而這種轉化是建立在表現層之上的,所以就是"表現層狀態轉化"。

客戶端用到的手段,只能是 HTTP 協議。具體來說,就是 HTTP 協議裏面,四個表示操作方式的動詞:GET、POST、PUT、DELETE。它們分別對應四種基本操作:

  • GET 用來獲取資源
  • POST 用來新建資源(也可以用於更新資源)
  • PUT 用來更新資源
  • DELETE 用來刪除資源

13.6 綜述

綜合上面的解釋,我們總結一下什麼是 RESTful 架構:

  • 每一個 URI 代表一種資源;
  • 客戶端和服務器之間,傳遞這種資源的某種表現層;
  • 客戶端通過四個 HTTP 動詞,對服務器端資源進行操作,實現"表現層狀態轉化"。

13.7 誤區

RESTful 架構有一些典型的設計誤區。

最常見的一種設計錯誤,就是 URI 包含動詞。因爲"資源"表示一種實體,所以應該是名詞,URI 不應該有動詞,動詞應該放在 HTTP 協議中。

舉例來說,某個 URI 是 /posts/show/1,其中 show 是動詞,這個 URI 就設計錯了,正確的寫法應該是 /posts/1,然後用 GET 方法表示 show。

如果某些動作是HTTP動詞表示不了的,你就應該把動作做成一種資源。比如網上匯款,從賬戶 1 向賬戶 2 匯款 500 元,錯誤的 URI 是:

  • POST /accounts/1/transfer/500/to/2

正確的寫法是把動詞 transfer 改成名詞 transaction,資源不能是動詞,但是可以是一種服務:

POST /transaction HTTP/1.1
Host: 127.0.0.1
from=1&to=2&amount=500.00

另一個設計誤區,就是在URI中加入版本號:

  • http://www.example.com/app/1.0/foo
  • http://www.example.com/app/1.1/foo
  • http://www.example.com/app/2.0/foo

因爲不同的版本,可以理解成同一種資源的不同表現形式,所以應該採用同一個 URI。版本號可以在 HTTP 請求頭信息的 Accept 字段中進行區分(參見 Versioning REST Services):

Accept: vnd.example-com.foo+json; version=1.0
Accept: vnd.example-com.foo+json; version=1.1
Accept: vnd.example-com.foo+json; version=2.0

13.8 SpringMVC 的支持

SpringMVC 對 RESTful 提供了非常全面的支持,主要有如下幾個註解:

  • @RestController

這個註解是一個組合註解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {

    /**
     * The value may indicate a suggestion for a logical component name,
     * to be turned into a Spring bean in case of an autodetected component.
     * @return the suggested component name, if any (or empty String otherwise)
     * @since 4.0.1
     */
    @AliasFor(annotation = Controller.class)
    String value() default "";

}

一般,直接用 @RestController 來標記 Controller,可以不使用 @Controller。

請求方法中,提供了常見的請求方法:

  • @PostMapping
  • @GetMapping
  • @PutMapping
  • @DeleteMapping

另外還有一個提取請求地址中的參數的註解 @PathVariable:

@GetMapping("/book/{id}")//http://localhost:8080/book/2
public Book getBookById(@PathVariable Integer id) {
    Book book = new Book();
    book.setId(id);
    return book;
}

參數 2 將被傳遞到 id 這個變量上。

14. 靜態資源訪問

在 SpringMVC 中,靜態資源,默認都是被攔截的,例如 html、js、css、jpg、png、txt、pdf 等等,都是無法直接訪問的。因爲所有請求都被攔截了,所以,針對靜態資源,我們要做額外處理,處理方式很簡單,直接在 SpringMVC 的配置文件中,添加如下內容:

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

mapping 表示映射規則,也是攔截規則,就是說,如果請求地址是 /static/html 這樣的格式的話,那麼對應的資源就去 /static/html/ 這個目錄下查找。

在映射路徑的定義中,最後是兩個 *,這是一種 Ant 風格的路徑匹配符號,一共有三個通配符:

通配符含義
** 匹配多層路徑
* 匹配一層路徑
? 匹配任意單個字符

一個比較原始的配置方式可能如下:

<mvc:resources mapping="/static/html/**" location="/static/html/"/>
<mvc:resources mapping="/static/js/**" location="/static/js/"/>
<mvc:resources mapping="/static/css/**" location="/static/css/"/>

但是,由於 ** 可以表示多級路徑,所以,以上配置,我們可以進行簡化:

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

15. 攔截器

SpringMVC 中的攔截器,相當於 Jsp/Servlet 中的過濾器,只不過攔截器的功能更爲強大。

攔截器的定義非常容易:

@Component
public class MyInterceptor1 implements HandlerInterceptor {
    /**
     * 這個是請求預處理的方法,只有當這個方法返回值爲 true 的時候,後面的方法纔會執行
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("MyInterceptor1:preHandle");
        return true;
    }

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("MyInterceptor1:postHandle");

    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("MyInterceptor1:afterCompletion");

    }
}
@Component
public class MyInterceptor2 implements HandlerInterceptor {
    /**
     * 這個是請求預處理的方法,只有當這個方法返回值爲 true 的時候,後面的方法纔會執行
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("MyInterceptor2:preHandle");
        return true;
    }

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("MyInterceptor2:postHandle");

    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("MyInterceptor2:afterCompletion");

    }
}

攔截器定義好之後,需要在 SpringMVC 的配置文件中進行配置:

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <ref bean="myInterceptor1"/>
    </mvc:interceptor>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <ref bean="myInterceptor2"/>
    </mvc:interceptor>
</mvc:interceptors>

如果存在多個攔截器,攔截規則如下:

  • preHandle 按攔截器定義順序調用
  • postHandler 按攔截器定義逆序調用
  • afterCompletion 按攔截器定義逆序調用
  • postHandler 在攔截器鏈內所有攔截器返成功調用
  • afterCompletion 只有 preHandle 返回 true 才調用

關注微信公衆號【江南一點雨】,回覆 springmvc,獲取本文電子版,或者訪問 http://springmvc.javaboy.org 查看本文電子書。

藍天爲路,陽光滿屋。
 
分類: java
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章