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只是重复的工作,也不会报错;

带着疑问去思考,然后串联,进而归纳总结,不断追问自己,进行自我辩证,像侦查嫌疑案件一样看待技术问题,漆黑的街道,你我一起寻找线索,你就是技术界大侦探福尔摩斯
原文地址:https://www.cnblogs.com/cainiao-Shun666/p/14651635.html