zoukankan      html  css  js  c++  java
  • FeignClient调用POST请求时查询参数被丢失的情况分析与处理

    前言

    本文没有详细介绍 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
    來源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

  • 相关阅读:
    Compression algorithm (deflate)
    tcpip数据包编码解析(chunk and gzip)_space of Jialy_百度空间
    What's the difference between the "gzip" and "deflate" HTTP 1.1 encodings?
    gzip压缩算法: gzip 所使用压缩算法的基本原理
    Decompressing a GZip Stream with Zlib
    Frequently Asked Questions about zlib
    how to decompress gzip stream with zlib
    自己动手写web服务器四(web服务器是如何通过压缩数据,web服务器的gzip模块的实现)
    What's the difference between the "gzip" and "deflate" HTTP 1.1 encodings?
    C语言抓http gzip包并解压 失败 C/C++ ChinaUnix.net
  • 原文地址:https://www.cnblogs.com/jpfss/p/10766322.html
Copyright © 2011-2022 走看看