Spring Security技術棧學習筆記(二)RESTful API詳解

RESTful一種軟件架構風格、設計風格,而不是標準,只是提供了一組設計原則和約束條件。它主要用於客戶端和服務器交互類的軟件。基於這個風格設計的軟件可以更簡潔,更有層次,更易於實現緩存等機制。本篇博客主要講述使用Spring MVC開發RESTful風格的API

一、傳統API和RESTful API

傳統的APIRESTful 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及其變體(@GetMappingPostMapping等),映射HTTP請求到Java方法

  • @RequestParam映射請求參數到Java方法的參數

  • @PathVariable映射URL片段到Java方法的參數

  • @PageableDefault指定默認分頁參數

  • @JsonView按照指定方式序列化Java對象

代碼案例:這裏有UserUserController以及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));
    }
}

第三個類,也就是UserControllerTestRESTful API的測試類,現在對其進行簡單介紹:
由於RESSTful風格的API不能通過瀏覽器地址欄來進行測試,因爲地址欄發送的請求都是GET類型的,而RESTful API正是通過請求方法來判斷請求行爲是查詢、修改、刪除、增加中的哪一種的,所以測試RESSTful風格的API都是通過編碼來進行測試的。

  • 通過@Autowired WebApplicationContext webApplicationContext:注入web環境的ApplicationContext容器;

  • 然後通過MockMvcBuilders.webAppContextSetup(webApplicationContext).build()創建一個MockMvcMVC環境進行測試;

  • 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字符串,人爲使得usernamepassword兩個字段爲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是基於方法來進行區分的,所以設計到數據的修改和刪除使用的方法是PUTDELETE,接下來使用案例的方式介紹修改和刪除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)
在這裏插入圖片描述

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