Spring Boot 2.1.18.RELEASE
Spring Cloud Greenwich.SR6

基于 Spring Cloud 全家桶构建微服务时,Feign 是远程调用的基础组件。

Feign 的原理就是将编码转换成 RestTemplate 对象,然后发起 HTTP 请求,从而实现服务间的调用。

关于 HTTP 请求存在多种请求方法:POST、GET、DELETE、PUT 等。

虽然在开发中会偏向于 RESTFUL 接口设计原则,但是通常来说,项目中通常只会使用 POST 和 GET ,POST 标记资源的增删改、GET 标记资源的查询。

一、场景问题

假设存在两个服务 order-serverstorage-server
服务间通过 Feign 发起 HTTP 请求进行通讯。

当前订单服务 order-server对 库存服务 storage-server 发起 GET 请求调用。

Feign 封装方法如下

image.png

上述代码中存在两种类型方法请求方式

  • 方法入参
  • 表单入参

上述两种方式 feign 的编码方式发起的 GET 请求会存在两种问题

  • 问题一:报错(当前服务资源参数是方法入参同时加了 @RequestParam 时) ``` Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback.

Wed Nov 10 19:19:53 CST 2020 There was an unexpected error (type=Internal Server Error, status=500). status 400 reading StorageApi#decrease(Long)

  1. - 问题二:不报错,但是无法传递参数<br />该问题,无论是表单入参,或者方法入参(不加 `@RequestParam` 注解)都存在<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2737632/1636534204336-40df864e-c1d4-47c8-86d3-78a9df2727c3.png#clientId=u5e1f0801-8b4b-4&from=paste&height=290&id=ue8c7e011&margin=%5Bobject%20Object%5D&name=image.png&originHeight=579&originWidth=941&originalType=binary&ratio=1&size=108595&status=done&style=none&taskId=uc7c8151e-8a87-4f16-80cc-2ee7669dd96&width=470.5)
  2. <a name="zADch"></a>
  3. ### 二、解决方案
  4. | | 报错问题 | 不报错,空传值问题 | 推荐指数 |
  5. | --- | --- | --- | --- |
  6. | 不做任何操作(client 不加参数注解,资源服务加了注解如@RequestParam | 方法入参报错 | 表单入参空传值 | |
  7. | 不做任何操作(client 和资源服务都不加参数注解) | 入参空传值 | 入参空传值 | |
  8. | GET 改为 POST | 解决 | 解决 | ** |
  9. | 更改 HTTP Client | 解决 | 未解决 | ** |
  10. | Feign 追加参数注解 | 解决 | 解决 | ***** |
  11. <a name="CNXuN"></a>
  12. #### 2.1、将 GET 全部变更为 POST(不推荐)
  13. > Feign 会将 GET 方法自动转为 POST 请求,并将数据放入 body 中。
  14. - storage-server 服务资源变更<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2737632/1636533230516-93edf139-df1a-4326-a08d-7c2dc6a53b24.png#clientId=u5e1f0801-8b4b-4&from=paste&height=279&id=u124ea9b5&margin=%5Bobject%20Object%5D&name=image.png&originHeight=558&originWidth=832&originalType=binary&ratio=1&size=73357&status=done&style=none&taskId=u26fd7eb7-83de-419f-994e-468fe05d3fa&width=416)
  15. - order-server feign 远程调用变更<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2737632/1636533292173-251e81d1-9abc-44d9-86e4-ddabe1a21065.png#clientId=u5e1f0801-8b4b-4&from=paste&height=263&id=uebcafeef&margin=%5Bobject%20Object%5D&name=image.png&originHeight=526&originWidth=606&originalType=binary&ratio=1&size=59217&status=done&style=none&taskId=uac791168-ef67-44b7-8c44-fe4a329afde&width=303)
  16. <a name="nAhKA"></a>
  17. #### 2.2、保留 GET 请求,使用注解标识入参 防止参数被放入body中(推荐)
  18. > 仅修改 Feign 远程调用,资源服务无需变更
  19. - 方法入参使用:`@RequestParam`<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2737632/1636536609487-a010952a-fed7-409c-8d7f-4c3fc355bf27.png#clientId=u4e7014a1-dc3a-4&from=paste&height=217&id=uf2398814&margin=%5Bobject%20Object%5D&name=image.png&originHeight=217&originWidth=429&originalType=binary&ratio=1&size=23284&status=done&style=none&taskId=u8050fb15-15eb-4878-beaa-bbaa8e5ded9&width=429)
  20. - 表单入参使用:`@SpringQueryMap`<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2737632/1636536655622-0b583233-2ccb-4958-b444-6ed98fb9fc87.png#clientId=u4e7014a1-dc3a-4&from=paste&height=217&id=uf8684598&margin=%5Bobject%20Object%5D&name=image.png&originHeight=217&originWidth=545&originalType=binary&ratio=1&size=25773&status=done&style=none&taskId=u242618ec-9f82-4bb2-885c-9c02e43559b&width=545)
  21. <a name="iNiuA"></a>
  22. #### 2.3、更改 Fieng HTTP Client 组件(只能解决方法入参报错问题,为空问题无法解决)
  23. > 修改 Feign 依赖 Http Client ,防止 GET 请求参数被放入 body 中发起请求。
  24. > 该方式,也仅仅只能解决方法入参报错的问题,无法传递的问题仍然无法解决,需要通过 2.2 中,通过追加注解的方式,让方法参数正常传递。
  25. <a name="VfGDG"></a>
  26. ##### step1、发起 Feign 的服务更换 Http Client(即 order-server)
  27. - 添加依赖<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2737632/1636536822418-728bda1e-8424-49cb-be72-b6cd61688aa2.png#clientId=u4e7014a1-dc3a-4&from=paste&height=218&id=u727c8fac&margin=%5Bobject%20Object%5D&name=image.png&originHeight=292&originWidth=497&originalType=binary&ratio=1&size=30854&status=done&style=none&taskId=u692efdf9-4066-4204-b1be-3f3578afb15&width=371)
  28. - 添加配置<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2737632/1636536849621-9dde06a1-8dd7-4796-8822-05bb1e084e93.png#clientId=u4e7014a1-dc3a-4&from=paste&height=87&id=u4cf28f6b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=87&originWidth=190&originalType=binary&ratio=1&size=3651&status=done&style=none&taskId=u317995b8-e573-49b7-b62e-f8243fd902d&width=190)
  29. <a name="Ka9iu"></a>
  30. ##### step2、仍然需要使用 2.2 中的参数注解解决正确传值的问题。
  31. <a name="GcpUi"></a>
  32. ### 三、扩展(GET 入参 被放入 body中 问题及解决 - 原理)
  33. <a name="MtAML"></a>
  34. #### 3.1、GET 请求无法正常访问的本质原因
  35. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2737632/1636537935533-c521c4e8-54be-4490-a830-c9fbe8b279bf.png#clientId=u4e7014a1-dc3a-4&from=paste&height=307&id=u4648805d&margin=%5Bobject%20Object%5D&name=image.png&originHeight=453&originWidth=554&originalType=binary&ratio=1&size=48752&status=done&style=none&taskId=u010e69c0-fb2b-477b-88ef-4078965821a&width=375)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2737632/1636594667929-a6959492-81e7-49e9-8a6c-5369759ebef7.png#clientId=u4e7014a1-dc3a-4&from=paste&height=232&id=ua08ee3d8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=308&originWidth=739&originalType=binary&ratio=1&size=54332&status=done&style=none&taskId=ucb82a9a2-4b08-4931-8bc1-18b345ef9c2&width=556)
  36. 这里以 client 服务资源都不加参数注解为例。此时Feign GET 请求,会出现空传值问题。
  37. > 针对 client 不加参数注解,服务资源加参数注解的情况,Client 发起请求 `Content-Type=application/json;charset=UTF-8`,而服务资源不支持该接收方式,所以报错,这里不演示,感兴趣,自行测试。
  38. GET 请求空传值问题,其本质原因是 Feign 发起 HTTP 请求时,GET 请求参数会被存放进 body ,同时 `content-type=application/json;charset=UTF-8`导致资源服务服务正确接收参数。
  39. 下面通过源码,简单分析该问题。
  40. > 参考:[Feign Client 实例化](https://www.yuque.com/zhi-xing/spring/nl76su#847f35b4)
  41. > [Feign 调用流程](https://www.yuque.com/zhi-xing/spring/nl76su#2979510e)
  42. 调用 Feign Client 方法,请求最终的发起和结果的收集都在 `SynchronousMethondHandler#invoke`中处理。<br />关键代码如下
  43. ```java
  44. final class SynchronousMethodHandler implements MethodHandler {
  45. // 省略 ......
  46. @Override
  47. public Object invoke(Object[] argv) throws Throwable {
  48. // 构建 请求对象
  49. RequestTemplate template = buildTemplateFromArgs.create(argv);
  50. Options options = findOptions(argv);
  51. Retryer retryer = this.retryer.clone();
  52. while (true) {
  53. try {
  54. // 执行请求并得到结果
  55. return executeAndDecode(template, options);
  56. } catch (RetryableException e) {
  57. // 省略 ......
  58. }
  59. }
  60. }
  61. }

通过 debug 查看发起请求时构建的请求对象,debug 结果如下:

image.png

上图重要的两点

  • 请求参数存放到了 body 中
  • Content-Type=application/json;charset=UTF-8

为什么上述两点会导致服务资源无法正确接收参数导致空值问题?

服务资源请求的处理是 Spring MVC 处理,在 Spring MVC 入参解析器 中提到,针对每种入参类型,Spring MVC 都提供了对应的入参解析器来讲请求参数绑定到资源方法参数中。
上述案例中,Feign Client 发起的GET 请求参数都放到了 body 中,同时 Content-Type=application/json;charset=UTF-8
针对 json 格式的入参(不同的 jason 框架实现有细微差别,但本质一样,这里以 jackson 为例),由 RequestResponseBodyMethodProcessor解析器进行处理。

而上述案例的服务资源为方法入参,默认的解析器为:ServletModelAttributeMethodProcessor

“请求” 和“处理” 不匹配所以无法正确的进行传值解析,从而导致空传值的问题。

3.2、解决 GET 传值问题

网络上有说 GET 被强制转换 POST 请求,未找到入口,后期验证。

无论“报错”还是”空传值“问题。解决方案就是保证发起 GET 请求满足

  • 不将请求参数放入 body 中
  • 不设置 Content-Type=application/json;charset=UTF-8

上述两个条件不是 &&的关系。

例如:参数可以放到 body 中,但是 Content-Type需要为 multipart/form-dataapplication/x-www-form-urlencoded

3.2.1、注解参数如何解决该问题

image.png
通过在 Feign Client 请求方法上加 @RequestParam@SpringQueryMap注解能够解决问题。

首先,通过 debug 的方式,看一下加了参数注解后,Feign 构建的请求对象和原来的区别,如下图:

image.png

正确的构建了 请求参数。

参数请求的构建取决于什么?

通过端点的方式能够确定两个点

  • Content-Type=application/json以及 body 的设置取决于 MethodMetadata#bodyIndex
    参考源码 feign.ReflectiveFeign.BuildEncodedTemplateFromArgs#resolve
    image.png
  • 表单参数 queries取决于 MethodMetadata#queryMapIndex
    参考源码:feign.ReflectiveFeign.BuildTemplateByResolvingArgs#create
    image.png

bodyIndexqueryMapIndex来自于对象实例化时的方法元数据信息。
这是就需要追踪到 Feign Client 实例化时的相关源代码。

针对 Feign Client 实例化流程,最终会进入到 ReflectiveFeign#newInstance

// TODO 分析源码,找到参数设置方法

关键方法:feign.Contract.BaseContract#parseAndValidateMetadata
org.springframework.cloud.openfeign.support.SpringMvcContract#processAnnotationsOnParameter
image.png