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-server
和 storage-server
服务间通过 Feign 发起 HTTP 请求进行通讯。
当前订单服务 order-server
对 库存服务 storage-server
发起 GET 请求调用。
Feign 封装方法如下
上述代码中存在两种类型方法请求方式
- 方法入参
- 表单入参
上述两种方式 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)
- 问题二:不报错,但是无法传递参数<br />该问题,无论是表单入参,或者方法入参(不加 `@RequestParam` 注解)都存在<br />
<a name="zADch"></a>
### 二、解决方案
| | 报错问题 | 不报错,空传值问题 | 推荐指数 |
| --- | --- | --- | --- |
| 不做任何操作(client 不加参数注解,资源服务加了注解如@RequestParam) | 方法入参报错 | 表单入参空传值 | |
| 不做任何操作(client 和资源服务都不加参数注解) | 入参空传值 | 入参空传值 | |
| GET 改为 POST | 解决 | 解决 | ** |
| 更改 HTTP Client | 解决 | 未解决 | ** |
| Feign 追加参数注解 | 解决 | 解决 | ***** |
<a name="CNXuN"></a>
#### 2.1、将 GET 全部变更为 POST(不推荐)
> Feign 会将 GET 方法自动转为 POST 请求,并将数据放入 body 中。
- storage-server 服务资源变更<br />
- order-server feign 远程调用变更<br />
<a name="nAhKA"></a>
#### 2.2、保留 GET 请求,使用注解标识入参 防止参数被放入body中(推荐)
> 仅修改 Feign 远程调用,资源服务无需变更
- 方法入参使用:`@RequestParam`<br />
- 表单入参使用:`@SpringQueryMap`<br />
<a name="iNiuA"></a>
#### 2.3、更改 Fieng HTTP Client 组件(只能解决方法入参报错问题,为空问题无法解决)
> 修改 Feign 依赖 Http Client ,防止 GET 请求参数被放入 body 中发起请求。
> 该方式,也仅仅只能解决方法入参报错的问题,无法传递的问题仍然无法解决,需要通过 2.2 中,通过追加注解的方式,让方法参数正常传递。
<a name="VfGDG"></a>
##### step1、发起 Feign 的服务更换 Http Client(即 order-server)
- 添加依赖<br />
- 添加配置<br />
<a name="Ka9iu"></a>
##### step2、仍然需要使用 2.2 中的参数注解解决正确传值的问题。
略
<a name="GcpUi"></a>
### 三、扩展(GET 入参 被放入 body中 问题及解决 - 原理)
<a name="MtAML"></a>
#### 3.1、GET 请求无法正常访问的本质原因
<br />
这里以 client 和 服务资源都不加参数注解为例。此时Feign GET 请求,会出现空传值问题。
> 针对 client 不加参数注解,服务资源加参数注解的情况,Client 发起请求 `Content-Type=application/json;charset=UTF-8`,而服务资源不支持该接收方式,所以报错,这里不演示,感兴趣,自行测试。
GET 请求空传值问题,其本质原因是 Feign 发起 HTTP 请求时,GET 请求参数会被存放进 body ,同时 `content-type=application/json;charset=UTF-8`导致资源服务服务正确接收参数。
下面通过源码,简单分析该问题。
> 参考:[Feign Client 实例化](https://www.yuque.com/zhi-xing/spring/nl76su#847f35b4)
> [Feign 调用流程](https://www.yuque.com/zhi-xing/spring/nl76su#2979510e)
调用 Feign Client 方法,请求最终的发起和结果的收集都在 `SynchronousMethondHandler#invoke`中处理。<br />关键代码如下
```java
final class SynchronousMethodHandler implements MethodHandler {
// 省略 ......
@Override
public Object invoke(Object[] argv) throws Throwable {
// 构建 请求对象
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
// 执行请求并得到结果
return executeAndDecode(template, options);
} catch (RetryableException e) {
// 省略 ......
}
}
}
}
通过 debug 查看发起请求时构建的请求对象,debug 结果如下:
上图重要的两点
- 请求参数存放到了 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-data
或 application/x-www-form-urlencoded
3.2.1、注解参数如何解决该问题
通过在 Feign Client 请求方法上加 @RequestParam
或 @SpringQueryMap
注解能够解决问题。
首先,通过 debug 的方式,看一下加了参数注解后,Feign 构建的请求对象和原来的区别,如下图:
正确的构建了 请求参数。
参数请求的构建取决于什么?
通过端点的方式能够确定两个点
Content-Type=application/json
以及 body 的设置取决于MethodMetadata#bodyIndex
参考源码feign.ReflectiveFeign.BuildEncodedTemplateFromArgs#resolve
- 表单参数
queries
取决于MethodMetadata#queryMapIndex
参考源码:feign.ReflectiveFeign.BuildTemplateByResolvingArgs#create
而 bodyIndex
和 queryMapIndex
来自于对象实例化时的方法元数据信息。
这是就需要追踪到 Feign Client 实例化时的相关源代码。
针对 Feign Client 实例化流程,最终会进入到 ReflectiveFeign#newInstance
。
// TODO 分析源码,找到参数设置方法
关键方法:feign.Contract.BaseContract#parseAndValidateMetadata
org.springframework.cloud.openfeign.support.SpringMvcContract#processAnnotationsOnParameter