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
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM