FeignClient调用POST请求时查询参数被丢失的情况分析与处理

前言 本文没有详细介绍 FeignClient 的知识点,网上有很多优秀的文章介绍了 FeignCient 的知识点,在这里本人就不重复了,只是专注在这个问题点上。
查询参数丢失场景 业务描述: 业务系统需要更新用户系统中的A资源,由于只想更新A资源的一个字段信息为B,所以没有选择通过 entity 封装B,而是直接通过查询参数来传递B信息
文字描述:使用FeignClient来进行远程调用时,如果POST请求中有查询参数并且没有请求实体(body为空),那么查询参数被丢失,服务提供者获取不到查询参数的值。
代码描述:B的值被丢失,服务提供者获取不到B的值

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

问题分析 背景
  1. 使用 FeignClient 客户端
  2. 使用 feign-httpclient 中的 ApacheHttpClient 来进行实际请求的调用
com.netflix.feign feign-httpclient 8.18.0

直入源码
通过对 FeignClient 的源码阅读,发现问题不是出在参数解析上,而是在使用 ApacheHttpClient 进行请求时,其将查询参数放进请求body中了,下面看源码具体是如何处理的
feign.httpclient.ApacheHttpClient 这是 feign-httpclient 进行实际请求的方法
@Override public Response execute(Request request, Request.Options options) throws IOException { HttpUriRequest httpUriRequest; try { httpUriRequest = toHttpUriRequest(request, options); } catch (URISyntaxException e) { throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e); } HttpResponse httpResponse = client.execute(httpUriRequest); return toFeignResponse(httpResponse); }HttpUriRequest toHttpUriRequest(Request request, Request.Options options) throws UnsupportedEncodingException, MalformedURLException, URISyntaxException { RequestBuilder requestBuilder = RequestBuilder.create(request.method()); //per request timeouts RequestConfig requestConfig = RequestConfig .custom() .setConnectTimeout(options.connectTimeoutMillis()) .setSocketTimeout(options.readTimeoutMillis()) .build(); requestBuilder.setConfig(requestConfig); URI uri = new URIBuilder(request.url()).build(); requestBuilder.setUri(uri.getScheme() + "://" + uri.getAuthority() + uri.getRawPath()); //request query params List queryParams = URLEncodedUtils.parse(uri, requestBuilder.getCharset().name()); for (NameValuePair queryParam: queryParams) { requestBuilder.addParameter(queryParam); }//request headers boolean hasAcceptHeader = false; for (Map.Entry> headerEntry : request.headers().entrySet()) { String headerName = headerEntry.getKey(); if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) { hasAcceptHeader = true; }if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH)) { // The 'Content-Length' header is always set by the Apache client and it // doesn't like us to set it as well. continue; }for (String headerValue : headerEntry.getValue()) { requestBuilder.addHeader(headerName, headerValue); } } //some servers choke on the default accept string, so we'll set it to anything if (!hasAcceptHeader) { requestBuilder.addHeader(ACCEPT_HEADER_NAME, "*/*"); }//request body if (request.body() != null) {//body为空,则HttpEntity为空HttpEntity entity = null; if (request.charset() != null) { ContentType contentType = getContentType(request); String content = new String(request.body(), request.charset()); entity = new StringEntity(content, contentType); } else { entity = new ByteArrayEntity(request.body()); }requestBuilder.setEntity(entity); }//调用org.apache.http.client.methods.RequestBuilder#build方法 return requestBuilder.build(); }

org.apache.http.client.methods.RequestBuilder 此类是 HttpUriRequest 的Builder类,下面看build方法
public HttpUriRequest build() { final HttpRequestBase result; URI uriNotNull = this.uri != null ? this.uri : URI.create("/"); HttpEntity entityCopy = this.entity; if (parameters != null && !parameters.isEmpty()) { // 这里:如果HttpEntity为空,并且为POST请求或者为PUT请求时,这个方法会将查询参数取出来封装成了HttpEntity // 就是在这里查询参数被丢弃了,准确的说是被转换位置了 if (entityCopy == null && (HttpPost.METHOD_NAME.equalsIgnoreCase(method) || HttpPut.METHOD_NAME.equalsIgnoreCase(method))) { entityCopy = new UrlEncodedFormEntity(parameters, charset != null ? charset : HTTP.DEF_CONTENT_CHARSET); } else { try { uriNotNull = new URIBuilder(uriNotNull) .setCharset(this.charset) .addParameters(parameters) .build(); } catch (final URISyntaxException ex) { // should never happen } } } if (entityCopy == null) { result = new InternalRequest(method); } else { final InternalEntityEclosingRequest request = new InternalEntityEclosingRequest(method); request.setEntity(entityCopy); result = request; } result.setProtocolVersion(this.version); result.setURI(uriNotNull); if (this.headergroup != null) { result.setHeaders(this.headergroup.getAllHeaders()); } result.setConfig(this.config); return result; }

解决方案 既然已经知道原因了,那么解决方法就有很多种了,下面就介绍常规的解决方案:
  1. 使用 feign-okhttp 来进行请求调用,这里就不列源码了,感兴趣大家可以去看, feign-okhttp 底层没有判断如果body为空则把查询参数放入body中。
  2. 使用 io.github.openfeign:feign-httpclient:9.5.1 依赖,截取部分源码说明原因如下:
HttpUriRequest toHttpUriRequest(Request request, Request.Options options) throws UnsupportedEncodingException, MalformedURLException, URISyntaxException { RequestBuilder requestBuilder = RequestBuilder.create(request.method()); //省略部分代码 //request body if (request.body() != null) { //省略部分代码 } else { // 此处,如果为null,则会塞入一个byte数组为0的对象 requestBuilder.setEntity(new ByteArrayEntity(new byte[0])); }return requestBuilder.build(); }

推荐的依赖
io.github.openfeign feign-httpclient 9.5.1

或者
io.github.openfeign feign-okhttp 9.5.1

总结 【FeignClient调用POST请求时查询参数被丢失的情况分析与处理】目前绝大部分的介绍 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 来进行维护了。
参考资料:
  • OpenFeign github 地址

    推荐阅读