RESTful
一種軟件架構風格、設計風格,而不是標準,只是提供了一組設計原則和約束條件。它主要用於客戶端和服務器交互類的軟件。基於這個風格設計的軟件可以更簡潔,更有層次,更易於實現緩存等機制。本篇博客主要講述使用Spring MVC
開發RESTful
風格的API
。
一、傳統API和RESTful API
傳統的API
和RESTful API
如下表所示:
行爲 | 傳統API | RESTful API | 方法 |
---|---|---|---|
查詢 | /user/query?name=lemon |
/user?name=lemon |
GET |
詳情 | /user/getInfo?id=1 |
/user/1 |
GET |
創建 | /user/create?name=lemon |
/user |
POST |
修改 | /user/update?id=1&name=tom |
/user/1 |
POST |
刪除 | /user/delete?id=1 |
/user/1 |
GET |
RESTful
風格的API
有如下幾個特點:
-
使用
URL
描述資源 -
使用
HTTP
方法描述行爲,使用HTTP
狀態碼來表示不同的結果 -
使用
JSON
進行數據交互 -
RESTful
只是一種風格,並不是一種強制的標準
二、常用註解介紹
這裏介紹幾個常用的註解:
-
@RestController
標明此Controller
提供RESTful API
-
@RequestMapping
及其變體(@GetMapping
、PostMapping
等),映射HTTP
請求到Java
方法 -
@RequestParam
映射請求參數到Java
方法的參數 -
@PathVariable
映射URL
片段到Java
方法的參數 -
@PageableDefault
指定默認分頁參數 -
@JsonView
按照指定方式序列化Java
對象
代碼案例:這裏有User
和UserController
以及UserControllerTest
三個類,其中UserControllerTest
的四個測試方法分別對應UserController
類中的四個方法。
User
類
package com.lemon.security.web.dto;
import lombok.Data;
/**
* @author lemon
* @date 2018/3/22 下午3:40
*/
@Data
public class User {
private String username;
private String password;
}
UserController
類
package com.lemon.security.web.controller;
import com.lemon.security.web.dto.User;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
/**
* @author lemon
* @date 2018/3/22 下午3:39
*/
@RestController
public class UserController {
@RequestMapping(value = "/user1", method = RequestMethod.GET)
public List<User> query1() {
return generateUsers();
}
@GetMapping("/user2")
public List<User> query2(@RequestParam String username) {
System.out.println(username);
return generateUsers();
}
@GetMapping("/user3/{username}")
public List<User> query3(@PathVariable String username) {
System.out.println(username);
return generateUsers();
}
@GetMapping("/user4")
public List<User> query4(@PageableDefault(page = 1, size = 2, sort = "username") Pageable pageable) {
System.out.println(pageable.getPageNumber());
System.out.println(pageable.getPageSize());
System.out.println(pageable.getSort());
return generateUsers();
}
private List<User> generateUsers() {
List<User> users = new ArrayList<>();
users.add(new User());
users.add(new User());
users.add(new User());
return users;
}
}
UserControllerTest
類
package com.lemon.security.web;
import com.lemon.security.web.application.MainApplication;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
/**
* @author lemon
* @date 2018/3/22 下午3:14
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MainApplication.class)
public class UserControllerTest {
// 注入一個web應用環境(容器)
@Autowired
private WebApplicationContext webApplicationContext;
// MVC環境對象
private MockMvc mockMvc;
@Before
public void init() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
public void query1() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/user1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
}
@Test
public void query2() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/user2")
.param("username", "lemon")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
}
@Test
public void query3() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/user3/lemon")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
}
@Test
public void query4() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/user4")
.param("size", "3")
.param("page", "1")
.param("sort", "username,desc")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));
}
}
第三個類,也就是UserControllerTest
是RESTful API
的測試類,現在對其進行簡單介紹:
由於RESSTful
風格的API
不能通過瀏覽器地址欄來進行測試,因爲地址欄發送的請求都是GET
類型的,而RESTful API
正是通過請求方法來判斷請求行爲是查詢、修改、刪除、增加中的哪一種的,所以測試RESSTful
風格的API
都是通過編碼來進行測試的。
-
通過
@Autowired WebApplicationContext webApplicationContext
:注入web
環境的ApplicationContext
容器; -
然後通過
MockMvcBuilders.webAppContextSetup(webApplicationContext).build()
創建一個MockMvc
的MVC
環境進行測試; -
MockMvcRequestBuilders.get()
方法是發送一個GET
請求,param()
是設置請求參數,contentType()
是我設置內容類型(JSON
格式),andExpect()
方法是希望得到什麼樣的測試結果,MockMvcResultMatchers()
是返回結果的匹配是否正確。jsonPath()
方法是解析返回的JSON
數據,關於它的介紹可以在github
上找到。
運行上面的四個測試方法都可以通過測試。對於@PathVariable
再寫一個測試案例:
Controller
方法:
@GetMapping("/getInfo/{id:\\d+}")
public User getInfo(@PathVariable String id) {
System.out.println("查詢的對象ID爲:".concat(id));
User user = new User();
user.setUsername("lemon");
return user;
}
上面的方法URL
片段進行了正則表達式的驗證,ID
只能是數字。
- 測試方法:
@Test
public void getInfo() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/getInfo/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.username").value("lemon"));
}
接下來詳細介紹@JsonView這個註解的使用。
@JsonView
的使用步驟
-
使用接口來聲明多個視圖
-
在值對象的
get
方法上指定視圖 -
在
Controller
方法上指定視圖
對於上面的步驟,進行如下解釋如下:
一般對Java
對象進行序列化Json
的時候,會考慮到只序列化部分字段,那麼就可以使用@JsonView
這個註解。在這裏使用User
實體類進行舉例,首先,在實體類上定義兩個接口,第一個接口是簡單視圖(UserSimpleView
),表示之序列化username
這個字段,而第二個接口是詳情視圖(UserDetailView extends UserSimpleView
),表示不僅序列化username
字段,還序列化password
字段。然後使用@JsonView
註解將兩個視圖綁定到對應的字段的get
方法上面,由於UserDetailView
繼承了UserSimpleView
這個視圖,所以在Controller
方法上使用UserDetailView
視圖的時候,會同時序列化兩個字段,而使用UserSimpleView
的時候僅僅只會序列化username
這一個字段。下面進行代碼展示:
- User類
package com.lemon.security.web.dto;
import com.fasterxml.jackson.annotation.JsonView;
/**
* @author lemon
* @date 2018/3/22 下午3:40
*/
public class User {
public interface UserSimpleView {}
public interface UserDetailView extends UserSimpleView {}
private String username;
private String password;
@JsonView(UserSimpleView.class)
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@JsonView(UserDetailView.class)
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
- UserController的兩個方法
@GetMapping("/getSimpleUser")
@JsonView(User.UserSimpleView.class)
public User getSimpleUser() {
User user = new User();
user.setUsername("lemon");
user.setPassword("123456");
return user;
}
@GetMapping("/getDetailUser")
@JsonView(User.UserDetailView.class)
public User getDetailUser() {
User user = new User();
user.setUsername("lemon");
user.setPassword("123456");
return user;
}
從上面的步驟分析可知,第一個方法返回的user對象在序列化爲json的時候,只會序列化username字段,而第二個方法則會同時序列化兩個字段。
- 兩個測試方法
@Test
public void getSimpleUser() throws Exception {
String result = mockMvc.perform(MockMvcRequestBuilders.get("/getSimpleUser")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
@Test
public void getDetailUser() throws Exception {
String result = mockMvc.perform(MockMvcRequestBuilders.get("/getDetailUser")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
兩個方法打印的結果分別爲:
{"username":"lemon"}
{"username":"lemon","password":"123456"}
三、編寫RESTful API
1、用戶詳情請求(GET)
對於RESTful API
,一般都不再使用傳統的參數傳遞,而是使用資源映射的方式,也就是使用@PathVariable
,爲了保持文檔的完整性,這裏再次使用上面已經舉過的案例:
Controller
方法:
@GetMapping("/getInfo/{id:\\d+}")
public User getInfo(@PathVariable String id) {
System.out.println("查詢的對象ID爲:".concat(id));
User user = new User();
user.setUsername("lemon");
return user;
}
上面的方法URL
片段進行了正則表達式的驗證,ID
只能是數字。
- 測試方法:
@Test
public void getInfo() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/getInfo/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.username").value("lemon"));
}
2、用戶創建請求(POST)
這裏主要介紹三個知識點:
-
@RequestBody
映射請求體到Java
方法參數 -
@Valid
註解和BindingResult
驗證請求參數的合法性並處理校驗結果 -
@RequestBody
是將前臺傳遞過來的JSON
字符串轉換成Java
對象,
1)第一個知識點的案例,將JSON
字符串映射到Java
對象中
在之前的User
類上加上一個id
字段,然後進行下面的測試。
Controller
方法:用戶創建的方法
@PostMapping("/user1")
public User create1(@RequestBody User user) {
System.out.println(ReflectionToStringBuilder.reflectionToString(user, ToStringStyle.MULTI_LINE_STYLE));
user.setId(1);
return user;
}
測試方法:測試創建用戶方法
@Test
public void create1() throws Exception {
String content = "{\"username\":\"lemon\",\"password\":\"123456\"}";
mockMvc.perform(MockMvcRequestBuilders.post("/user1")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1));
}
測試方法傳遞過來的數據是一個JSON
字符串,正是@RequestBody
註解將JSON
字符串轉化成爲Java
對象。
2)第二個知識點的案例,@Valid
註解和BindingResult
驗證請求參數的合法性並處理校驗結果
當使用Java
類來接受參數的是,往往需要對參數進行校驗,而校驗一般都是使用Hibernate
提供的校驗器來進行校驗,在Java
實體類的字段上,我們常常加上@NotBlank
、@NotNull
、@Null
、@Min
、@Max
、@NotEmpty
等註解進行校驗規則定義,然後在Controller
方法參數前加上@Valid
註解來進行校驗,校驗的錯誤結果存儲在BindingResult
對象內。這裏我向後臺傳遞一個JSON
字符串,人爲使得username
和password
兩個字段爲null
。這裏僅僅簡單介紹表單驗證的註解,下一篇博客將重點介紹。接下來請看案例:
- User類字段
private Integer id;
@NotEmpty(message = "用戶名不能爲空")
private String username;
@NotEmpty(message = "密碼不能爲空")
private String password;
private Date birthday;
- UserController的create2()方法
@PostMapping("/user2")
public User create2(@Valid @RequestBody User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
bindingResult.getAllErrors().forEach(error -> System.out.println(error.getDefaultMessage()));
}
System.out.println(ReflectionToStringBuilder.reflectionToString(user, ToStringStyle.MULTI_LINE_STYLE));
user.setId(2);
return user;
}
- 測試方法
@Test
public void create2() throws Exception {
String content = "{\"username\":null,\"password\":null}";
mockMvc.perform(MockMvcRequestBuilders.post("/user2")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(2));
}
運行結果爲:
用戶名不能爲空
密碼不能爲空
com.lemon.security.web.dto.User@58d79479[
id=<null>
username=<null>
password=<null>
birthday=<null>
]
3、用戶修改和刪除請求(PUT、DELETE)
由於RESTful
風格的API
是基於方法來進行區分的,所以設計到數據的修改和刪除使用的方法是PUT
和DELETE
,接下來使用案例的方式介紹修改和刪除API
的開發。
- 測試方法:
@Test
public void update() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.put("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.username").value("lemon"));
}
@Test
public void delete() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.delete("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.status().isOk());
}
Controller
方法:
@PutMapping("/user/{id:\\d+}")
public User update(@PathVariable Integer id) {
User user = new User();
user.setId(id);
System.out.println("模擬修改");
user.setUsername("lemon");
return user;
}
@DeleteMapping("/user/{id:\\d+}")
public void delete(@PathVariable Integer id) {
System.out.println("模擬修改,修改ID:".concat(String.valueOf(id)));
}
回顧一下RESTful風格的API,都是使用URL描述資源,使用請求方法來區別不同的API。這極大程度地簡化了API開發的流程,推薦使用。
Spring Security技術棧開發企業級認證與授權系列文章列表:
Spring Security技術棧學習筆記(一)環境搭建
Spring Security技術棧學習筆記(二)RESTful API詳解
Spring Security技術棧學習筆記(三)表單校驗以及自定義校驗註解開發
Spring Security技術棧學習筆記(四)RESTful API服務異常處理
Spring Security技術棧學習筆記(五)使用Filter、Interceptor和AOP攔截REST服務
Spring Security技術棧學習筆記(六)使用REST方式處理文件服務
Spring Security技術棧學習筆記(七)使用Swagger自動生成API文檔
Spring Security技術棧學習筆記(八)Spring Security的基本運行原理與個性化登錄實現
Spring Security技術棧學習筆記(九)開發圖形驗證碼接口
Spring Security技術棧學習筆記(十)開發記住我功能
Spring Security技術棧學習筆記(十一)開發短信驗證碼登錄
Spring Security技術棧學習筆記(十二)將短信驗證碼驗證方式集成到Spring Security
Spring Security技術棧學習筆記(十三)Spring Social集成第三方登錄驗證開發流程介紹
Spring Security技術棧學習筆記(十四)使用Spring Social集成QQ登錄驗證方式
Spring Security技術棧學習筆記(十五)解決Spring Social集成QQ登錄後的註冊問題
Spring Security技術棧學習筆記(十六)使用Spring Social集成微信登錄驗證方式
示例代碼下載地址:
項目已經上傳到碼雲,歡迎下載,內容所在文件夾爲
chapter002
。
更多幹貨分享,歡迎關注我的微信公衆號:爪哇論劍(微信號:itlemon)