解決FeignClient Get請求參數(shù)接收不到的問題
FeignClient Get請求參數(shù)接收不到的問題
場景:
在gateway攔截請求獲取token調(diào)用認證服務認證token正確性。
1.在auth-service服務端提供驗證token的服務接口,它是這個樣子的
@RestController @RequestMapping("auth") public class AuthController { @RequestMapping(value = "/info", method = RequestMethod.GET) public CommonResponse<String> auth(String token){ System.out.println(token); String s = JwtTokenUtil.parseToken(token); System.out.println(s); if("ycc".equals(s)){ return new CommonResponse<>(CommonResultEnum.SUCCESS); } return new CommonResponse<>(CommonResultEnum.FAILED_INSUFFICIENT_AUTHORITY); } }
這個接口寫起來非常簡單,但實際 springmvc 做了非常多的兼容,使得這個接口可以接受多種請求方式。
RequestMapping 代表映射的路徑,使用 GET,POST,PUT,DELETE 方式都可以映射到該端點。
SpringMVC 中常用的請求參數(shù)注解有(@RequestParam,@RequestBody,@PathVariable)等。token 被默認當做 @RequestParam。
形參 String token
由框架使用字節(jié)碼技術(shù)獲取 token 這個名稱,自動檢測請求參數(shù)中 key 值為 token 的參數(shù),也可以使用 @RequestParam(“token”) 覆蓋變量本身的名稱。
當我們在 url 中攜帶 token 參數(shù)或者 form 表單中攜帶 token 參數(shù)時,會被獲取到。
POST /hello HTTP/1.1 Host: localhost:8987 Content-Type: application/x-www-form-urlencoded token=xxxxxxxxxxx
或
GET /auth/info?token=xxxxxx HTTP/1.1 Host: localhost:8987
2.在gateway的一端需要拿到token進行Feign調(diào)用auth服務,它是這個樣子的
- AuthService
@FeignClient(name = "auth-service") public interface AuthService { @RequestMapping(value = "/auth/info", method = RequestMethod.GET) CommonResponse<String> getAuthInfo(String token); }
- TokenFilter調(diào)用處
log.info("authenticate token start..."); if(token.contains("Bearer")){ token = token.substring(token.indexOf("Bearer ")+7); } CommonResponse<String> authInfo = authService.getAuthInfo(token); if (!"200".equals(authInfo.getCode())) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); }
一切看上去都那么對稱美好,沒有瑕疵。
我們按照寫 SpringMVC 的 RestController 的習慣寫了一個 FeignClient,按照我們的一開始的想法,由于指定了請求方式是 GET,那么 token 應該會作為 QueryString 拼接到 Url 中吧?發(fā)出一個這樣的 GET 請求:
GET /auth/info?token=xxx HTTP/1.1 Host: localhost:8987
當我們啟動項目開始調(diào)用的時候,
- gateway調(diào)用處斷點
出現(xiàn)的結(jié)果是:
并沒有按照期望使用 GET 方式發(fā)送請求,而是 POST 方式.
既然這樣,那我們就讓auth-service支持POST請求;
再來調(diào)用一次;
gateway調(diào)用處斷點
出現(xiàn)的結(jié)果是:
這個時候顯示成功了,那么現(xiàn)在去auth-service查看結(jié)果;
看了個寂寞,參數(shù)并未接收到。
Feign 的請求參數(shù)綁定機制
查看文檔發(fā)現(xiàn),如果不加默認的注解,F(xiàn)eign 則會對參數(shù)默認加上 @RequestBody 注解,而 RequestBody 一定是包含在請求體中的,GET 方式無法包含。所以上述兩個現(xiàn)象得到了解釋。Feign 在 GET 請求包含 RequestBody 時強制轉(zhuǎn)成了 POST 請求,而不是報錯。
理解清楚了這個機制我們就可以在開發(fā) Feign 接口避免很多坑。而解決上述這個問題也很簡單
- 在 Feign 接口中為 token添加 @RequestParam(“token”) 注解,token必須指定,F(xiàn)eign 的請求參數(shù)不會利用 SpringMVC 字節(jié)碼的機制自動給定一個默認的名稱。
- 由于 Feign 默認使用 @RequestBody,也可以改造 RestController,使用 @RequestBody 接收。但是,請求參數(shù)通常是多個,推薦使用上述的 @RequestParam,而 @RequestBody 一般只用于傳遞對象。
Feign 綁定復合參數(shù)
指定請求參數(shù)的類型與請求方式,上述問題的出現(xiàn)實際上是由于在沒有理清楚 Feign 內(nèi)部機制的前提下想當然的和 SpringMVC 進行了類比。同樣,在使用對象作為參數(shù)時,也需要注意這樣的問題。
對于這樣的接口
@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> }
使用 @RequestBody 傳遞對象是最常用的方式。
如果參數(shù)并不是很多,可以平鋪開使用 @RequestParam
使用 Map,這也是完全可以的,但不太符合面向?qū)ο蟮乃枷?,不能從代碼立刻看出該接口需要什么樣的參數(shù)。
錯誤的用法,F(xiàn)eign 沒有提供這樣的機制自動轉(zhuǎn)換實體為 Map。
按照這個說法修改我們的接口和FeignClient
- AuthController
@RestController @RequestMapping("auth") public class AuthController { @RequestMapping(value = "/info", method = RequestMethod.GET) public CommonResponse<String> auth(String token){ System.out.println(token); String s = JwtTokenUtil.parseToken(token); System.out.println(s); if("ycc".equals(s)){ return new CommonResponse<>(CommonResultEnum.SUCCESS); } return new CommonResponse<>(CommonResultEnum.FAILED_INSUFFICIENT_AUTHORITY); } }
- AuthService
@FeignClient(name = "auth-service") public interface AuthService { @RequestMapping(value = "/auth/info", method = RequestMethod.GET) CommonResponse<String> getAuthInfo(@RequestParam("token") String token); }
再次調(diào)試:
調(diào)用成功…
Feign 中使用 @PathVariable 與 RESTFUL 規(guī)范
這涉及到一個如何設計 RESTFUL 接口的話題,我們知道在自從 RESTFUL 在 2000 年初被提出來之后,就不乏文章提到資源,契約規(guī)范,CRUD 對應增刪改查操作等等。
下面筆者從兩個實際的接口來聊聊自己的看法。
根據(jù) id 查找用戶接口:
@FeignClient("user") public interface UserApi { @RequestMapping(value = "/user/{userId}",method = RequestMethod.GET) String findById(@PathVariable("id") String userId); }
這應該是沒有爭議的,注意前面強調(diào)的,@PathVariable(“id”) 括號中的 id 不可以忘記。那如果是“根據(jù)郵箱查找用戶呢”? 很有可能下意識的寫出這樣的接口:
@FeignClient("user") public interface UserApi { @RequestMapping(value = "/user/{email}",method = RequestMethod.GET) String findByEmail(@PathVariable("email") String email); }
- 首先看看 Feign 的問題。email 中通常包含’.‘這個特殊字符,如果在路徑中包含,會出現(xiàn)意想不到的結(jié)果。我不想探討如何去解決它(實際上可以使用 {email:.+} 的方式), 因為我覺得這不符合設計。
- 再談談規(guī)范的問題。這兩個接口是否是相似的,email 是否應該被放到 path 中?這就要聊到 RESTFUL 的初衷,為什么 userId 這個屬性被普遍認為適合出現(xiàn)在 RESTFUL 路徑中,因為 id 本身起到了資源定位的作用,他是資源的標記。而 email 不同,它可能是唯一的,但更多的,它是資源的屬性,所以,筆者認為不應該在路徑中出現(xiàn)非定位性的動態(tài)參數(shù)。而是把 email 作為 @RequestParam 參數(shù)。
RESUFTL 結(jié)構(gòu)化查詢
筆者成功的從 Feign 的話題過度到了 RESTFUL 接口的設計問題,也導致了本文的篇幅變長了,不過也不打算再開一片文章談了。
再考慮一個接口設計,查詢某一個月某個用戶的訂單,可能還會攜帶分頁參數(shù),這時候參數(shù)變得很多,按照傳統(tǒng)的設計,這應該是一個查詢操作,也就是與 GET 請求對應,那是不是意味著應當將這些參數(shù)拼接到 url 后呢?再思考 Feign,正如本文的第二段所述,是不支持 GET 請求攜帶實體類的,這讓我們設計陷入了兩難的境地。而實際上參考一些 DSL 語言的設計如 elasticSearch,也是使用 POST JSON 的方式來進行查詢的,所以在實際項目中,筆者并不是特別青睞 CRUD 與四種請求方式對應的這種所謂的 RESTFUL 規(guī)范,如果說設計 RESTFUL 應該遵循什么規(guī)范,那大概是另一些名詞,如契約規(guī)范和領(lǐng)域驅(qū)動設計。
@FeignClient("order") public interface BookApi { @RequestMapping(value = "/order/history",method = RequestMethod.POST) Page<List<Orders>> queryOrderHistory(@RequestBody QueryVO queryVO); }
RESTFUL 行為限定
在實際接口設計中,我遇到了這樣的需求,用戶模塊的接口需要支持修改用戶密碼,修改用戶郵箱,修改用戶姓名,而筆者之前閱讀過一篇文章,也是講舍棄 CRUD 而是用領(lǐng)域驅(qū)動設計來規(guī)范 RESTFUL 接口的定義,與項目中我的想法不謀而合。
看似這三個屬性是同一個實體類的三個屬性,完全可以如下設計:
@FeignClient("user") public interface UserApi { @RequestMapping(value = "/user",method = RequestMethod.POST) User update(@RequestBody User user); }
但實際上,如果再考慮多一層,就應該產(chǎn)生這樣的思考:這三個功能所需要的權(quán)限一致嗎?真的應該將他們放到一個接口中嗎?
實際上,筆者并不希望接口調(diào)用方傳遞一個實體,因為這樣的行為是不可控的,完全不知道它到底是修改了什么屬性,如果真的要限制行為,還需要在 User 中添加一個操作類型的字段,然后在接口實現(xiàn)方加以校驗,這太麻煩了。而實際上,筆者覺得規(guī)范的設計應當如下:
@FeignClient("user") public interface UserApi { @RequestMapping(value = "/user/{userId}/password/update",method = RequestMethod.POST) ResultBean<Boolean> updatePassword(@PathVariable("userId) String userId,@RequestParam("password") password); @RequestMapping(value = "/user/{userId}/email/update",method = RequestMethod.POST) ResultBean<Boolean> updateEmail(@PathVariable("userId) String userId,@RequestParam("email") String email); @RequestMapping(value = "/user/{userId}/username/update",method = RequestMethod.POST) ResultBean<Boolean> updateUsername(@PathVariable("userId) String userId,@RequestParam("username") String username); }
- 一般意義上 RESTFUL 接口不應該出現(xiàn)動詞,這里的 update 并不是一個動作,而是標記著操作的類型,因為針對某個屬性可能出現(xiàn)的操作類型可能會有很多,所以我習慣加上一個 update 后綴,明確表達想要進行的操作,而不是僅僅依賴于 GET,POST,PUT,DELETE。實際上,修改操作推薦使用的請求方式應當是 PUT,這點筆者的理解是,已經(jīng)使用 update 標記了行為,實際開發(fā)中不習慣使用 PUT。
- password,email,username 都是 user 的屬性,而 userId 是 user 的識別符號,所以 userId 以 PathVariable 的形式出現(xiàn)在 url 中,而三個屬性出現(xiàn)在 ReqeustParam 中。
順帶談談邏輯刪除,如果一個需求是刪除用戶的常用地址,這個 api 的操作類型,我通常也不會設計為 DELETE 請求,而是同樣使用 delete 來標記操作行為
@RequestMapping(value = "/user/{userId}/address/{addressId}/delete",method = RequestMethod.POST) ResultBean<Boolean> updateEmail(@PathVariable("userId") String userId,@PathVariable("userId") String email);
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
java捕獲AOP級別的異常并將其傳遞到Controller層
如何在一個現(xiàn)代的Java應用中,捕獲AOP(面向切面編程)級別的異常,并將這些異常傳遞到Controller層進行合適的處理,異常處理在構(gòu)建可靠的應用程序中起著關(guān)鍵作用,而AOP則可以幫助我們更好地管理和組織代碼,我們將深入研究如何結(jié)合AOP和異常處理來構(gòu)建健壯的應用2023-09-09Java 重命名 Excel 工作表并設置工作表標簽顏色的示例代碼
這篇文章主要介紹了Java 重命名 Excel 工作表并設置工作表標簽顏色的示例代碼,代碼簡單易懂,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-10-10Spring主配置文件(applicationContext.xml) 導入約束詳解
在本篇文章里我們給各位整理的是關(guān)于Spring主配置文件(applicationContext.xml) 導入約束的相關(guān)知識點內(nèi)容,需要參考下。2019-08-08Spring Boot中捕獲異常錯誤信息并將其保存到數(shù)據(jù)庫中的操作方法
這篇文章主要介紹了Spring Boot中捕獲異常錯誤信息并將其保存到數(shù)據(jù)庫中的操作方法,通過實例代碼介紹了使用Spring Data JPA創(chuàng)建一個異常信息的存儲庫接口,以便將異常信息保存到數(shù)據(jù)庫,需要的朋友可以參考下2023-10-10