實習第一週學習總結

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

若是一個架構符合REST原則,就稱它爲RESTful架構。html

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

資源(Resources)web

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

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

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

表現層(Representation)apache

"資源"是一種信息實體,它能夠有多種外在表現形式。咱們把"資源"具體呈現出來的形式,叫作它的"表現層"(Representation)。瀏覽器

好比,文本能夠用txt格式表現,也能夠用HTML格式、XML格式、JSON格式表現,甚至能夠採用二進制格式;圖片能夠用JPG格式表現,也能夠用PNG格式表現。spring-mvc

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

狀態轉化(State Transfer)

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

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

客戶端用到的手段,只能是HTTP協議。具體來講,就是HTTP協議裏面,四個表示操做方式的動詞:GET、POST、PUT、DELETE。它們分別對應四種基本操做:GET用來獲取資源,POST用來新建資源(也能夠用於更新資源),PUT用來更新資源,DELETE用來刪除資源。

綜述

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

  (1)每個URI表明一種資源;

  (2)客戶端和服務器之間,傳遞這種資源的某種表現層;

  (3)客戶端經過四個HTTP動詞,對服務器端資源進行操做,實現"表現層狀態轉化"。

 

 

 

Spring MVC 的快速入門

環境準備

  • 一個稱手的文本編輯器(例如Vim、Emacs、Sublime Text)或者IDE(Eclipse、Idea Intellij)
  • Java環境(JDK 1.7或以上版本)
  • Maven 3.0+(Eclipse和Idea IntelliJ內置,若是使用IDE而且不使用命令行工具能夠不安裝)

一個最簡單的Web應用

使用Spring Boot框架能夠大大加速Web應用的開發過程,首先在Maven項目依賴中引入spring-boot-starter-web

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.tianmaying</groupId>
  <artifactId>spring-web-demo</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>spring-web-demo</name>
  <description>Demo project for Spring WebMvc</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.2.5.RELEASE</version>
    <relativePath/>
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>

接下來建立src/main/java/Application.java:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class Application {

    @RequestMapping("/")
    public String greeting() {
        return "Hello World!";
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

運行應用:mvn spring-boot:run或在IDE中運行main()方法,在瀏覽器中訪問http://localhost:8080Hello World!就出如今了頁面中。只用了區區十幾行Java代碼,一個Hello World應用就能夠正確運行了,那麼這段代碼究竟作了什麼呢?咱們從程序的入口SpringApplication.run(Application.class, args);開始分析:

  1. SpringApplication是Spring Boot框架中描述Spring應用的類,它的run()方法會建立一個Spring應用上下文(Application Context)。另外一方面它會掃描當前應用類路徑上的依賴,例如本例中發現spring-webmvc(由 spring-boot-starter-web傳遞引入)在類路徑中,那麼Spring Boot會判斷這是一個Web應用,並啓動一個內嵌的Servlet容器(默認是Tomcat)用於處理HTTP請求。

  2. Spring WebMvc框架會將Servlet容器裏收到的HTTP請求根據路徑分發給對應的@Controller類進行處理,@RestController是一類特殊的@Controller,它的返回值直接做爲HTTP Response的Body部分返回給瀏覽器。

  3. @RequestMapping註解代表該方法處理那些URL對應的HTTP請求,也就是咱們常說的URL路由(routing),請求的分發工做是有Spring完成的。例如上面的代碼中http://localhost:8080/ 根路徑就被路由至greeting()方法進行處理。若是訪問http://localhost:8080/hello ,則會出現 404 Not Found錯誤,由於咱們並無編寫任何方法來處理/hello`請求。

使用@Controller實現URL路由

現代Web應用每每包括不少頁面,不一樣的頁面也對應着不一樣的URL。對於不一樣的URL,一般須要不一樣的方法進行處理並返回不一樣的內容。

匹配多個URL

@RestController
public class Application {

    @RequestMapping("/")
    public String index() {
        return "Index Page";
    }

    @RequestMapping("/hello")
    public String hello() {
        return "Hello World!";
    }
}

@RequestMapping能夠註解@Controller類:

@RestController
@RequestMapping("/classPath")
public class Application {
    @RequestMapping("/methodPath")
    public String method() {
        return "mapping url is /classPath/methodPath";
    }
}

method方法匹配的URL是/classPath/methodPath"

提示

能夠定義多個@Controller將不一樣URL的處理方法分散在不一樣的類中。

URL中的變量——PathVariable

在Web應用中URL一般不是一成不變的,例如微博兩個不一樣用戶的我的主頁對應兩個不一樣的URL: http://weibo.com/user1 和http://weibo.com/user2。 咱們不可能對於每個用戶都編寫一個被@RequestMapping註解的方法來處理其請求,Spring MVC提供了一套機制來處理這種狀況:

@RequestMapping("/users/{username}")
public String userProfile(@PathVariable("username") String username) {
    return String.format("user %s", username);
}

@RequestMapping("/posts/{id}")
public String post(@PathVariable("id") int id) {
    return String.format("post %d", id);
}

在上述例子中,URL中的變量能夠用{variableName}來表示,同時在方法的參數中加上@PathVariable("variableName"),那麼當請求被轉發給該方法處理時,對應的URL中的變量會被自動賦值給被@PathVariable註解的參數(可以自動根據參數類型賦值,例如上例中的int)。

支持HTTP方法

對於HTTP請求除了其URL,還須要注意它的方法(Method)。例如咱們在瀏覽器中訪問一個頁面一般是GET方法,而表單的提交通常是POST方法。@Controller中的方法一樣須要對其進行區分:

@RequestMapping(value = "/login", method = RequestMethod.GET)
public String loginGet() {
    return "Login Page";
}

@RequestMapping(value = "/login", method = RequestMethod.POST)
public String loginPost() {
    return "Login Post Request";
}

Spring MVC最新的版本中提供了一種更加簡潔的配置HTTP方法的方式,增長了四個標註:

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

在Web應用中經常使用的HTTP方法有四種:

  • PUT方法用來添加的資源
  • GET方法用來獲取已有的資源
  • POST方法用來對資源進行狀態轉換
  • DELETE方法用來刪除已有的資源

這四個方法能夠對應到CRUD操做(Create、Read、Update和Delete),好比博客的建立操做,按照REST風格設計URL就應該使用PUT方法,讀取博客使用GET方法,更新博客使用POST方法,刪除博客使用DELETE方法。

每個Web請求都是屬於其中一種,在Spring MVC中若是不特殊指定的話,默認是GET請求。

好比@RequestMapping("/")@RequestMapping("/hello")和對應的Web請求是:

  • GET /
  • GET /hello

實際上@RequestMapping("/")@RequestMapping("/", method = RequestMethod.GET)的簡寫,便可以經過method屬性,設置請求的HTTP方法。

好比PUT /hello請求,對應於@RequestMapping("/hello", method = RequestMethod.PUT)

基於新的標註@RequestMapping("/hello", method = RequestMethod.PUT)能夠簡寫爲@PutMapping("/hello")@RequestMapping("/hello")GetMapping("/hello")等價。

模板渲染

在以前全部的@RequestMapping註解的方法中,返回值字符串都被直接傳送到瀏覽器端並顯示給用戶。可是爲了可以呈現更加豐富、美觀的頁面,咱們須要將HTML代碼返回給瀏覽器,瀏覽器再進行頁面的渲染、顯示。

一種很直觀的方法是在處理請求的方法中,直接返回HTML代碼,可是這樣作的問題在於——一個複雜的頁面HTML代碼每每也很是複雜,而且嵌入在Java代碼中十分不利於維護。更好的作法是將頁面的HTML代碼寫在模板文件中,渲染後再返回給用戶。爲了可以進行模板渲染,須要將@RestController改爲@Controller

import org.springframework.ui.Model;

@Controller
public class HelloController {

    @RequestMapping("/hello/{name}")
    public String hello(@PathVariable("name") String name, Model model) {
        model.addAttribute("name", name);
        return "hello"
    }
}

在上述例子中,返回值"hello"並不是直接將字符串返回給瀏覽器,而是尋找名字爲hello的模板進行渲染,咱們使用Thymeleaf模板引擎進行模板渲染,須要引入依賴:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

接下來須要在默認的模板文件夾src/main/resources/templates/目錄下添加一個模板文件hello.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Getting Started: Serving Web Content</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <p th:text="'Hello, ' + ${name} + '!'" />
</body>
</html>

th:text="'Hello, ' + ${name} + '!'"也就是將咱們以前在@Controller方法裏添加至Model的屬性name進行渲染,並放入<p>標籤中(由於th:text<p>標籤的屬性)。模板渲染還有更多的用法,請參考Thymeleaf官方文檔

處理靜態文件

瀏覽器頁面使用HTML做爲描述語言,那麼必然也脫離不了CSS以及JavaScript。爲了可以瀏覽器可以正確加載相似/css/style.css/js/main.js等資源,默認狀況下咱們只須要在src/main/resources/static目錄下添加css/style.cssjs/main.js文件後,Spring MVC可以自動將他們發佈,經過訪問/css/style.css/js/main.js也就能夠正確加載這些資源。

文件上傳

Spring MVC還可以支持更爲複雜的HTTP請求——文件資源。咱們在網站中常常遇到上傳圖片、附件一類的需求,就是經過文件上傳技術來實現的。

處理文件的表單和普通表單的惟一區別在於設置enctype——multipart編碼方式則須要設置enctypemultipart/form-data

<form method="post" enctype="multipart/form-data">
    <input type="text" name="title" value="tianmaying">
    <input type="file" name="avatar">
    <input type="submit">
</form>

這裏咱們還設置了<input type='text'>的默認值爲tianmaying

該表單將會顯示爲一個文本框、一個文件按鈕、一個提交按鈕。而後咱們選擇一個文件:chrome.png,點擊表單提交後產生的請求多是這樣的:

請求頭:

POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA

請求體:

------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="title"

tianmaying
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="avatar"; filename="chrome.png"
Content-Type: image/png

 ... content of chrome.png ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--

這即是一個multipart編碼的表單。Content-Type中還包含了boundary的定義,它用來分隔請求體中的每一個字段。正是這一機制,使得請求體中能夠包含二進制文件(固然文件中不能包含boundary)。文件上傳正是利用這種機制來完成的。

若是不設置<form>enctype編碼,一樣能夠在表單中設置type=file類型的輸入框,可是請求體和傳統的表單同樣,這樣服務器程序沒法獲取真正的文件內容。

在服務端,爲了支持文件上傳咱們還須要進行一些配置。

控制器邏輯

對於表單中的文本信息輸入,咱們能夠經過@RequestParam註解獲取。對於上傳的二進制文件(文本文件一樣會轉化爲byte[]進行傳輸),就須要藉助Spring提供的MultipartFile類來獲取了:

@Controller
public class FileUploadController {

    @PostMapping("/upload")
    @ResponseBody
    public String handleFileUpload(@RequestParam("file") MultipartFile file) {
        byte[] bytes = file.getBytes();

        return "file uploaded successfully."
    }
}

經過MultipartFilegetBytes()方法便可以獲得上傳的文件內容(<form>中定義了一個type="file"的,在這裏咱們能夠將它保存到本地磁盤。另外,在默認的狀況下Spring僅僅支持大小爲128KB的文件,爲了調整它,咱們能夠修改Spring的配置文件src/main/resources/application.properties

multipart.maxFileSize: 128KB
multipart.maxRequestSize: 128KB

修改上述數值便可完成配置。

HTML表單

HTML中支持文件上傳的表單元素仍然是<input>,只不過它的類型是file

<html>
<body>
  <form method="POST" enctype="multipart/form-data" action="/upload">
    File to upload: <input type="file" name="file"><br />
    Name: <input type="text" name="name"><br /> <br />
    <input type="submit" value="Upload"> Press here to upload the file!
  </form>
</body>
</html>

multipart/form-data表單既能夠上傳文件類型,也能夠和普通表單同樣提交其餘類型的數據,在Spring MVC的@RequestMapping方法參數中用@RequestParam標註便可(也能夠利用數據綁定機制,綁定一個對象)

攔截器Interceptor

Spring MVC框架中的Interceptor,與Servlet API中的Filter十分相似,用於對Web請求進行預處理/後處理。一般狀況下這些預處理/後處理邏輯是通用的,能夠被應用於全部或多個Web請求,例如:

  • 記錄Web請求相關日誌,能夠用於作一些信息監控、統計、分析
  • 檢查Web請求訪問權限,例如發現用戶沒有登陸後,重定向到登陸頁面
  • 打開/關閉數據庫鏈接——預處理時打開,後處理關閉,能夠避免在全部業務方法中都編寫相似代碼,也不會忘記關閉數據庫鏈接

Spring MVC請求處理流程

1.png

上圖是Spring MVC框架處理Web請求的基本流程,請求會通過DispatcherServlet的分發後,會按順序通過一系列的Interceptor並執行其中的預處理方法,在請求返回時一樣會執行其中的後處理方法。

DispatcherServletController之間哪些豎着的彩色細條,是攔截請求進行額外處理的地方,因此命名爲攔截器(Interceptor)。

HandlerInterceptor接口

Spring MVC中攔截器是實現了HandlerInterceptor接口的Bean:

public interface HandlerInterceptor {
    boolean preHandle(HttpServletRequest request, 
                      HttpServletResponse response, 
                      Object handler) throws Exception;

    void postHandle(HttpServletRequest request, 
                    HttpServletResponse response, 
                    Object handler, ModelAndView modelAndView) throws Exception;

    void afterCompletion(HttpServletRequest request, 
                         HttpServletResponse response, 
                         Object handler, Exception ex) throws Exception;
}
  • preHandle():預處理回調方法,若方法返回值爲true,請求繼續(調用下一個攔截器或處理器方法);若方法返回值爲false,請求處理流程中斷,不會繼續調用其餘的攔截器或處理器方法,此時須要經過response產生響應;
  • postHandle():後處理回調方法,實現處理器的後處理(但在渲染視圖以前),此時能夠經過ModelAndView對模型數據進行處理或對視圖進行處理
  • afterCompletion():整個請求處理完畢回調方法,即在視圖渲染完畢時調用

HandlerInterceptor有三個方法須要實現,但大部分時候可能只須要實現其中的一個方法,HandlerInterceptorAdapter是一個實現了HandlerInterceptor的抽象類,它的三個實現方法都爲空實現(或者返回true),繼承該抽象類後能夠僅僅實現其中的一個方法:

public class Interceptor extends HandlerInterceptorAdapter {

    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        // 在controller方法調用前打印信息
        System.out.println("This is interceptor.");
        // 返回true,將強求繼續傳遞(傳遞到下一個攔截器,沒有其它攔截器了,則傳遞給Controller)
        return true;
    }
}

配置Interceptor

定義HandlerInterceptor後,須要建立WebMvcConfigurerAdapter在MVC配置中將它們應用於特定的URL中。通常一個攔截器都是攔截特定的某一部分請求,這些請求經過URL模型來指定。

下面是一個配置的例子:

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LocaleInterceptor());
        registry.addInterceptor(new ThemeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
        registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*");
    }
}

@ModelAttribute

方法使用@ModelAttribute標註

@ModelAttribute標註可被應用在方法或方法參數上。

標註在方法上的@ModelAttribute說明方法是用於添加一個或多個屬性到model上。這樣的方法能接受與@RequestMapping標註相同的參數類型,只不過不能直接被映射到具體的請求上。

在同一個控制器中,標註了@ModelAttribute的方法實際上會在@RequestMapping方法以前被調用。

如下是示例:

// Add one attribute
// The return value of the method is added to the model under the name "account"
// You can customize the name via @ModelAttribute("myAccount")

@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountManager.findAccount(number);
}

// Add multiple attributes

@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountManager.findAccount(number));
    // add more ...
}

@ModelAttribute方法一般被用來填充一些公共須要的屬性或數據,好比一個下拉列表所預設的幾種狀態,或者寵物的幾種類型,或者去取得一個HTML表單渲染所須要的命令對象,好比Account等。

@ModelAttribute標註方法有兩種風格:

  • 在第一種寫法中,方法經過返回值的方式默認地將添加一個屬性;
  • 在第二種寫法中,方法接收一個Model對象,而後能夠向其中添加任意數量的屬性。

能夠在根據須要,在兩種風格中選擇合適的一種。

一個控制器能夠擁有多個@ModelAttribute方法。同個控制器內的全部這些方法,都會在@RequestMapping方法以前被調用。

@ModelAttribute方法也能夠定義在@ControllerAdvice標註的類中,而且這些@ModelAttribute能夠同時對許多控制器生效。

屬性名沒有被顯式指定的時候又當如何呢?在這種狀況下,框架將根據屬性的類型給予一個默認名稱。舉個例子,若方法返回一個Account類型的對象,則默認的屬性名爲"account"。能夠經過設置@ModelAttribute標註的值來改變默認值。當向Model中直接添加屬性時,請使用合適的重載方法addAttribute(..)-即帶或不帶屬性名的方法。

@ModelAttribute標註也能夠被用在@RequestMapping方法上。這種狀況下,@RequestMapping方法的返回值將會被解釋爲model的一個屬性,而非一個視圖名,此時視圖名將以視圖命名約定來方式來肯定。

方法參數使用@ModelAttribute標註

@ModelAttribute標註既能夠被用在方法上,也能夠被用在方法參數上。

標註在方法參數上的@ModelAttribute說明了該方法參數的值將由model中取得。若是model中找不到,那麼該參數會先被實例化,而後被添加到model中。在model中存在之後,請求中全部名稱匹配的參數都會填充到該參數中。

這在Spring MVC中被稱爲數據綁定,一個很是有用的特性,咱們不用每次都手動從表格數據中轉換這些字段數據。

@RequestMapping(path = "/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@ModelAttribute Pet pet) { }

以上面的代碼爲例,這個Pet類型的實例可能來自哪裏呢?有幾種可能:

  • 它可能由於@SessionAttributes標註的使用已經存在於model中
  • 它可能由於在同個控制器中使用了@ModelAttribute方法已經存在於model中——正如上一小節所敘述的
  • 它多是由URI模板變量和類型轉換中取得的(下面會詳細講解)
  • 它多是調用了自身的默認構造器被實例化出來的

@ModelAttribute方法經常使用於從數據庫中取一個屬性值,該值可能經過@SessionAttributes標註在請求中間傳遞。在一些狀況下,使用URI模板變量和類型轉換的方式來取得一個屬性是更方便的方式。這裏有個例子:

@RequestMapping(path = "/accounts/{account}", method = RequestMethod.PUT)
public String save(@ModelAttribute("account") Account account) {

}

這個例子中,model屬性的名稱("account")與URI模板變量的名稱相匹配。若是配置了一個能夠將String類型的帳戶值轉換成Account類型實例的轉換器Converter<String, Account>,那麼上面這段代碼就能夠工做的很好,而不須要再額外寫一個@ModelAttribute方法。

下一步就是數據的綁定。WebDataBinder類能將請求參數——包括字符串的查詢參數和表單字段等——經過名稱匹配到model的屬性上。成功匹配的字段在須要的時候會進行一次類型轉換(從String類型到目標字段的類型),而後被填充到model對應的屬性中。

進行了數據綁定後,則可能會出現一些錯誤,好比沒有提供必須的字段、類型轉換過程的錯誤等。若想檢查這些錯誤,能夠在標註了@ModelAttribute的參數緊跟着聲明一個BindingResult參數:

@RequestMapping(path = "/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {
    if (result.hasErrors()) {
        return "petForm";
    }

    // ...

}

拿到BindingResult參數後,能夠檢查是否有錯誤,能夠經過Spring的<errors>表單標籤來在同一個表單上顯示錯誤信息。

BindingResult被用於記錄數據綁定過程的錯誤,所以除了數據綁定外,還能夠把該對象傳給本身定製的驗證器來調用驗證。這使得數據綁定過程和驗證過程出現的錯誤能夠被蒐集到一塊兒,而後一併返回給用戶:

@RequestMapping(path = "/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {

    new PetValidator().validate(pet, result);
    if (result.hasErrors()) {
        return "petForm";
    }

    // ...

}

又或者能夠經過添加一個JSR-303規範的@Valid標註,這樣驗證器會自動被調用。

@RequestMapping(path = "/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) {

    if (result.hasErrors()) {
        return "petForm";
    }

    // ...

}

異常處理

Spring MVC框架提供了多種機制用來處理異常,初次接觸可能會對他們用法以及適用的場景感到困惑。如今以一個簡單例子來解釋這些異常處理的機制。

假設如今咱們開發了一個博客應用,其中最重要的資源就是文章(Post),應用中的URL設計以下:

  • 獲取文章列表:GET /posts/
  • 添加一篇文章:POST /posts/
  • 獲取一篇文章:GET /posts/{id}
  • 更新一篇文章:PUT /posts/{id}
  • 刪除一篇文章:DELETE /posts/{id}

這是很是標準的複合RESTful風格的URL設計,在Spring MVC實現的應用過程當中,相應也會有5個對應的用@RequestMapping註解的方法來處理相應的URL請求。在處理某一篇文章的請求中(獲取、更新、刪除),無疑須要作這樣一個判斷——請求URL中的文章id是否在於系統中,若是不存在須要返回404 Not Found

使用HTTP狀態碼

在默認狀況下,Spring MVC處理Web請求時若是發現存在沒有應用代碼捕獲的異常,那麼會返回HTTP 500(Internal Server Error)錯誤。可是若是該異常是咱們本身定義的而且使用@ResponseStatus註解進行修飾,那麼Spring MVC則會返回指定的HTTP狀態碼:

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No Such Post")//404 Not Found
public class PostNotFoundException extends RuntimeException {
}

Controller中能夠這樣使用它:

@RequestMapping(value = "/posts/{id}", method = RequestMethod.GET)
public String showPost(@PathVariable("id") long id, Model model) {
    Post post = postService.get(id);
    if (post == null) throw new PostNotFoundException("post not found");
    model.addAttribute("post", post);
    return "postDetail";
}

這樣若是咱們訪問了一個不存在的文章,那麼Spring MVC會根據拋出的PostNotFoundException上的註解值返回一個HTTP 404 Not Found給瀏覽器。

最佳實踐

上述場景中,除了獲取一篇文章的請求,還有更新和刪除一篇文章的方法中都須要判斷文章id是否存在。在每個方法中都加上if (post == null) throw new PostNotFoundException("post not found");是一種解決方案,但若是有10個、20個包含/posts/{id}的方法,雖然只有一行代碼但讓他們重複10次、20次也是很是不優雅的。

爲了解決這個問題,能夠將這個邏輯放在Service中實現:

@Service
public class PostService {

    @Autowired
    private PostRepository postRepository;

    public Post get(long id) {
        return postRepository.findById(id)
                .orElseThrow(() -> new PostNotFoundException("post not found"));
    }
}

這裏`PostRepository`繼承了`JpaRepository`,能夠定義`findById`方法返回一個`Optional<Post>`——若是不存在則Optional爲空,拋出異常。

這樣在全部的Controller方法中,只須要正常獲取文章便可,全部的異常處理都交給了Spring MVC。

Controller中處理異常

Controller中的方法除了能夠用於處理Web請求,還可以用於處理異常處理——爲它們加上@ExceptionHandler便可:

@Controller
public class ExceptionHandlingController {

  // @RequestHandler methods
  ...

  // Exception handling methods

  // Convert a predefined exception to an HTTP Status code
  @ResponseStatus(value=HttpStatus.CONFLICT, reason="Data integrity violation")  // 409
  @ExceptionHandler(DataIntegrityViolationException.class)
  public void conflict() {
    // Nothing to do
  }

  // Specify the name of a specific view that will be used to display the error:
  @ExceptionHandler({SQLException.class,DataAccessException.class})
  public String databaseError() {
    // Nothing to do.  Returns the logical view name of an error page, passed to
    // the view-resolver(s) in usual way.
    // Note that the exception is _not_ available to this view (it is not added to
    // the model) but see "Extending ExceptionHandlerExceptionResolver" below.
    return "databaseError";
  }

  // Total control - setup a model and return the view name yourself. Or consider
  // subclassing ExceptionHandlerExceptionResolver (see below).
  @ExceptionHandler(Exception.class)
  public ModelAndView handleError(HttpServletRequest req, Exception exception) {
    logger.error("Request: " + req.getRequestURL() + " raised " + exception);

    ModelAndView mav = new ModelAndView();
    mav.addObject("exception", exception);
    mav.addObject("url", req.getRequestURL());
    mav.setViewName("error");
    return mav;
  }
}

首先須要明確的一點是,在Controller方法中的@ExceptionHandler方法只可以處理同一個Controller中拋出的異常。這些方法上同時也能夠繼續使用@ResponseStatus註解用於返回指定的HTTP狀態碼,但同時還可以支持更加豐富的異常處理:

  • 渲染特定的視圖頁面
  • 使用ModelAndView返回更多的業務信息

大多數網站都會使用一個特定的頁面來響應這些異常,而不是直接返回一個HTTP狀態碼或者顯示Java異常調用棧。固然異常信息對於開發人員是很是有用的,若是想要在視圖中直接看到它們能夠這樣渲染模板(以JSP爲例):

<h1>Error Page</h1>
<p>Application has encountered an error. Please contact support on ...</p>

<!--
Failed URL: ${url}
Exception:  ${exception.message}
<c:forEach items="${exception.stackTrace}" var="ste">    ${ste} 
</c:forEach>
-->

全局異常處理

@ControllerAdvice提供了和上一節同樣的異常處理能力,可是能夠被應用於Spring應用上下文中的全部@Controller

@ControllerAdvice
class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.CONFLICT)  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleConflict() {
        // Nothing to do
    }
}

Spring MVC默認對於沒有捕獲也沒有被@ResponseStatus以及@ExceptionHandler聲明的異常,會直接返回500,這顯然並不友好,能夠在@ControllerAdvice中對其進行處理(例如返回一個友好的錯誤頁面,引導用戶返回正確的位置或者提交錯誤信息):

@ControllerAdvice
class GlobalDefaultExceptionHandler {
    public static final String DEFAULT_ERROR_VIEW = "error";

    @ExceptionHandler(value = Exception.class)
    public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
        // If the exception is annotated with @ResponseStatus rethrow it and let
        // the framework handle it - like the OrderNotFoundException example
        // at the start of this post.
        // AnnotationUtils is a Spring Framework utility class.
        if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)
            throw e;

        // Otherwise setup and send the user to a default error-view.
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", e);
        mav.addObject("url", req.getRequestURL());
        mav.setViewName(DEFAULT_ERROR_VIEW);
        return mav;
    }
}

總結

Spring在異常處理方面提供了一如既往的強大特性和支持,那麼在應用開發中咱們應該如何使用這些方法呢?如下提供一些經驗性的準則:

  • 不要在@Controller中本身進行異常處理邏輯。即便它只是一個Controller相關的特定異常,在@Controller中添加一個@ExceptionHandler方法處理。
  • 對於自定義的異常,能夠考慮對其加上@ResponseStatus註解
  • 使用@ControllerAdvice處理通用異常(例如資源不存在、資源存在衝突等)
相關文章
相關標籤/搜索