第 2-8 課:Spring Boot 構建一個 RESTful Web 服務

現在越來越多的企業推薦使用 RESTful 風格來構建企業的應用接口,那麼什麼是 RESTful 呢?

什麼是 RESTful

RESTful 是目前最流行的一種互聯網軟件架構。REST(Representational State Transfer,表述性狀態轉移)一詞是由 Roy Thomas Fielding 在他 2000 年博士論文中提出的,定義了他對互聯網軟件的架構原則,如果一個架構符合 REST 原則,則稱它爲 RESTful 架構。

RESTful 架構一個核心概念是“資源”(Resource)。從 RESTful 的角度看,網絡裏的任何東西都是資源,它可以是一段文本、一張圖片、一首歌曲、一種服務等,每個資源都對應一個特定的 URI(統一資源定位符),並用它進行標示,訪問這個 URI 就可以獲得這個資源。

資源可以有多種表現形式,也就是資源的“表述”(Representation),比如一張圖片可以使用 JPEG 格式也可以使用 PNG 格式。URI 只是代表了資源的實體,並不能代表它的表現形式。

互聯網中,客戶端和服務端之間的互動傳遞的就只是資源的表述,我們上網的過程,就是調用資源的 URI,獲取它不同表現形式的過程。這種互動只能使用無狀態協議 HTTP,也就是說,服務端必須保存所有的狀態,客戶端可以使用 HTTP 的幾個基本操作,包括 GET(獲取)、POST(創建)、PUT(更新)與 DELETE(刪除),使得服務端上的資源發生“狀態轉化”(State Transfer),也就是所謂的“表述性狀態轉移”。

Spring Boot 對 RESTful 的支持

Spring Boot 全面支持開發 RESTful 程序,通過不同的註解來支持前端的請求,除了經常使用的註解外,Spring Boot 還提了一些組合註解。這些註解來幫助簡化常用的 HTTP 方法的映射,並更好地表達被註解方法的語義。

  • @GetMapping,處理 Get 請求
  • @PostMapping,處理 Post 請求
  • @PutMapping,用於更新資源
  • @DeleteMapping,處理刪除請求
  • @PatchMapping,用於更新部分資源

其實這些組合註解就是我們使用的 @RequestMapping 的簡寫版本,下面是 Java 類中的使用示例:

@GetMapping(value="/xxx")
等價於
@RequestMapping(value = "/xxx",method = RequestMethod.GET)

@PostMapping(value="/xxx")
等價於
@RequestMapping(value = "/xxx",method = RequestMethod.POST)

@PutMapping(value="/xxx")
等價於
@RequestMapping(value = "/xxx",method = RequestMethod.PUT)

@DeleteMapping(value="/xxx")
等價於
@RequestMapping(value = "/xxx",method = RequestMethod.DELETE)

@PatchMapping(value="/xxx")
等價於
@RequestMapping(value = "/xxx",method = RequestMethod.PATCH)

通過以上可以看出 RESTful 在請求的類型中就指定了對資源的操控。

快速上手

按照 RESTful 的思想我們來設計一組對用戶操作的 RESTful API:

請求 地址 說明
get /messages 獲取所有消息
post /message 創建一個消息
put /message 修改消息內容
patch /message/text 修改消息的 text 字段
get /message/id 根據 ID 獲取消息
delete /message/id 根據 ID 刪除消息

put 方法主要是用來更新整個資源的,而 patch 方法主要表示更新部分字段。

開發實體列的操作

首先定義一個 Message 對象:

public class Message {
    private Long id;
    private String text;
    private String summary;
    // 省略 getter setter
}

我們使用 ConcurrentHashMap 來模擬存儲 Message 對象的增刪改查,AtomicLong 做爲消息的自增組建來使用。ConcurrentHashMap 是 Java 中高性能併發的 Map 接口,AtomicLong 作用是對長整形進行原子操作,可以在高並場景下獲取到唯一的 Long 值。

@Service("messageRepository")
public class InMemoryMessageRepository implements MessageRepository {

    private static AtomicLong counter = new AtomicLong();
    private final ConcurrentMap<Long, Message> messages = new ConcurrentHashMap<>();
}

查詢所有用戶,就是將 Map 中的信息全部返回。

@Override
public List<Message> findAll() {
    List<Message> messages = new ArrayList<Message>(this.messages.values());
    return messages;
}

保持消息時,需要判斷是否存在 ID,如果沒有,可以使用 AtomicLong 獲取一個。

@Override
public Message save(Message message) {
    Long id = message.getId();
    if (id == null) {
        id = counter.incrementAndGet();
        message.setId(id);
    }
    this.messages.put(id, message);
    return message;
}

更新時直接覆蓋對應的 Key:

@Override
public Message update(Message message) {
    this.messages.put(message.getId(), message);
    return message;
}

更新 text 字段:

@Override
public Message updateText(Message message) {
    Message msg=this.messages.get(message.getId());
    msg.setText(message.getText());
    this.messages.put(msg.getId(), msg);
    return msg;
}

最後封裝根據 ID 查找和刪除消息。

@Override
public Message findMessage(Long id) {
    return this.messages.get(id);
}

@Override
public void deleteMessage(Long id) {
    this.messages.remove(id);
}

封裝 RESTful 的處理

將上面封裝好的 MessageRepository 注入到 Controller 中,調用對應的增刪改查方法。

@RestController
@RequestMapping("/")
public class MessageController {

    @Autowired
    private  MessageRepository messageRepository;

    // 獲取所有消息體
    @GetMapping(value = "messages")
    public List<Message> list() {
        List<Message> messages = this.messageRepository.findAll();
        return messages;
    }

    // 創建一個消息體
    @PostMapping(value = "message")
    public Message create(Message message) {
        message = this.messageRepository.save(message);
        return message;
    }

    // 使用 put 請求進行修改
    @PutMapping(value = "message")
    public Message modify(Message message) {
        Message messageResult=this.messageRepository.update(message);
        return messageResult;
    }

    // 更新消息的 text 字段
    @PatchMapping(value="/message/text")
    public Message patch(Message message) {
        Message messageResult=this.messageRepository.updateText(message);
        return messageResult;
    }

    @GetMapping(value = "message/{id}")
    public Message get(@PathVariable Long id) {
        Message message = this.messageRepository.findMessage(id);
        return message;
    }

    @DeleteMapping(value = "message/{id}")
    public void delete(@PathVariable("id") Long id) {
        this.messageRepository.deleteMessage(id);
    }
}

進行測試

我們使用 MockMvc 進行測試。MockMvc 實現了對 Http 請求的模擬,能夠直接使用網絡的形式,轉換到 Controller 的調用,這樣可以使得測試速度快、不依賴網絡環境,而且提供了一套驗證的工具,這樣可以使得請求的驗證統一而且很方便。

下面是 MockMvc 的主體架構:

@RunWith(SpringRunner.class)
@SpringBootTest
public class MessageControllerTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }
}
  • @SpringBootTest 註解是 SpringBoot 自 1.4.0 版本開始引入的一個用於測試的註解
  • @RunWith(SpringRunner.class) 代表運行一個 Spring 容器
  • @Before 代表在測試啓動時候需要提前加載的內容,這裏是提前加載 MVC 環境

1. 測試創建消息(post 請求)

我們先來測試創建一個消息體:

@Test
public void saveMessage() throws Exception {
    final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("text", "text");
    params.add("summary", "summary");
    String mvcResult=  mockMvc.perform(MockMvcRequestBuilders.post("/message")
            .params(params)).andReturn().getResponse().getContentAsString();
    System.out.println("Result === "+mvcResult);
}
  • MultiValueMap 用來存儲需要發送的請求參數。
  • MockMvcRequestBuilders.post 代表使用 post 請求。

運行這個測試後返回結果如下:

Result === {"id":10,"text":"text","summary":"summary","created":"2018-07-28T06:27:23.176+0000"}

表明創建消息成功。

2. 批量添加消息體(post 請求)

爲了方便後面測試,需要啓動時在內存中存入一些消息來測試。

封裝一個 saveMessages() 方法批量存儲 9 條消息:

private void  saveMessages()  {
    for (int i=1;i<10;i++){
        final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("text", "text"+i);
        params.add("summary", "summary"+i);
        try {
            MvcResult mvcResult=  mockMvc.perform(MockMvcRequestBuilders.post("/message")
                    .params(params)).andReturn();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

並且將 saveMessages() 方法添加到 setup() 中,這樣啓動測試的時候內存中就已經保存了一些數據。

@Before
public void setup() {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    saveMessages();
}

3. 測試獲取所有消息(get 請求)

@Test
public void getAllMessages() throws Exception {
    String mvcResult= mockMvc.perform(MockMvcRequestBuilders.get("/messages"))
            .andReturn().getResponse().getContentAsString();
    System.out.println("Result === "+mvcResult);
}

運行後返回結果:

Result === [{"id":1,"text":"text1","summary":"summary1","created":"2018-07-28T06:34:20.583+0000"},{"id":2,"text":"text2","summary":"summary2","created":"2018-07-28T06:34:20.675+0000"},{"id":3,"text":"text3","summary":"summary3","created":"2018-07-28T06:34:20.677+0000"},{"id":4,"text":"text4","summary":"summary4","created":"2018-07-28T06:34:20.678+0000"},{"id":5,"text":"text5","summary":"summary5","created":"2018-07-28T06:34:20.680+0000"},{"id":6,"text":"text6","summary":"summary6","created":"2018-07-28T06:34:20.682+0000"},{"id":7,"text":"text7","summary":"summary7","created":"2018-07-28T06:34:20.684+0000"},{"id":8,"text":"text8","summary":"summary8","created":"2018-07-28T06:34:20.685+0000"},{"id":9,"text":"text9","summary":"summary9","created":"2018-07-28T06:34:20.687+0000"}]

可以看出初始化的數據已經保存到內存 Map 中,另一方面表明獲取數據測試成功。

4. 測試獲取單個消息(get 請求)

@Test
public void getMessage() throws Exception {
    String mvcResult= mockMvc.perform(MockMvcRequestBuilders.get("/message/6"))
            .andReturn().getResponse().getContentAsString();
    System.out.println("Result === "+mvcResult);
}

上面代碼表明獲取 ID 爲 6 的消息。

運行後返回結果:

Result === {"id":6,"text":"text6","summary":"summary6","created":"2018-07-28T06:37:26.014+0000"}

5. 測試修改(put 請求)

@Test
public void modifyMessage() throws Exception {
    final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("id", "6");
    params.add("text", "text");
    params.add("summary", "summary");
    String mvcResult= mockMvc.perform(MockMvcRequestBuilders.put("/message").params(params))
            .andReturn().getResponse().getContentAsString();
    System.out.println("Result === "+mvcResult);
}

上面代碼更新 ID 爲 6 的消息體。

運行後返回結果:

Result === {"id":6,"text":"text","summary":"summary","created":"2018-07-28T06:38:32.277+0000"}

我們發現 ID 爲 6 的消息 text 字段值由 text6 變爲 text,summary 字段值由 summary6 變爲 summary,表示消息更新成功。

6. 測試局部修改(patch 請求)

@Test
public void patchMessage() throws Exception {
    final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("id", "6");
    params.add("text", "text");
    String mvcResult= mockMvc.perform(MockMvcRequestBuilders.patch("/message/text").params(params))
            .andReturn().getResponse().getContentAsString();
    System.out.println("Result === "+mvcResult);
}

同樣是更新 ID 爲 6 的消息體,但只是更新消息屬性的一個字段。

運行後返回結果:

Result === {"id":6,"text":"text","summary":"summary6","created":"2018-07-28T06:41:51.816+0000"}

這次發現只有 text 字段值由 text6 變爲 text,summary 字段值沒有變化,表明局部更新成功。

7. 測試刪除(delete 請求)

@Test
public void deleteMessage() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.delete("/message/6"))
            .andReturn();
    String mvcResult= mockMvc.perform(MockMvcRequestBuilders.get("/messages"))
            .andReturn().getResponse().getContentAsString();
    System.out.println("Result === "+mvcResult);
}

測試刪除 ID 爲 6 的消息體,最後重新查詢所有的消息。

運行後返回結果:

Result === [{"id":1,"text":"text1","summary":"summary1","created":"2018-07-28T06:43:47.185+0000"},{"id":2,"text":"text2","summary":"summary2","created":"2018-07-28T06:43:47.459+0000"},{"id":3,"text":"text3","summary":"summary3","created":"2018-07-28T06:43:47.461+0000"},{"id":4,"text":"text4","summary":"summary4","created":"2018-07-28T06:43:47.463+0000"},{"id":5,"text":"text5","summary":"summary5","created":"2018-07-28T06:43:47.464+0000"},{"id":7,"text":"text7","summary":"summary7","created":"2018-07-28T06:43:47.468+0000"},{"id":8,"text":"text8","summary":"summary8","created":"2018-07-28T06:43:47.468+0000"},{"id":9,"text":"text9","summary":"summary9","created":"2018-07-28T06:43:47.470+0000"}]

運行後發現 ID 爲 6 的消息已經被刪除。

總結

RESTful 是一種非常優雅的設計,相同 URL 請求方式不同後端處理邏輯不同,利用 RESTful 風格很容易設計出更優雅和直觀的 API 交互接口。同時 Spring Boot 對 RESTful 的支持也做了大量的優化,方便在 Spring Boot 體系內使用 RESTful 架構。

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