Feign調試總結
-
-
feign.SynchronousMethodHandler#invoke
-
feign.SynchronousMethodHandler#executeAndDecode
-
org.springframework.web.client.HttpMessageConverterExtractor#extractData
-
-
-
請求內容是否支持是看接收方feign方法的聲明情況,比如接收方使用@postMapping是可以接收到最終請求頭為post的請求的,即使發送方的聲明是@GetMapping,只需要跟最終的請求頭方法類型一致就行
-
響應內容是否支持是看發送方feign方法的響應聲明情況的,比如,如果發送方用了注解@ResponseBody,發送方用String接收,則響應response的content type為text/plain,而發送方用對象接收,則響應的content type為application/json,也可以在發送方使用produces指定接收的響應內容content type,但是 返回的json內容是什么格式的,是由接收方的返回對象情況決定的
-
接收方聲明方法的對象為DeferredResult<ResponseEntity<String>> 響應的json情況和String一樣,content type是text/plain,內容格式是和String的內容一樣,但是feign得到的responseType是type類型,並不是class類型,所以會報錯,即使添加produces指定接收的響應內容content type為application/json,json解析也會報錯,因為內容格式並不是DeferredResult<ResponseEntity<String>
-
1 byte b[] = new byte[200]; 2 inputMessage.getBody().read(b); //inputMessage是PushbackInputStream類型的實例 3 new String(b);
通過這個可以從PushbackInputStream中獲取實際json字符串數據,調試源碼時,通過這個方法可以獲得接收方返回的json,從流中讀取查看具體的json字符串是什么內容;
- 接收方聲明方法返回對象為ResponseEntity<String>響應的json情況和String一樣,但是feign得到的responseType是string 的class類型,content type是text/plain,所以不會報錯,可以通過流得到string的內容,封裝成ResponseEntity<String>
- @ResponseBody 只需要添加在接收方,用來返回響應內容時進行序列化為json,發送方的feign接口聲明可以不需要加@ResponseBody,發送方的方法返回值決定接受的json內容進行反序列化的對象類型。但是發送方的@GetMapping 、@RequestMapping、@PathVariable等注解必須要,用來解析構造請求內容進行發送,@GetMapping等注解的value內容決定請求的url路徑,@PathVariable、@RequestParam、@RequestBody等注解決定請求內容的格式(path參數、body參數),需要和接收方對應。通過feign.SynchronousMethodHandler#executeAndDecode中的變量metadata可以知道解析注解后的參數情況(Request request = targetRequest(template)獲得的變量request,可以知道請求的url)。
1 @GetMapping("{id}/{tt}") 2 List<UserDTO> findById(@PathVariable Integer id, @PathVariable Integer tt); 3 /** 4 這里請求兩個請求參數id,tt,那么metadata對象中的indexToName如下: 5 key:0 value: id; 6 key:1 value: tt; 7 request的url為http://user-center/users/1/100 對應的接收方控制層的url,1對應{id},100對應{tt} 8 */
1 @GetMapping("test2") 2 String test2(@RequestParam Integer id, @RequestParam Integer tt); 3 /** 4 這里請求兩個請求參數id,tt, 那么metadata對象中的indexToName如下: 5 key:0 value: id; 6 key:1 value: tt; 7 request的url為http://user-center/users/test2?id=1&tt=50 參數為queryStr 8 */
1 @GetMapping("test3/{age}") 2 String test3(@RequestParam Integer id, @PathVariable Integer age); 3 /** 4 這里請求兩個請求參數id,age, 那么metadata對象中的indexToName如下: 5 key:0 value: id; 6 key:1 value: age; 7 request的url為http://user-center/users/test3/29?id=1 包括路徑參數age和queryStr:id 8 */
1 @GetMapping("test1") 2 String test1(@RequestBody Integer id); 3 //這里@RequestBody發送方可以省略,不過不添加會默認使用當做body處理,但是接收方必須加上,如果不加請求可以接收成功,但是參數不能綁定成功; 4 /** 5 前面說的metadata都是一個feign.MethodMetadata對象實例; 6 這里請求參數id會當做body處理, 那么metadata對象中的indexToName沒有內容,bodyindex=0,bodyType是一個Type類型實例,意味着支持Type下的很多子類,class就是Type中的一個子類,這里bodyType存放的是一個Integer的class對象,因為@RequestBody修飾的參數是Integer的;indexToExpander也沒有內容; 7 8 request的url為:http://user-center/users/test1 沒有路徑參數和queryStr,但是request對象中: 9 feign.Request.Body#data的屬性不再為空,存放的就是序列化后的字節數組,通過new String(request.body.data)可以得到對應的json字符串,這里因為是簡單Integer類型,如果傳入1,得到的就是1的字符串; 10 這也就能解釋為什么@RequesetBody 只能存在一個了,因為通過序列化放入body中,通過流的形式發送出去,只能存在一份請求體,流也不可能重復讀取,所以springmvc和feign調用都只能存在一份@RequestBody; 11 */
1 @PostMapping(value = "test4") 2 ResponseEntity<UserDTO> test4(@RequestParam Integer id, User user); 3 //注意這里雖然沒用@RequestBody注解(上面提到,接收方必須加上,不然綁定user參數會失敗),但是第二個參數user沒有注解修飾,會被當做body請求體處理(跟@RequestBody效果一致),所以請求會被強制轉換為post請求,原理如下:下面會說;接收方必須為@PostMapping,發送方可以為@GetMapping 4 5 /** 6 這里存在@RequestParam 所以metadata對象中的indexToName如下: 7 key:0 value:id 8 indexToExpander存放了一個@RequestParam注解信息 9 存在請求體body類型參數user,所以bodyIndex=1,bodyType 存放的是user的class對象 10 request的url為:http://user-center/users/test4?id=10 其中id作為了queryStr, 11 feign.Request.Body#data的屬性不在為空,存放的就是序列化后的字節數組,通過new String(request.body.data)可以得到對應的json字符串,這里是user對象序列化后的結果,所以得出的字符串為:{"id":10,"wxId":null,"wxNickname":"我是一個java開發!","roles":null,"avatarUrl":null,"createTime":null,"updateTime":null,"bonus":5} 12 metadata也存放了returnType,也是Type的類型; 13 request還存放了headers,其中包含兩個,一個content-length,一個content-type 如下圖,此時httpMethod為GET,因為這個例子發送方feign接口聲明是@GetMapping,但是真正發請求時會判斷是否有body,如下: 14 */ 15 if (request.requestBody().asBytes() != null) { //request中的body不為null 16 if (contentLength != null) { 17 connection.setFixedLengthStreamingMode(contentLength); 18 } else { 19 connection.setChunkedStreamingMode(8196); 20 } 21 connection.setDoOutput(true); //這是重點,設置doOutPut為true 22 OutputStream out = connection.getOutputStream(); //這是重點,通過輸出流發送body請求體 23 if (gzipEncodedRequest) { 24 out = new GZIPOutputStream(out); 25 } else if (deflateEncodedRequest) { 26 out = new DeflaterOutputStream(out); 27 } 28 try { 29 out.write(request.requestBody().asBytes()); 30 } finally { 31 try { 32 out.close(); 33 } catch (IOException suppressed) { // NOPMD 34 } 35 } 36 } 37 //connection.getOutputStream(); 方法最終會調用sun.net.www.protocol.http.HttpURLConnection#getOutputStream0方法,該方法關鍵代碼如下: 38 if (!this.doOutput) { 39 throw new ProtocolException("cannot write to a URLConnection if doOutput=false - call setDoOutput(true)"); 40 } else { 41 if (this.method.equals("GET")) { 42 this.method = "POST"; 43 } 44 //簡單解釋上面代碼就是:doOutput為true,則方法如果為GET,則重新賦值為POST,這是sun.net.www.protocol.http.HttpURLConnection也就是jdk連接中的情況,而feign的默認連接就是采用該連接發送的請求; 所以在正在發請求的時候,會使用post方式,這也就是為什么feign只要存在請求體,不論發送方接口聲明是不是post,都會強制發送post請求的原因