使用RESTful風格開發Java Web

 

什麼是RESTful風格?

REST是REpresentational State Transfer的縮寫(一般中文翻譯爲表述性狀態轉移),REST 是一種體系結構,而 HTTP 是一種包含了 REST 架構屬性的協議,爲了便於理解,我們把它的首字母拆分成不同的幾個部分:

  • 表述性(REpresentational): REST 資源實際上可以用各種形式來進行表述,包括 XML、JSON 甚至 HTML——最適合資源使用者的任意形式;
  • 狀態(State): 當使用 REST 的時候,我們更關注資源的狀態而不是對資源採取的行爲;
  • 轉義(Transfer): REST 涉及到轉移資源數據,它以某種表述性形式從一個應用轉移到另一個應用。

簡單地說,REST 就是將資源的狀態以適合客戶端或服務端的形式從服務端轉移到客戶端(或者反過來)。在 REST 中,資源通過 URL 進行識別和定位,然後通過行爲(即 HTTP 方法)來定義 REST 來完成怎樣的功能。

實例說明:

在平時的 Web 開發中,method 常用的值是 GET 和 POST,但是實際上,HTTP 方法還有 PATCH、DELETE、PUT 等其他值,這些方法又通常會匹配爲如下的 CRUD 動作:

CRUD 動作 HTTP 方法
Create POST
Read GET
Update PUT 或 PATCH
Delete DELETE

儘管通常來講,HTTP 方法會映射爲 CRUD 動作,但這並不是嚴格的限制,有時候 PUT 也可以用來創建新的資源,POST 也可以用來更新資源。實際上,POST 請求非冪等的特性(即同一個 URL 可以得到不同的結果)使其成一個非常靈活地方法,對於無法適應其他 HTTP 方法語義的操作,它都能夠勝任。

在使用 RESTful 風格之前,我們如果想要增加一條商品數據通常是這樣的:

/addCategory?name=xxx

但是使用了 RESTful 風格之後就會變成:

/category

這就變成了使用同一個 URL ,通過約定不同的 HTTP 方法來實施不同的業務,這就是 RESTful 風格所做的事情了,爲了有一個更加直觀的理解,引用一下來自how2j.cn的圖:

SpringBoot 中使用 RESTful

下面我使用 SpringBoot 結合文章:http://blog.didispace.com/springbootrestfulapi/ 來實例演示如何在 SpringBoot 中使用 RESTful 風格的編程並如何做單元測試

RESTful API 具體設計如下:

User實體定義:

public class User { 
 
    private Long id; 
    private String name; 
    private Integer age; 
 
    // 省略setter和getter 
     
}

實現對User對象的操作接口

@RestController 
@RequestMapping(value="/users")     // 通過這裏配置使下面的映射都在/users下 
public class UserController { 
 
    // 創建線程安全的Map 
    static Map<Long, User> users = Collections.synchronizedMap(new HashMap<Long, User>()); 
 
    @RequestMapping(value="/", method=RequestMethod.GET) 
    public List<User> getUserList() { 
        // 處理"/users/"的GET請求,用來獲取用戶列表 
        // 還可以通過@RequestParam從頁面中傳遞參數來進行查詢條件或者翻頁信息的傳遞 
        List<User> r = new ArrayList<User>(users.values()); 
        return r; 
    } 
 
    @RequestMapping(value="/", method=RequestMethod.POST) 
    public String postUser(@ModelAttribute User user) { 
        // 處理"/users/"的POST請求,用來創建User 
        // 除了@ModelAttribute綁定參數之外,還可以通過@RequestParam從頁面中傳遞參數 
        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綁定到函數的參數中 
        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列表,應該爲空
        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列表,應該有剛纔插入的數據
        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列表,應該爲空
        request = get("/users/");
        mvc.perform(request)
                .andExpect(status().isOk())
                .andExpect(content().string(equalTo("[]")));

    }

}

MockMvc實現了對HTTP請求的模擬,從示例的代碼就能夠看出MockMvc的簡單用法,它能夠直接使用網絡的形式,轉換到Controller的調用,這樣使得測試速度快、不依賴網絡環境,而且提供了一套驗證的工具,這樣可以使得請求的驗證統一而且很方便。

需要注意的就是在MockMvc使用之前需要先用MockMvcBuilders構建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 風格能成功調用到正確的方法並且能獲取到或者返回正確的參數,沒有任何錯誤,則說明成功!

如果你想要看到更多的細節信息,可以在每次調用 perform() 方法後再跟上一句 .andDo(MockMvcResultHandlers.print()) ,例如:

        // 1、get查一下user列表,應該爲空
        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 = []

總結

我們仍然使用 @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構造RESTful API文檔

參考文章:http://blog.didispace.com/springbootswagger2/

RESTful 風格爲後臺與前臺的交互提供了簡潔的接口API,並且有利於減少與其他團隊的溝通成本,通常情況下,我們會創建一份RESTful API文檔來記錄所有的接口細節,但是這樣做有以下的幾個問題:

  1. 由於接口衆多,並且細節複雜(需要考慮不同的HTTP請求類型、HTTP頭部信息、HTTP請求內容等),高質量地創建這份文檔本身就是件非常吃力的事,下游的抱怨聲不絕於耳。
  2. 隨着時間推移,不斷修改接口實現的時候都必須同步修改接口文檔,而文檔與代碼又處於兩個不同的媒介,除非有嚴格的管理機制,不然很容易導致不一致現象。

Swagger2的出現就是爲了解決上述的這些問題,並且能夠輕鬆的整合到我們的SpringBoot中去,它既可以減少我們創建文檔的工作量,同時說明內容又可以整合到代碼之中去,讓維護文檔和修改代碼整合爲一體,可以讓我們在修改代碼邏輯的同時方便的修改文檔說明,這太酷了,另外Swagger2頁提供了強大的頁面測試功能來調試每個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>

第二步:創建Swagger2配置類

在SpringBoot啓動類的同級目錄下創建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構建RESTful APIs")
                .description("原文地址鏈接:http://blog.didispace.com/springbootswagger2/")
                .termsOfServiceUrl("http://blog.didispace.com/")
                .contact("@我沒有三顆心臟")
                .version("1.0")
                .build();
    }

}

如上面的代碼所示,通過 @Configuration 註解讓Spring來加載該配置類,再通過 @EnableSwagger2 註解來啓動Swagger2;

再通過 createRestApi 函數創建 Docket 的Bean之後,apiInfo() 用來創建該API的基本信息(這些基本信息會展現在文檔頁面中),select() 函數返回一個 ApiSelectorBuilder 實例用來控制哪些接口暴露給Swagger來展現,本例採用指定掃描的包路徑來定義,Swagger會掃描該包下所有的Controller定義的API,併產生文檔內容(除了被 @ApiIgnore 指定的請求)

第三步:添加文檔內容

在完成了上述配置後,其實已經可以生產文檔內容,但是這樣的文檔主要針對請求本身,而描述主要來源於函數等命名產生,對用戶並不友好,我們通常需要自己增加一些說明來豐富文檔內容。如下所示,我們通過@ApiOperation註解來給API增加說明、通過@ApiImplicitParams@ApiImplicitParam註解來給參數增加說明。

@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="創建用戶", notes="根據User對象創建用戶")
    @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="根據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="根據url的id來指定更新對象,並根據傳過來的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="根據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信息以及參數user的描述信息,如下圖所示:

API文檔訪問與調試

在上圖請求的頁面中,我們可以看到一個Value的輸入框,並且在右邊的Model Schema中有示例的User對象模板,我們點擊右邊黃色的區域Value框中就會自動填好示例的模板數據,我們可以稍微修改修改,然後點擊下方的 “Try it out!” 按鈕,即可完成一次請求調用,這太酷了。

總結

對比之前用文檔來記錄RESTful API的方式,我們通過增加少量的配置內容,在原有代碼的基礎上侵入了忍受範圍內的代碼,就可以達到如此方便、直觀的效果,可以說是使用Swagger2來對API文檔進行管理,是個很不錯的選擇!



作者:我沒有三顆心臟
鏈接:https://www.jianshu.com/p/91600da4df95
來源:簡書

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