前言

本文没有详细介绍 FeignClient 的知识点,网上有很多优秀的文章介绍了 FeignCient 的知识点,在这里本人就不重复了,只是专注在这个问题点上。

查询参数丢失场景

业务描述: 业务系统需要更新用户系统中的A资源,由于只想更新A资源的一个字段信息为B,所以没有选择通过 entity 封装B,而是直接通过查询参数来传递B信息

文字描述:使用FeignClient来进行远程调用时,如果POST请求中有查询参数并且没有请求实体(body为空),那么查询参数被丢失,服务提供者获取不到查询参数的值。

代码描述:B的值被丢失,服务提供者获取不到B的值


  1. @FeignClient(name = "a-service", configuration = FeignConfiguration.class)
  2. public interface ACall {
  3. @RequestMapping(method = RequestMethod.POST, value = "/api/xxx/{A}", headers = {"Content-Type=application/json"})
  4. void updateAToB(@PathVariable("A") final String A, @RequestParam("B") final String B) throws Exception;
  5. }

问题分析

背景

  1. 使用 FeignClient 客户端
  2. 使用 feign-httpclient 中的 ApacheHttpClient 来进行实际请求的调用

  1. <dependency>
  2. <groupId>com.netflix.feign</groupId>
  3. <artifactId>feign-httpclient</artifactId>
  4. <version>8.18.0</version>
  5. </dependency>

直入源码

通过对 FeignClient 的源码阅读,发现问题不是出在参数解析上,而是在使用 ApacheHttpClient 进行请求时,其将查询参数放进请求body中了,下面看源码具体是如何处理的
feign.httpclient.ApacheHttpClient 这是 feign-httpclient 进行实际请求的方法


  1. @Override
  2. public Response execute(Request request, Request.Options options) throws IOException {
  3. HttpUriRequest httpUriRequest;
  4. try {
  5. httpUriRequest = toHttpUriRequest(request, options);
  6. } catch (URISyntaxException e) {
  7. throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e);
  8. }
  9. HttpResponse httpResponse = client.execute(httpUriRequest);
  10. return toFeignResponse(httpResponse);
  11. }
  12. HttpUriRequest toHttpUriRequest(Request request, Request.Options options) throws
  13. UnsupportedEncodingException, MalformedURLException, URISyntaxException {
  14. RequestBuilder requestBuilder = RequestBuilder.create(request.method());
  15. //per request timeouts
  16. RequestConfig requestConfig = RequestConfig
  17. .custom()
  18. .setConnectTimeout(options.connectTimeoutMillis())
  19. .setSocketTimeout(options.readTimeoutMillis())
  20. .build();
  21. requestBuilder.setConfig(requestConfig);
  22. URI uri = new URIBuilder(request.url()).build();
  23. requestBuilder.setUri(uri.getScheme() + "://" + uri.getAuthority() + uri.getRawPath());
  24. //request query params
  25. List<NameValuePair> queryParams = URLEncodedUtils.parse(uri, requestBuilder.getCharset().name());
  26. for (NameValuePair queryParam: queryParams) {
  27. requestBuilder.addParameter(queryParam);
  28. }
  29. //request headers
  30. boolean hasAcceptHeader = false;
  31. for (Map.Entry<String, Collection<String>> headerEntry : request.headers().entrySet()) {
  32. String headerName = headerEntry.getKey();
  33. if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) {
  34. hasAcceptHeader = true;
  35. }
  36. if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH)) {
  37. // The 'Content-Length' header is always set by the Apache client and it
  38. // doesn't like us to set it as well.
  39. continue;
  40. }
  41. for (String headerValue : headerEntry.getValue()) {
  42. requestBuilder.addHeader(headerName, headerValue);
  43. }
  44. }
  45. //some servers choke on the default accept string, so we'll set it to anything
  46. if (!hasAcceptHeader) {
  47. requestBuilder.addHeader(ACCEPT_HEADER_NAME, "*/*");
  48. }
  49. //request body
  50. if (request.body() != null) {
  51. //body为空,则HttpEntity为空
  52. HttpEntity entity = null;
  53. if (request.charset() != null) {
  54. ContentType contentType = getContentType(request);
  55. String content = new String(request.body(), request.charset());
  56. entity = new StringEntity(content, contentType);
  57. } else {
  58. entity = new ByteArrayEntity(request.body());
  59. }
  60. requestBuilder.setEntity(entity);
  61. }
  62. //调用org.apache.http.client.methods.RequestBuilder#build方法
  63. return requestBuilder.build();
  64. }

org.apache.http.client.methods.RequestBuilder 此类是 HttpUriRequest 的Builder类,下面看build方法


  1. public HttpUriRequest build() {
  2. final HttpRequestBase result;
  3. URI uriNotNull = this.uri != null ? this.uri : URI.create("/");
  4. HttpEntity entityCopy = this.entity;
  5. if (parameters != null && !parameters.isEmpty()) {
  6. // 这里:如果HttpEntity为空,并且为POST请求或者为PUT请求时,这个方法会将查询参数取出来封装成了HttpEntity
  7. // 就是在这里查询参数被丢弃了,准确的说是被转换位置了
  8. if (entityCopy == null && (HttpPost.METHOD_NAME.equalsIgnoreCase(method)
  9. || HttpPut.METHOD_NAME.equalsIgnoreCase(method))) {
  10. entityCopy = new UrlEncodedFormEntity(parameters, charset != null ? charset : HTTP.DEF_CONTENT_CHARSET);
  11. } else {
  12. try {
  13. uriNotNull = new URIBuilder(uriNotNull)
  14. .setCharset(this.charset)
  15. .addParameters(parameters)
  16. .build();
  17. } catch (final URISyntaxException ex) {
  18. // should never happen
  19. }
  20. }
  21. }
  22. if (entityCopy == null) {
  23. result = new InternalRequest(method);
  24. } else {
  25. final InternalEntityEclosingRequest request = new InternalEntityEclosingRequest(method);
  26. request.setEntity(entityCopy);
  27. result = request;
  28. }
  29. result.setProtocolVersion(this.version);
  30. result.setURI(uriNotNull);
  31. if (this.headergroup != null) {
  32. result.setHeaders(this.headergroup.getAllHeaders());
  33. }
  34. result.setConfig(this.config);
  35. return result;
  36. }

解决方案

既然已经知道原因了,那么解决方法就有很多种了,下面就介绍常规的解决方案:

  1. 使用 feign-okhttp 来进行请求调用,这里就不列源码了,感兴趣大家可以去看, feign-okhttp 底层没有判断如果body为空则把查询参数放入body中。
  2. 使用 io.github.openfeign:feign-httpclient:9.5.1 依赖,截取部分源码说明原因如下:

  1. HttpUriRequest toHttpUriRequest(Request request, Request.Options options) throws
  2. UnsupportedEncodingException, MalformedURLException, URISyntaxException {
  3. RequestBuilder requestBuilder = RequestBuilder.create(request.method());
  4. //省略部分代码
  5. //request body
  6. if (request.body() != null) {
  7. //省略部分代码
  8. } else {
  9. // 此处,如果为null,则会塞入一个byte数组为0的对象
  10. requestBuilder.setEntity(new ByteArrayEntity(new byte[0]));
  11. }
  12. return requestBuilder.build();
  13. }

推荐的依赖


  1. <dependency>
  2. <groupId>io.github.openfeign</groupId>
  3. <artifactId>feign-httpclient</artifactId>
  4. <version>9.5.1</version>
  5. </dependency>

或者


  1. <dependency>
  2. <groupId>io.github.openfeign</groupId>
  3. <artifactId>feign-okhttp</artifactId>
  4. <version>9.5.1</version>
  5. </dependency>

总结

目前绝大部分的介绍 feign 的文章(本人所看到的,包括本人之前写的一篇文章也是)中都是推荐的 com.netflix.feign:feign-httpclient:8.18.0com.netflix.feign:feign-okhttp:8.18.0 ,如果不巧你使用了 com.netflix.feign:feign-httpclient:8.18.0,那么在POST请求时并且body为空时就会发生丢失查询参数的问题。

这里推荐大家使用 feign-httpclient 或者是 feign-okhttp的时候不要依赖 com.netflix.feign,而应该选择 io.github.openfeign,因为看起来 Netflix 很久没有对这两个组件进行维护了,而是由 OpenFeign 来进行维护了。

参考资料:

作者:vincent_ren

链接:https://www.jianshu.com/p/7cfa4250d5ab

來源:简书

简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

FeignClient调用POST请求时查询参数被丢失的情况分析与处理的更多相关文章

  1. 为什么返回的数据前面有callback? ashx/json.ashx?的后面加 callback=? 起什么作用 js url?callback=xxx xxx的介绍 ajax 跨域请求时url参数添加callback=?会实现跨域问题

    为什么返回的数据前面有callback?   这是一个同学出现的问题,问到了我. 应该是这样的: 但问题是这样的: 我看了所请求的格式和后台要求的也是相同的.而且我也是这种做法,为什么他的就不行呢? ...

  2. 网络请求 get 请求时, 如果参数中的字符带有+号

    网络请求 get 请求时, 如果参数中的字符带有+号, 今天前端在调用我的API时, 发现有个参数一直没法通过我后台的验证, 但是在前端查看时, 该参数结构又没有什么异常, 又是一番查找, 直到在后端 ...

  3. feignclient发送get请求,传递参数为对象

    feignclient发送get请求,传递参数为对象.此时不能使用在地址栏传递参数的方式,需要将参数放到请求体中. 第一步: 修改application.yml中配置feign发送请求使用apache ...

  4. 解决爬虫浏览器中General显示 Status Code:304 NOT MODIFIED,而在requests请求时出现403被拦截的情况。

    在此,非常感谢 “完美风暴4” 的无私共享经验的精神    在Python爬虫爬取网站时,莫名遇到 浏览器中General显示  Status Code: 304 NOT MODIFIED 而在req ...

  5. url请求时,参数中的+在服务器接收时为空格,导致AES加密报出javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher

    报错的意思的是使用该种解密方式出入长度应为16bit的倍数,但实际的错误却不是这个,错误原因根本上是因为在http请求是特殊字符编码错误,具体就是base64生成的+号,服务器接收时成了空格,然后导致 ...

  6. spring boot:用cookie保存i18n信息避免每次请求时传递参数(spring boot 2.3.3)

    一,用cookie保存i18n信息的优点? 当开发一个web项目(非api站)时,如果把i18n的选择信息保存到cookie, 则不需要在每次发送请求时都传递所选择语言的参数, 也不需要增加heade ...

  7. ajax 跨域请求时url参数添加callback=?会实现跨域问题

    例如: 1.在 jQuery 中,可以通过使用JSONP 形式的回调函数来加载其他网域的JSON数据,如 "myurl?callback=?".jQuery 将自动替换 ? 为正确 ...

  8. ASP.NET MVC API与JS进行POST请求时传递参数 -CHPowerljp原创

    在API前添加    [HttpPost] 表示只允许POST方式请求 [HttpPost] public IHttpActionResult Get_BIGDATA([FromBody]Datas  ...

  9. angularjs中ajax请求时传递参数的方法

    method1方法使用的是params参数,该用法会把参数直接附加到url中 method2方法使用的是data参数,该参数会把页面参数类型从默认的multipart/form-data改为appli ...

随机推荐

  1. MySQL中 如何查询表名中包含某字段的表

    查询tablename 数据库中 以"_copy" 结尾的表 select table_name from information_schema.tables where tabl ...

  2. 【刷题】LOJ 556 「Antileaf's Round」咱们去烧菜吧

    题目描述 你有 \(m\) 种物品,第 \(i\) 种物品的大小为 \(a_i\) ​,数量为 \(b_i\)​( \(b_i=0\) 表示有无限个). 你还有 \(n\) 个背包,体积分别为 \(1 ...

  3. 【BZOJ5318】[JSOI2018]扫地机器人(动态规划)

    [BZOJ5318][JSOI2018]扫地机器人(动态规划) 题面 BZOJ 洛谷 题解 神仙题.不会.... 先考虑如果一个点走向了其下方的点,那么其右侧的点因为要被访问到,所以必定只能从其右上方 ...

  4. 【dfs】LETTERS

    1212:LETTERS [题目描述] 给出一个roe×colroe×col的大写字母矩阵,一开始的位置为左上角,你可以向上下左右四个方向移动,并且不能移向曾经经过的字母.问最多可以经过几个字母. [ ...

  5. 「NOI2016」优秀的拆分 解题报告

    「NOI2016」优秀的拆分 这不是个SAM题,只是个LCP题目 95分的Hash很简单,枚举每个点为开头和末尾的AA串个数,然后乘一下之类的. 考虑怎么快速求"每个点为开头和末尾的AA串个 ...

  6. HR_Jumping on the Clouds

    1.没有考虑i+2越界的问题 2.没有考虑结尾三个零导致 -5 3.没有考虑len(c)<2 导致 -5 #!/bin/python3 import math import os import ...

  7. Eclipse Memory Analyzer(MAT)使用

    https://user.qzone.qq.com/731573705/blog/1436389384 Eclipse Memory Analyzer(MAT)使用  一.OutOfMemoryErr ...

  8. [BJOI2012]最多的方案(记忆化搜索)

    第二关和很出名的斐波那契数列有关,地球上的OIer都知道:F1=1, F2=2, Fi = Fi-1 + Fi-2,每一项都可以称为斐波那契数.现在给一个正整数N,它可以写成一些斐波那契数的和的形式. ...

  9. 8、16、32-BIT系列单片机区别与特点

    一.8位单片机 8031/8051/8751是Intel公司早期的产品 1.8031的特点 8031片内不带程序存储器ROM,使用时用户需外接程序存储器和一片逻辑电路373,外接的程序存储器多为EPR ...

  10. CAN总线网络的传输模式

    CAN总线网络的传输模式根据触发条件的不同,在车身CAN网络中可分为事件型.周期性及混合型三种传输模式: 1.事件型传输模式: 随着类型或数据的转变及时发送的消息.此类型消息的好处是极少占用总线资源, ...