從Feign使用注意點到RESTFUL接口設計規範

最近項目中大量使用了Spring Cloud Feign來對接http接口,踩了不少坑,也產生了一些對RESTFUL接口設計的想法,特此一篇記錄下。

SpringMVC的請求參數綁定機制

瞭解Feign歷史的朋友會知道,Feign本身是Netflix的產品,Spring Cloud Feign是在原生Feign的基礎上進行了封裝,引入了大量的SpringMVC註解支持,這一方面使得其更容易被廣大的Spring使用者開箱即用,但也產生了不小的混淆作用。所以在使用Spring Cloud Feign之前,筆者先介紹一下SpringMVC的一個入參機制。預設一個RestController,在本地的8080端口啓動一個應用,用於接收http請求。

1
2
3
4
5
6
7
8
9
@RestController
public class BookController {

    @RequestMapping(value = "/hello") // <1>
    public String hello(String name) { // <2>
        return "hello " + name;
    }

}

這個接口寫起來非常簡單,但實際springmvc做了非常多的兼容,使得這個接口可以接受多種請求方式。

<1> RequestMapping代表映射的路徑,使用GET,POST,PUT,DELETE方式都可以映射到該端點。

<2> SpringMVC中常用的請求參數註解有(@RequestParam,@RequestBody,@PathVariable)等。name被默認當做@RequestParam。形參String name由框架使用字節碼技術獲取name這個名稱,自動檢測請求參數中key值爲name的參數,也可以使用@RequestParam(“name”)覆蓋變量本身的名稱。當我們在url中攜帶name參數或者form表單中攜帶name參數時,會被獲取到。

1
2
3
4
5
POST /hello HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded

name=formParam

1
2
GET /hello?name=queryString HTTP/1.1
Host: localhost:8080

Feign的請求參數綁定機制

上述的SpringMVC參數綁定機制,大家應該都是非常熟悉的,但這一切在Feign中有些許的不同。

我們來看一個非常簡單的,但是實際上錯誤的接口寫法:

1
2
3
4
5
6
7
8
//注意:錯誤的接口寫法
@FeignClient("book")
public interface BookApi {

    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    String hello(String name);

}

配置請求地址:

1
2
3
4
5
6
7
ribbon:
  eureka:
   enabled: false

book:
  ribbon:
    listOfServers: http://localhost:8080

我們按照寫SpringMVC的RestController的習慣寫了一個FeignClient,按照我們的一開始的想法,由於指定了請求方式是GET,那麼name應該會作爲QueryString拼接到Url中吧?發出一個這樣的GET請求:

1
2
GET /hello?name=xxx HTTP/1.1
Host: localhost:8080

而實際上,RestController並沒有接收到,我們在RestController一側的應用中獲得了一些提示:

服務端DEBUG信息服務端DEBUG信息

  • 並沒有按照期望使用GET方式發送請求,而是POST方式
  • name參數沒有被封裝,獲得了一個null值

查看文檔發現,如果不加默認的註解,Feign則會對參數默認加上@RequestBody註解,而RequestBody一定是包含在請求體中的,GET方式無法包含。所以上述兩個現象得到了解釋。Feign在GET請求包含RequestBody時強制轉成了POST請求,而不是報錯。

理解清楚了這個機制我們就可以在開發Feign接口避免很多坑。而解決上述這個問題也很簡單

  • 在Feign接口中爲name添加@RequestParam(“name”)註解,name必須指定,Feign的請求參數不會利用SpringMVC字節碼的機制自動給定一個默認的名稱。
  • 由於Feign默認使用@RequestBody,也可以改造RestController,使用@RequestBody接收。但是,請求參數通常是多個,推薦使用上述的@RequestParam,而@RequestBody一般只用於傳遞對象。

Feign綁定複合參數

指定請求參數的類型與請求方式,上述問題的出現實際上是由於在沒有理清楚Feign內部機制的前提下想當然的和SpringMVC進行了類比。同樣,在使用對象作爲參數時,也需要注意這樣的問題。

對於這樣的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@FeignClient("book")
public interface BookApi {

    @RequestMapping(value = "/book",method = RequestMethod.POST)
    Book book(@RequestBody Book book); // <1>
  
    @RequestMapping(value = "/book",method = RequestMethod.POST)
    Book book(@RequestParam("id") String id,@RequestParam("name") String name); // <2>
  
    @RequestMapping(value = "/book",method = RequestMethod.POST)
    Book book(@RequestParam Map map); // <3>
  
    //錯誤的寫法
  	@RequestMapping(value = "/book",method = RequestMethod.POST)
    Book book(@RequestParam Book book); // <4>

}

<1> 使用@RequestBody傳遞對象是最常用的方式。

<2> 如果參數並不是很多,可以平鋪開使用@RequestParam

<3> 使用Map,這也是完全可以的,但不太符合面向對象的思想,不能從代碼立刻看出該接口需要什麼樣的參數。

<4> 錯誤的用法,Feign沒有提供這樣的機制自動轉換實體爲Map。

Feign中使用@PathVariable與RESTFUL規範

這涉及到一個如何設計RESTFUL接口的話題,我們知道在自從RESTFUL在2000年初被提出來之後,就不乏文章提到資源,契約規範,CRUD對應增刪改查操作等等。下面筆者從兩個實際的接口來聊聊自己的看法。

根據id查找用戶接口:

1
2
3
4
5
6
7
@FeignClient("user")
public interface UserApi {

    @RequestMapping(value = "/user/{userId}",method = RequestMethod.GET)
    String findById(@PathVariable("id") String userId);

}

這應該是沒有爭議的,注意前面強調的,@PathVariable(“id”)括號中的id不可以忘記。那如果是“根據郵箱查找用戶呢”?很有可能下意識的寫出這樣的接口:

1
2
3
4
5
6
7
@FeignClient("user")
public interface UserApi {
  
    @RequestMapping(value = "/user/{email}",method = RequestMethod.GET)
    String findByEmail(@PathVariable("email") String email);

}
  • 首先看看Feign的問題。email中通常包含’.‘這個特殊字符,如果在路徑中包含,會出現意想不到的結果。我不想探討如何去解決它(實際上可以使用{email:.+}的方式),因爲我覺得這不符合設計。
  • 再談談規範的問題。這兩個接口是否是相似的,email是否應該被放到path中?這就要聊到RESTFUL的初衷,爲什麼userId這個屬性被普遍認爲適合出現在RESTFUL路徑中,因爲id本身起到了資源定位的作用,他是資源的標記。而email不同,它可能是唯一的,但更多的,它是資源的屬性,所以,筆者認爲不應該在路徑中出現非定位性的動態參數。而是把email作爲@RequestParam參數。

RESUFTL結構化查詢

筆者成功的從Feign的話題過度到了RESTFUL接口的設計問題,也導致了本文的篇幅變長了,不過也不打算再開一片文章談了。

再考慮一個接口設計,查詢某一個月某個用戶的訂單,可能還會攜帶分頁參數,這時候參數變得很多,按照傳統的設計,這應該是一個查詢操作,也就是與GET請求對應,那是不是意味着應當將這些參數拼接到url後呢?再思考Feign,正如本文的第二段所述,是不支持GET請求攜帶實體類的,這讓我們設計陷入了兩難的境地。而實際上參考一些DSL語言的設計如elasticSearch,也是使用POST JSON的方式來進行查詢的,所以在實際項目中,筆者並不是特別青睞CRUD與四種請求方式對應的這種所謂的RESTFUL規範,如果說設計RESTFUL應該遵循什麼規範,那大概是另一些名詞,如契約規範和領域驅動設計。

1
2
3
4
5
6
7
@FeignClient("order")
public interface BookApi {

    @RequestMapping(value = "/order/history",method = RequestMethod.POST)
    Page<List<Orders>> queryOrderHistory(@RequestBody QueryVO queryVO);

}

RESTFUL行爲限定

在實際接口設計中,我遇到了這樣的需求,用戶模塊的接口需要支持修改用戶密碼,修改用戶郵箱,修改用戶姓名,而筆者之前閱讀過一篇文章,也是講捨棄CRUD而是用領域驅動設計來規範RESTFUL接口的定義,與項目中我的想法不謀而合。看似這三個屬性是同一個實體類的三個屬性,完全可以如下設計:

1
2
3
4
5
6
7
@FeignClient("user")
public interface UserApi {

    @RequestMapping(value = "/user",method = RequestMethod.PUT)
    User update(@RequestBody User user);

}

但實際上,如果再考慮多一層,就應該產生這樣的思考:這三個功能所需要的權限一致嗎?真的應該將他們放到一個接口中嗎?實際上,筆者並不希望接口調用方傳遞一個實體,因爲這樣的行爲是不可控的,完全不知道它到底是修改了什麼屬性,如果真的要限制行爲,還需要在User中添加一個操作類型的字段,然後在接口實現方加以校驗,這太麻煩了。而實際上,筆者覺得規範的設計應當如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@FeignClient("user")
public interface UserApi {

    @RequestMapping(value = "/user/{userId}/password",method = RequestMethod.PUT)
    ResultBean<Boolean> updatePassword(@PathVariable("userId) String userId,@RequestParam("password") password);
    
    @RequestMapping(value = "/user/{userId}/email",method = RequestMethod.PUT)
    ResultBean<Boolean> updateEmail(@PathVariable("userId) String userId,@RequestParam("email") String email);
    
    @RequestMapping(value = "/user/{userId}/username",method = RequestMethod.PUT)
    ResultBean<Boolean> updateUsername(@PathVariable("userId) String userId,@RequestParam("username") String username);

}
  • 一般意義上RESTFUL接口不應該出現動詞。修改操作推薦使用的請求方式應當是PUT
  • password,email,username都是user的屬性,而userId是user的識別符號,所以userId以PathVariable的形式出現在url中,而三個屬性出現在ReqeustParam中。

順帶談談邏輯刪除

1
2
@RequestMapping(value = "/user/{userId}",method = RequestMethod.DELETE)
ResultBean<Boolean> updateEmail(@PathVariable("userId") String userId);

總結

本文從Feign的使用注意點,聊到了RESTFUL接口的設計問題,其實是一個互相補充的行爲。接口設計需要載體,所以我以Feign的接口風格談了談自己對RESTFUL設計的理解,而Feign中一些坑點,也正是我想要規範RESTFUL設計的出發點。如有對RESTFUL設計不同的理解,歡迎與我溝通。

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