OpenFeign(二)


Feign調試總結

feign 調試的斷點位置順序如下,可以看到請求的發送和響應情況,為什么會失敗:

  1. feign.ReflectiveFeign.FeignInvocationHandler#invoke 代理對象調用方法

  2. feign.SynchronousMethodHandler#invoke

  3. feign.SynchronousMethodHandler#executeAndDecode

  4. org.springframework.web.client.HttpMessageConverterExtractor#extractData

  5. feign.Client.Default#execute (正在執行http請求的地方)

Feign 通過看源碼得到如下知識點(接收方:服務提供方,服務端;發送方:服務調用方,客戶端)

  1. 請求參數不管是基本類型還是引用類型,一旦沒有添加@RequestParam,就會當做body處理(相當於自動添加了@RequestBody),請求方法類型也會變成post

  2. 請求內容是否支持是看接收方feign方法的聲明情況,比如接收方使用@postMapping是可以接收到最終請求頭為post的請求的,即使發送方的聲明是@GetMapping,只需要跟最終的請求頭方法類型一致就行

  3. 響應內容是否支持是看發送方feign方法的響應聲明情況的,比如,如果發送方用了注解@ResponseBody,發送方用String接收,則響應response的content type為text/plain,而發送方用對象接收,則響應的content type為application/json,也可以在發送方使用produces指定接收的響應內容content type,但是 返回的json內容是什么格式的,是由接收方的返回對象情況決定的

  4. 接收方聲明方法的對象為DeferredResult<ResponseEntity<String>> 響應的json情況和String一樣,content type是text/plain,內容格式是和String的內容一樣,但是feign得到的responseType是type類型,並不是class類型,所以會報錯,即使添加produces指定接收的響應內容content type為application/json,json解析也會報錯,因為內容格式並不是DeferredResult<ResponseEntity<String>>形式的,而是簡單的字符串,所以反序列化會失敗;

  5. 1 byte b[] = new byte[200];
    2 inputMessage.getBody().read(b); //inputMessage是PushbackInputStream類型的實例
    3 new String(b);

    通過這個可以從PushbackInputStream中獲取實際json字符串數據,調試源碼時,通過這個方法可以獲得接收方返回的json,從流中讀取查看具體的json字符串是什么內容;

  6. 接收方聲明方法返回對象為ResponseEntity<String>響應的json情況和String一樣,但是feign得到的responseType是string 的class類型,content type是text/plain,所以不會報錯,可以通過流得到string的內容,封裝成ResponseEntity<String>響應對象返回
  7. @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)。

  @PathVariable:LinkedHashMap -> indexToName(key:0代表參數位置第一個 ,value :id 參數的變量名),還會有一個indexToExpander,存放了@PathVariable注解的屬性值的信息,例如發送方和接收方feign接口如下:

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 */

 

@RequestParam :跟@PathVariable一樣,只不過indexToExpander的是@RequestParam注解的屬性信息, 例如發送方和接收方feign接口:

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 */

 

@RequestParam和@PathVariable混合使用,情況如下發送方和接收方feign接口:

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 */

 

從這三個例子可以看出發送方、接收方feign聲明必須一致,使用參數注解以及url的路徑必須一致

@RequestBody 情況如下:

 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 */

 

@RequestBody 和 @RequestParam 配合使用,例子如下:

 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請求的原因

 

下面貼出調試時request和metadata對象的屬性情況:

 

 

 

注意:feign.MethodMetadata是項目啟動,feign.ReflectiveFeign#newInstance方法創建動態代理之前的時候,feign.ReflectiveFeign.ParseHandlersByName#apply方法中解析所有的聲明方法,生成對應的metadata對象;然后

feign.ReflectiveFeign.BuildTemplateByResolvingArgs#create 這個方法通過feign.MethodMetadata中的內容用來構造feign.RequestTemplate對象,然后在feign.SynchronousMethodHandler#executeAndDecode方法中Request request = targetRequest(template)就可以獲得request對象,也就是前面提到的request對象;

8.ResponseEntity會自動將返回轉換為json格式,不需要添加@ResponseBody(可以達到一樣的效果),只會將body屬性序列化,不會將ResponseEntity整個對象序列化,再添加@ResponseBody只是重復的工作,也不會報錯;

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM