使用RESTful風格開發Java Web

什麼是RESTful風格?

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

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

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

實例說明:

在平時的 Web 開發中,method 經常使用的值是 GET 和 POST,可是實際上,HTTP 方法還有 PATCH、DELETE、PUT 等其餘值,這些方法又一般會匹配爲以下的 CRUD 動做:git

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

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

在使用 RESTful 風格以前,咱們若是想要增長一條商品數據一般是這樣的:web

/addCategory?name=xxx

可是使用了 RESTful 風格以後就會變成:spring

/category

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

SpringBoot 中使用 RESTful

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

RESTful API 具體設計以下: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;

        // 一、get查一下user列表,應該爲空
        request = get("/users/");
        mvc.perform(request)
                .andExpect(status().isOk())
                .andExpect(content().string(equalTo("[]")));

        // 二、post提交一個user
        request = post("/users/")
                .param("id", "1")
                .param("name", "測試大師")
                .param("age", "20");
        mvc.perform(request)
                .andExpect(content().string(equalTo("success")));

        // 三、get獲取user列表,應該有剛纔插入的數據
        request = get("/users/");
        mvc.perform(request)
                .andExpect(status().isOk())
                .andExpect(content().string(equalTo("[{\"id\":1,\"name\":\"測試大師\",\"age\":20}]")));

        // 四、put修改id爲1的user
        request = put("/users/1")
                .param("name", "測試終極大師")
                .param("age", "30");
        mvc.perform(request)
                .andExpect(content().string(equalTo("success")));

        // 五、get一個id爲1的user
        request = get("/users/1");
        mvc.perform(request)
                .andExpect(content().string(equalTo("{\"id\":1,\"name\":\"測試終極大師\",\"age\":30}")));

        // 六、del刪除id爲1的user
        request = delete("/users/1");
        mvc.perform(request)
                .andExpect(content().string(equalTo("success")));

        // 七、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()) ,例如:

// 一、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文檔進行管理,是個很不錯的選擇!

歡迎轉載,轉載請註明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz 歡迎關注公衆微信號:wmyskxz_javaweb 分享本身的Java Web學習之路以及各類Java學習資料

相關文章
相關標籤/搜索