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 的快速入門
使用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:8080,Hello World!
就出如今了頁面中。只用了區區十幾行Java代碼,一個Hello World應用就能夠正確運行了,那麼這段代碼究竟作了什麼呢?咱們從程序的入口SpringApplication.run(Application.class, args);
開始分析:
SpringApplication
是Spring Boot框架中描述Spring應用的類,它的run()
方法會建立一個Spring應用上下文(Application Context)。另外一方面它會掃描當前應用類路徑上的依賴,例如本例中發現spring-webmvc
(由 spring-boot-starter-web
傳遞引入)在類路徑中,那麼Spring Boot會判斷這是一個Web應用,並啓動一個內嵌的Servlet容器(默認是Tomcat)用於處理HTTP請求。
Spring WebMvc框架會將Servlet容器裏收到的HTTP請求根據路徑分發給對應的@Controller
類進行處理,@RestController
是一類特殊的@Controller
,它的返回值直接做爲HTTP Response的Body部分返回給瀏覽器。
@RequestMapping
註解代表該方法處理那些URL對應的HTTP請求,也就是咱們常說的URL路由(routing),請求的分發工做是有Spring完成的。例如上面的代碼中http://localhost:8080/ 根路徑就被路由至greeting()
方法進行處理。若是訪問http://localhost:8080/hello ,則會出現 404 Not Found錯誤,由於咱們並無編寫任何方法來處理
/hello`請求。
@Controller
實現URL路由現代Web應用每每包括不少頁面,不一樣的頁面也對應着不一樣的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的處理方法分散在不一樣的類中。
在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請求除了其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方法有四種:
這四個方法能夠對應到CRUD操做(Create、Read、Update和Delete),好比博客的建立操做,按照REST風格設計URL就應該使用PUT方法,讀取博客使用GET方法,更新博客使用POST方法,刪除博客使用DELETE方法。
每個Web請求都是屬於其中一種,在Spring MVC中若是不特殊指定的話,默認是GET請求。
好比@RequestMapping("/")
和@RequestMapping("/hello")
和對應的Web請求是:
/
/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.css
和js/main.js
文件後,Spring MVC可以自動將他們發佈,經過訪問/css/style.css
, /js/main.js
也就能夠正確加載這些資源。
Spring MVC還可以支持更爲複雜的HTTP請求——文件資源。咱們在網站中常常遇到上傳圖片、附件一類的需求,就是經過文件上傳技術來實現的。
處理文件的表單和普通表單的惟一區別在於設置enctype
——multipart編碼方式則須要設置enctype
爲multipart/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." } }
經過MultipartFile
的getBytes()
方法便可以獲得上傳的文件內容(<form>
中定義了一個type="file"
的,在這裏咱們能夠將它保存到本地磁盤。另外,在默認的狀況下Spring僅僅支持大小爲128KB的文件,爲了調整它,咱們能夠修改Spring的配置文件src/main/resources/application.properties
:
multipart.maxFileSize: 128KB multipart.maxRequestSize: 128KB
修改上述數值便可完成配置。
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
標註便可(也能夠利用數據綁定機制,綁定一個對象)
Spring MVC框架中的Interceptor,與Servlet API中的Filter十分相似,用於對Web請求進行預處理/後處理。一般狀況下這些預處理/後處理邏輯是通用的,能夠被應用於全部或多個Web請求,例如:
上圖是Spring MVC框架處理Web請求的基本流程,請求會通過DispatcherServlet
的分發後,會按順序通過一系列的Interceptor
並執行其中的預處理方法,在請求返回時一樣會執行其中的後處理方法。
在DispatcherServlet
和Controller
之間哪些豎着的彩色細條,是攔截請求進行額外處理的地方,因此命名爲攔截器(Interceptor)。
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; } }
定義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
說明方法是用於添加一個或多個屬性到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
說明了該方法參數的值將由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中——正如上一小節所敘述的@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
。
在默認狀況下,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
處理通用異常(例如資源不存在、資源存在衝突等)