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

  • 相关阅读:
    汽车发动机参数指标含义
    谷歌浏览器Google Chrome和Adobe Flash Plugins插件安装问题
    今天研究成功ASP动态管理数据表及字段
    漂亮的弹出对话框!
    Opera Dragonfly 提供下载了
    javascript客户端验证函数大全
    C# Regex类用法
    只能输入数字的TextBox
    c#,winform,treeview,选中节点,选中相应的全部子节点,取消节点,取消父节点,小技巧
    WinForm中如何判断关闭事件来源于用户点击右上角的“关闭”按钮
  • 原文地址:https://www.cnblogs.com/jpfss/p/10766322.html
Copyright © 2020-2023  润新知