Spring MVC快速入門教程

今天給大家介紹一下Spring MVC,讓我們學習一下如何利用Spring MVC快速的搭建一個簡單的web應用。

更深入地學習Spring MVC,請大家參考Spring MVC實戰入門訓練

參考代碼請戳右上角,下載下來後可以在Eclipse或者IntelliJ中導入爲一個Maven項目。

環境準備

  • 一個稱手的文本編輯器(例如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/com.tmy.Application.java:

package com.tmy;

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處理通用異常(例如資源不存在、資源存在衝突等)

進一步閱讀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章