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