什么是RESTful風格陷猫?
REST是REpresentational State Transfer的縮寫(一般中文翻譯為表述性狀態(tài)轉(zhuǎn)移)踱蛀,REST 是一種體系結(jié)構(gòu),而 HTTP 是一種包含了 REST 架構(gòu)屬性的協(xié)議,為了便于理解组橄,我們把它的首字母拆分成不同的幾個部分:
- 表述性(REpresentational): REST 資源實際上可以用各種形式來進行表述,包括 XML罚随、JSON 甚至 HTML——最適合資源使用者的任意形式玉工;
- 狀態(tài)(State): 當使用 REST 的時候,我們更關(guān)注資源的狀態(tài)而不是對資源采取的行為淘菩;
- 轉(zhuǎn)義(Transfer): REST 涉及到轉(zhuǎn)移資源數(shù)據(jù)遵班,它以某種表述性形式從一個應(yīng)用轉(zhuǎn)移到另一個應(yīng)用。
簡單地說潮改,REST 就是將資源的狀態(tài)以適合客戶端或服務(wù)端的形式從服務(wù)端轉(zhuǎn)移到客戶端(或者反過來)狭郑。在 REST 中,資源通過 URL 進行識別和定位汇在,然后通過行為(即 HTTP 方法)來定義 REST 來完成怎樣的功能翰萨。
實例說明:
在平時的 Web 開發(fā)中,method 常用的值是 GET 和 POST糕殉,但是實際上亩鬼,HTTP 方法還有 PATCH、DELETE阿蝶、PUT 等其他值雳锋,這些方法又通常會匹配為如下的 CRUD 動作:
CRUD 動作 | HTTP 方法 |
---|---|
Create | POST |
Read | GET |
Update | PUT 或 PATCH |
Delete | DELETE |
盡管通常來講,HTTP 方法會映射為 CRUD 動作羡洁,但這并不是嚴格的限制玷过,有時候 PUT 也可以用來創(chuàng)建新的資源,POST 也可以用來更新資源。實際上冶匹,POST 請求非冪等的特性(即同一個 URL 可以得到不同的結(jié)果)使其成一個非常靈活地方法习劫,對于無法適應(yīng)其他 HTTP 方法語義的操作,它都能夠勝任嚼隘。
在使用 RESTful 風格之前诽里,我們?nèi)绻胍?strong>增加一條商品數(shù)據(jù)通常是這樣的:
/addCategory?name=xxx
但是使用了 RESTful 風格之后就會變成:
/category
這就變成了使用同一個 URL ,通過約定不同的 HTTP 方法來實施不同的業(yè)務(wù)飞蛹,這就是 RESTful 風格所做的事情了谤狡,為了有一個更加直觀的理解,引用一下來自how2j.cn的圖:
SpringBoot 中使用 RESTful
下面我使用 SpringBoot 結(jié)合文章:http://blog.didispace.com/springbootrestfulapi/ 來實例演示如何在 SpringBoot 中使用 RESTful 風格的編程并如何做單元測試
RESTful API 具體設(shè)計如下:
User實體定義:
public class User {
private Long id;
private String name;
private Integer age;
// 省略setter和getter
}
實現(xiàn)對User對象的操作接口
@RestController
@RequestMapping(value="/users") // 通過這里配置使下面的映射都在/users下
public class UserController {
// 創(chuàng)建線程安全的Map
static Map<Long, User> users = Collections.synchronizedMap(new HashMap<Long, User>());
@RequestMapping(value="/", method=RequestMethod.GET)
public List<User> getUserList() {
// 處理"/users/"的GET請求卧檐,用來獲取用戶列表
// 還可以通過@RequestParam從頁面中傳遞參數(shù)來進行查詢條件或者翻頁信息的傳遞
List<User> r = new ArrayList<User>(users.values());
return r;
}
@RequestMapping(value="/", method=RequestMethod.POST)
public String postUser(@ModelAttribute User user) {
// 處理"/users/"的POST請求墓懂,用來創(chuàng)建User
// 除了@ModelAttribute綁定參數(shù)之外,還可以通過@RequestParam從頁面中傳遞參數(shù)
users.put(user.getId(), user);
return "success";
}
@RequestMapping(value="/{id}", method=RequestMethod.GET)
public User getUser(@PathVariable Long id) {
// 處理"/users/{id}"的GET請求霉囚,用來獲取url中id值的User信息
// url中的id可通過@PathVariable綁定到函數(shù)的參數(shù)中
return users.get(id);
}
@RequestMapping(value="/{id}", method=RequestMethod.PUT)
public String putUser(@PathVariable Long id, @ModelAttribute User user) {
// 處理"/users/{id}"的PUT請求捕仔,用來更新User信息
User u = users.get(id);
u.setName(user.getName());
u.setAge(user.getAge());
users.put(id, u);
return "success";
}
@RequestMapping(value="/{id}", method=RequestMethod.DELETE)
public String deleteUser(@PathVariable Long id) {
// 處理"/users/{id}"的DELETE請求,用來刪除User
users.remove(id);
return "success";
}
}
編寫測試單元
參考文章:http://tengj.top/2017/12/28/springboot12/#Controller單元測試
看過這幾篇文章之后覺得好棒盈罐,還有這么方便的測試方法榜跌,這些以前都沒有接觸過...
下面針對該Controller編寫測試用例驗證正確性,具體如下盅粪。當然也可以通過瀏覽器插件等進行請求提交驗證钓葫,因為涉及一些包的導入,這里給出全部代碼:
package cn.wmyskxz.springboot;
import cn.wmyskxz.springboot.controller.UserController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockServletContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author: @我沒有三顆心臟
* @create: 2018-05-29-上午 8:39
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = MockServletContext.class)
@WebAppConfiguration
public class ApplicationTests {
private MockMvc mvc;
@Before
public void setUp() throws Exception {
mvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
}
@Test
public void testUserController() throws Exception {
// 測試UserController
RequestBuilder request = null;
// 1票顾、get查一下user列表础浮,應(yīng)該為空
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[]")));
// 2、post提交一個user
request = post("/users/")
.param("id", "1")
.param("name", "測試大師")
.param("age", "20");
mvc.perform(request)
.andExpect(content().string(equalTo("success")));
// 3奠骄、get獲取user列表豆同,應(yīng)該有剛才插入的數(shù)據(jù)
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[{\"id\":1,\"name\":\"測試大師\",\"age\":20}]")));
// 4、put修改id為1的user
request = put("/users/1")
.param("name", "測試終極大師")
.param("age", "30");
mvc.perform(request)
.andExpect(content().string(equalTo("success")));
// 5含鳞、get一個id為1的user
request = get("/users/1");
mvc.perform(request)
.andExpect(content().string(equalTo("{\"id\":1,\"name\":\"測試終極大師\",\"age\":30}")));
// 6诱告、del刪除id為1的user
request = delete("/users/1");
mvc.perform(request)
.andExpect(content().string(equalTo("success")));
// 7、get查一下user列表民晒,應(yīng)該為空
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[]")));
}
}
MockMvc實現(xiàn)了對HTTP請求的模擬精居,從示例的代碼就能夠看出MockMvc的簡單用法,它能夠直接使用網(wǎng)絡(luò)的形式潜必,轉(zhuǎn)換到Controller的調(diào)用靴姿,這樣使得測試速度快、不依賴網(wǎng)絡(luò)環(huán)境磁滚,而且提供了一套驗證的工具佛吓,這樣可以使得請求的驗證統(tǒng)一而且很方便宵晚。
需要注意的就是在MockMvc使用之前需要先用MockMvcBuilders構(gòu)建MockMvc對象,如果對單元測試感興趣的童鞋請戳上面的鏈接哦维雇,這里就不細說了
測試信息
運行測試類淤刃,控制臺返回的信息如下:
__ __ __
/\ \ __/\ \ /\ \
\ \ \/\ \ \ \ ___ ___ __ __ ____\ \ \/'\ __ _ ____
\ \ \ \ \ \ \ /' __` __`\/\ \/\ \ /',__\\ \ , < /\ \/'\/\_ ,`\
\ \ \_/ \_\ \/\ \/\ \/\ \ \ \_\ \/\__, `\\ \ \ \`\\/> </\/_/ /_
\ `\___x___/\ \_\ \_\ \_\/`____ \/\____/ \ \_\ \_\/\_/\_\ /\____\
'\/__//__/ \/_/\/_/\/_/`/___/> \/___/ \/_/\/_/\//\/_/ \/____/
/\___/
\/__/
2018-05-29 09:28:18.730 INFO 5884 --- [ main] cn.wmyskxz.springboot.ApplicationTests : Starting ApplicationTests on SC-201803262103 with PID 5884 (started by Administrator in E:\Java Projects\springboot)
2018-05-29 09:28:18.735 INFO 5884 --- [ main] cn.wmyskxz.springboot.ApplicationTests : No active profile set, falling back to default profiles: default
2018-05-29 09:28:18.831 INFO 5884 --- [ main] o.s.w.c.s.GenericWebApplicationContext : Refreshing org.springframework.web.context.support.GenericWebApplicationContext@7c37508a: startup date [Tue May 29 09:28:18 CST 2018]; root of context hierarchy
2018-05-29 09:28:19.200 INFO 5884 --- [ main] cn.wmyskxz.springboot.ApplicationTests : Started ApplicationTests in 1.184 seconds (JVM running for 2.413)
2018-05-29 09:28:19.798 INFO 5884 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/users/{id}],methods=[PUT]}" onto public java.lang.String cn.wmyskxz.springboot.controller.UserController.putUser(java.lang.Long,cn.wmyskxz.springboot.pojo.User)
2018-05-29 09:28:19.800 INFO 5884 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/users/],methods=[GET]}" onto public java.util.List<cn.wmyskxz.springboot.pojo.User> cn.wmyskxz.springboot.controller.UserController.getUserList()
2018-05-29 09:28:19.800 INFO 5884 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/users/],methods=[POST]}" onto public java.lang.String cn.wmyskxz.springboot.controller.UserController.postUser(cn.wmyskxz.springboot.pojo.User)
2018-05-29 09:28:19.801 INFO 5884 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/users/{id}],methods=[DELETE]}" onto public java.lang.String cn.wmyskxz.springboot.controller.UserController.deleteUser(java.lang.Long)
2018-05-29 09:28:19.801 INFO 5884 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/users/{id}],methods=[GET]}" onto public cn.wmyskxz.springboot.pojo.User cn.wmyskxz.springboot.controller.UserController.getUser(java.lang.Long)
2018-05-29 09:28:19.850 INFO 5884 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.test.web.servlet.setup.StubWebApplicationContext@42f8285e
2018-05-29 09:28:19.924 INFO 5884 --- [ main] o.s.mock.web.MockServletContext : Initializing Spring FrameworkServlet ''
2018-05-29 09:28:19.925 INFO 5884 --- [ main] o.s.t.web.servlet.TestDispatcherServlet : FrameworkServlet '': initialization started
2018-05-29 09:28:19.926 INFO 5884 --- [ main] o.s.t.web.servlet.TestDispatcherServlet : FrameworkServlet '': initialization completed in 1 ms
通過控制臺信息,我們得知通過 RESTful 風格能成功調(diào)用到正確的方法并且能獲取到或者返回正確的參數(shù)吱型,沒有任何錯誤逸贾,則說明成功!
如果你想要看到更多的細節(jié)信息津滞,可以在每次調(diào)用 perform()
方法后再跟上一句 .andDo(MockMvcResultHandlers.print())
铝侵,例如:
// 1、get查一下user列表触徐,應(yīng)該為空
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[]")))
.andDo(MockMvcResultHandlers.print());
就能看到詳細的信息咪鲜,就像下面這樣:
MockHttpServletRequest:
HTTP Method = GET
Request URI = /users/
Parameters = {}
Headers = {}
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = cn.wmyskxz.springboot.controller.UserController
Method = public java.util.List<cn.wmyskxz.springboot.pojo.User> cn.wmyskxz.springboot.controller.UserController.getUserList()
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = {Content-Type=[application/json;charset=UTF-8]}
Content type = application/json;charset=UTF-8
Body = []
Forwarded URL = null
Redirected URL = null
Cookies = []
總結(jié)
我們?nèi)匀皇褂?@RequestMapping 注解,但不同的是撞鹉,我們指定 method 屬性來處理不同的 HTTP 方法疟丙,并且通過 @PathVariable 注解來將 HTTP 請求中的屬性綁定到我們指定的形參上。
事實上鸟雏,Spring 4.3 之后享郊,為了更好的支持 RESTful 風格,增加了幾個注解:@PutMapping崔慧、@GetMapping拂蝎、@DeleteMapping穴墅、@PostMapping惶室,從名字也能大概的看出,其實也就是將 method 屬性的值與 @RequestMapping 進行了綁定而已玄货,例如皇钞,我們對UserController中的deleteUser方法進行改造:
-----------改造前-----------
@RequestMapping(value="/{id}", method=RequestMethod.DELETE)
public String deleteUser(@PathVariable Long id) {
// 處理"/users/{id}"的DELETE請求,用來刪除User
users.remove(id);
return "success";
}
-----------改造后-----------
@DeleteMapping("/{id}")
public String deleteUser(@PathVariable Long id) {
// 處理"/users/{id}"的DELETE請求松捉,用來刪除User
users.remove(id);
return "success";
}
使用Swagger2構(gòu)造RESTful API文檔
RESTful 風格為后臺與前臺的交互提供了簡潔的接口API夹界,并且有利于減少與其他團隊的溝通成本,通常情況下隘世,我們會創(chuàng)建一份RESTful API文檔來記錄所有的接口細節(jié)可柿,但是這樣做有以下的幾個問題:
- 由于接口眾多,并且細節(jié)復雜(需要考慮不同的HTTP請求類型丙者、HTTP頭部信息复斥、HTTP請求內(nèi)容等),高質(zhì)量地創(chuàng)建這份文檔本身就是件非常吃力的事械媒,下游的抱怨聲不絕于耳目锭。
- 隨著時間推移评汰,不斷修改接口實現(xiàn)的時候都必須同步修改接口文檔,而文檔與代碼又處于兩個不同的媒介痢虹,除非有嚴格的管理機制被去,不然很容易導致不一致現(xiàn)象。
Swagger2的出現(xiàn)就是為了解決上述的這些問題奖唯,并且能夠輕松的整合到我們的SpringBoot中去惨缆,它既可以減少我們創(chuàng)建文檔的工作量,同時說明內(nèi)容又可以整合到代碼之中去臭埋,讓維護文檔和修改代碼整合為一體踪央,可以讓我們在修改代碼邏輯的同時方便的修改文檔說明,這太酷了瓢阴,另外Swagger2頁提供了強大的頁面測試功能來調(diào)試每個RESTful API畅蹂,具體效果如下:
讓我們趕緊來看看吧:
第一步:添加Swagger2依賴:
在 pom.xml
中加入Swagger2的依賴:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.2.2</version>
</dependency>
第二步:創(chuàng)建Swagger2配置類
在SpringBoot啟動類的同級目錄下創(chuàng)建Swagger2的配置類 Swagger2
:
@Configuration
@EnableSwagger2
public class Swagger2 {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("cn.wmyskxz.springboot"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Spring Boot中使用Swagger2構(gòu)建RESTful APIs")
.description("原文地址鏈接:http://blog.didispace.com/springbootswagger2/")
.termsOfServiceUrl("http://blog.didispace.com/")
.contact("@我沒有三顆心臟")
.version("1.0")
.build();
}
}
如上面的代碼所示,通過 @Configuration
注解讓Spring來加載該配置類荣恐,再通過 @EnableSwagger2
注解來啟動Swagger2液斜;
再通過 createRestApi
函數(shù)創(chuàng)建 Docket
的Bean之后,apiInfo()
用來創(chuàng)建該API的基本信息(這些基本信息會展現(xiàn)在文檔頁面中)叠穆,select()
函數(shù)返回一個 ApiSelectorBuilder
實例用來控制哪些接口暴露給Swagger來展現(xiàn)少漆,本例采用指定掃描的包路徑來定義,Swagger會掃描該包下所有的Controller定義的API硼被,并產(chǎn)生文檔內(nèi)容(除了被 @ApiIgnore
指定的請求)
第三步:添加文檔內(nèi)容
在完成了上述配置后示损,其實已經(jīng)可以生產(chǎn)文檔內(nèi)容,但是這樣的文檔主要針對請求本身嚷硫,而描述主要來源于函數(shù)等命名產(chǎn)生检访,對用戶并不友好,我們通常需要自己增加一些說明來豐富文檔內(nèi)容仔掸。如下所示脆贵,我們通過@ApiOperation
注解來給API增加說明、通過@ApiImplicitParams
起暮、@ApiImplicitParam
注解來給參數(shù)增加說明卖氨。
@RestController
@RequestMapping(value="/users") // 通過這里配置使下面的映射都在/users下,可去除
public class UserController {
static Map<Long, User> users = Collections.synchronizedMap(new HashMap<Long, User>());
@ApiOperation(value="獲取用戶列表", notes="")
@RequestMapping(value={""}, method=RequestMethod.GET)
public List<User> getUserList() {
List<User> r = new ArrayList<User>(users.values());
return r;
}
@ApiOperation(value="創(chuàng)建用戶", notes="根據(jù)User對象創(chuàng)建用戶")
@ApiImplicitParam(name = "user", value = "用戶詳細實體user", required = true, dataType = "User")
@RequestMapping(value="", method=RequestMethod.POST)
public String postUser(@RequestBody User user) {
users.put(user.getId(), user);
return "success";
}
@ApiOperation(value="獲取用戶詳細信息", notes="根據(jù)url的id來獲取用戶詳細信息")
@ApiImplicitParam(name = "id", value = "用戶ID", required = true, dataType = "Long")
@RequestMapping(value="/{id}", method=RequestMethod.GET)
public User getUser(@PathVariable Long id) {
return users.get(id);
}
@ApiOperation(value="更新用戶詳細信息", notes="根據(jù)url的id來指定更新對象负懦,并根據(jù)傳過來的user信息來更新用戶詳細信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "用戶ID", required = true, dataType = "Long"),
@ApiImplicitParam(name = "user", value = "用戶詳細實體user", required = true, dataType = "User")
})
@RequestMapping(value="/{id}", method=RequestMethod.PUT)
public String putUser(@PathVariable Long id, @RequestBody User user) {
User u = users.get(id);
u.setName(user.getName());
u.setAge(user.getAge());
users.put(id, u);
return "success";
}
@ApiOperation(value="刪除用戶", notes="根據(jù)url的id來指定刪除對象")
@ApiImplicitParam(name = "id", value = "用戶ID", required = true, dataType = "Long")
@RequestMapping(value="/{id}", method=RequestMethod.DELETE)
public String deleteUser(@PathVariable Long id) {
users.remove(id);
return "success";
}
}
完成上述代碼添加之后筒捺,啟動Spring Boot程序,訪問:http://localhost:8080/swagger-ui.html纸厉,就能看到前文展示的RESTful API的頁面系吭,我們可以點開具體的API請求,POST類型的/users請求為例残腌,可找到上述代碼中我們配置的Notes信息以及參數(shù)user的描述信息村斟,如下圖所示:
API文檔訪問與調(diào)試
在上圖請求的頁面中贫导,我們可以看到一個Value的輸入框,并且在右邊的Model Schema中有示例的User對象模板蟆盹,我們點擊右邊黃色的區(qū)域Value框中就會自動填好示例的模板數(shù)據(jù)孩灯,我們可以稍微修改修改,然后點擊下方的 “Try it out!”
按鈕逾滥,即可完成一次請求調(diào)用峰档,這太酷了。
總結(jié)
對比之前用文檔來記錄RESTful API的方式寨昙,我們通過增加少量的配置內(nèi)容讥巡,在原有代碼的基礎(chǔ)上侵入了忍受范圍內(nèi)的代碼,就可以達到如此方便舔哪、直觀的效果欢顷,可以說是使用Swagger2來對API文檔進行管理,是個很不錯的選擇捉蚤!
歡迎轉(zhuǎn)載抬驴,轉(zhuǎn)載請注明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關(guān)注公眾微信號:wmyskxz_javaweb
分享自己的Java Web學習之路以及各種Java學習資料