Spring Boot 構建一個 RESTful Web 服務 2-9

現在越來越多的企業推薦使用 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("/me
ssage")
.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:3
4:20.583+0000"},{"id":2,"text":"text2","summary":"summary2","created":"2018-07-28T
06: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":"201
8-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","creat
ed":"2018-07-28T06:34:20.682+0000"},{"id":7,"text":"text7","summary":"summary7","c
reated":"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":"summ
ary9","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").param
s(params))
.andReturn().getResponse().getContentAsString();
System.out.println("Result === "+mvcResult);
}

上面代碼更新 ID 爲 6 的消息體。
運行後返回結果:

Result === {"id":6,"text":"text","summary":"summary","created":"2018-07-28T06:38:3
2.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:4
3:47.185+0000"},{"id":2,"text":"text2","summary":"summary2","created":"2018-07-28T
06: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":"201
8-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","creat
ed":"2018-07-28T06:43:47.468+0000"},{"id":8,"text":"text8","summary":"summary8","c
reated":"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 的支持也做了大量的優化,方便在 SpringBoot 體系內使用 RESTful 架構。

點擊這⾥下載源碼

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