Spring MVC快速入門教程
必看!!!
這篇文章講的很好很實用,應對Spring MVC基礎知識足夠了
文章轉載自:https://www.tianmaying.com/tutorial/spring-mvc-quickstart
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
今天給大家介紹一下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: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,通常需要不同的方法進行處理並返回不同的內容。
匹配多個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.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表單
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請求處理流程
上圖是Spring MVC框架處理Web請求的基本流程,請求會經過DispatcherServlet
的分發後,會按順序經過一系列的Interceptor
並執行其中的預處理方法,在請求返回時同樣會執行其中的後處理方法。
在DispatcherServlet
和Controller
之間哪些豎著的彩色細條,是攔截請求進行額外處理的地方,所以命名為攔截器(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
處理通用異常(例如資源不存在、資源存在衝突等)
進一步閱讀
版權宣告
本文由Ricky創作,轉載需署名作者且註明文章出處
參考程式碼
要獲取本文的參考程式碼,請訪問: https://www.tianmaying.com/tutorial/spring-mvc-quickstart/repo
相關文章
- 基於Spring的QQ第三方登入實現
- 基於Spring的微信第三方登入實現
- 基於Spring的新浪微博第三方登入實現
- 基於Spring提供支援不同裝置的頁面
- Spring Boot應用開發初探與示例
- 基於Spring Boot為關係型資料庫構建REST訪問介面
- 部署Spring Boot應用
- 測試Spring MVC應用
- 在Docker容器中執行Spring Boot應用
- Spring Boot——開發新一代Spring Java應用
其他文章
- Vim 多檔案編輯:緩衝區
- iOS開發基礎:迅速掌握Objective C
- 初次接觸OSGI Blueprint
- LeetCode題解 #50 Pow(x, n)
- LeetCode題解 #66 Plus One
- 如何打造一款網際網路軟體產品
- Spring處理表單提交
- LeetCode題解 #58 Length of Last Word
- 學習App開發的兩個關鍵點
- Swift訪問通訊錄
評論
謝謝提醒,或者給Application
加上@ComponentScan
標註,指定需要掃描的包。
參考程式碼中是放在一個包裡的,見SpringMvcQuickstartApplication
老師,這個教程可能需要稍微修改下。
因為直接執行會報這個錯誤:
Your ApplicationContext is unlikely to start due to a @ComponentScan of the default package
原因是: application.java 檔案不能直接放在main/java資料夾下,必須要建一個包把他放進去
非常好的教程,感謝!@David
看Url中包含的變數名,應該是想實現類似Github程式碼託管:https://github.com/pinterest/mysql_utils/blob/master/lib/backup.py
restOfUrl變數中間可能包含slash(/)
第一種辦法使用**
來匹配不定長的Url:
@RequestMapping("/{repoName}/**")
public Map searchWithSearchTerm(HttpServletRequest request, @PathVariable("repoName") String repoName) {
// Don't repeat a pattern
String pattern = (String)
request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
String restOfUrl = new AntPathMatcher().extractPathWithinPattern(pattern,
request.getServletPath());
...
}
第二種方法:
@RequestMapping(value = "/{repoName}/{path:.+}", method = RequestMethod.GET)
public Object path(@PathVariable String repoName,
@PathVariable String path)
{path:.+}
表示匹配任意字元,但是由於Spring MVC預設將/作為Url分隔符,所以需要更改一下配置:
@Configuration
@EnableWebMvc
public class WebMvc extends WebMvcConfigurerAdapter {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setUrlDecode(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
@RequestMapping
中獲取@PathVariable
時,如何獲取一個完整的路徑?舉例:
/{repoName}/{restOfUrl}應該可以解析/spring-boot/src/main/java/org/springframework
,其中
repoName = spring-boot
restOfUrl = src/main/java/org/springframework
其中restOfUrl
中可能會包含路徑分隔符/
@cser Spring的依賴注入使得我們的程式碼非常容易進行單元測試——@Controller
, @Service
,@Entity
等註解標註的類基本都是POJO(plain old Java object),也就是說很少依賴於Spring容器本身的API。我們可以非常容易地使用JUnit或TestNG編寫測試程式碼。另一方面,對於三層架構的Spring Web應用(Controller, Service, DAO),使用Mock活Stub方法也能夠更好的來測試我們的程式碼邏輯。例如Service層程式碼的單元測試中,依賴的DAO(或Repository)物件都是根據應用測試需求Mock出來的,而不需要真正去訪問資料庫。
參考這篇文章測試Spring MVC應用
Spring MVC的應用測試有什麼建議? @David
Spring MVC最新的版本中提供了一種更加簡潔的配置HTTP方法的方式,增加了四個標註:
@PutMapping
@GetMapping
@PostMapping
@DeleteMapping
對應於Web應用中常用的HTTP方法有四種:
- PUT方法用來新增的資源
- GET方法用來獲取已有的資源
- POST方法用來對資源進行狀態轉換
- DELETE方法用來刪除已有的資源
已新增
能把程式碼貼上來嗎? 參考程式碼是空的啊!