手碼兩萬餘字,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 能夠有效的簡化這一步驟。css

Spring Web MVC 也是服務到工做者模式的實現,但進行可優化。前端控制器是 DispatcherServlet;應用控制器能夠拆爲處理器映射器(Handler Mapping)進行處理器管理和視圖解析器(View Resolver)進行視圖管理;頁面控制器/動做/處理器爲 Controller 接口(僅包含 ModelAndView handleRequest(request, response) 方法,也有人稱做 Handler)的實現(也能夠是任何的 POJO 類);支持本地化(Locale)解析、主題(Theme)解析及文件上傳等;提供了很是靈活的數據驗證、格式化和數據綁定機制;提供了強大的約定大於配置(慣例優先原則)的契約式編程支持。html

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 的依賴:java

<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 等等就所有都加入進來了。git

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 就是前端請求處理接口。web

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 的配置。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/bl...

愈來愈多的人開始意識到,網站即軟件,並且是一種新型的軟件。這種"互聯網軟件"採用客戶端/服務器模式,創建在分佈式體系上,經過互聯網通訊,具備高延時(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 服務器軟件的做者之1、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中加入版本號:

由於不一樣的版本,能夠理解成同一種資源的不一樣表現形式,因此應該採用同一個 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 查看本文電子書。

相關文章
相關標籤/搜索