ToB企服应用市场:ToB评测及商务社交产业平台

标题: 【Feign调用】如何办理哀求参数过大,Feign启用Gzip压缩后,被调服务无法正 [打印本页]

作者: 守听    时间: 2024-12-5 14:27
标题: 【Feign调用】如何办理哀求参数过大,Feign启用Gzip压缩后,被调服务无法正
问题配景

通过Postman发送一个哀求到服务A,服务A正确接收到参数后,通过Feign进行长途调用服务服务B。在哀求的长度不凌驾2048的场景下,能够正常处理哀求,当哀求体长度凌驾2048的时候,就会发生JSON的反序列化失败。

抛出如下异常信息如下:
  1. org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Illegal character ((CTRL-CHAR, code 31)): only regular white space (\r, \n, \t) is allowed between tokens; nested exception is com.fasterxml.jackson.core.JsonParseException: Illegal character ((CTRL-CHAR, code 31)): only regular white space (\r, \n, \t) is allowed between tokens
  2. // .. 省略其他的异常输出
  3. Caused by: com.fasterxml.jackson.core.JsonParseException: Illegal character ((CTRL-CHAR, code 31)): only regular white space (\r, \n, \t) is allowed between tokens
  4. at [Source: (PushbackInputStream); line: 1, column: 2]
  5.         at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:2391)
  6.         at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:735)
  7.         at com.fasterxml.jackson.core.base.ParserMinimalBase._throwInvalidSpace(ParserMinimalBase.java:713)
  8.         at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._skipWSOrEnd(UTF8StreamJsonParser.java:3057)
  9.         at com.fasterxml.jackson.core.json.UTF8StreamJsonParser.nextToken(UTF8StreamJsonParser.java:756)
  10.         at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:4761)
  11.         at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4667)
  12.         at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3682)
  13.         at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:378)
  14.         ... 55 more
复制代码
原因分析

1、可能原因猜测

在哀求体长度比较小的场景,能够正常访问。长度凌驾某个阈值后就发生了异常,那么可以思量是否为客户端在发送哀求的时候,对哀求体进行特殊处理,如使用某种压缩算法对哀求进行压缩,以便减少哀求的巨细。
2、问题验证及办理方案

1)、查找跟哀求体巨细相关的配置项

那么我们查找Feign跟哀求体巨细相关的设置,在org.springframework.cloud.openfeign.encoding.FeignClientEncodingProperties中找到有关的配置项。
FeignClientEncodingProperties中源码如下:
  1. @ConfigurationProperties("feign.compression.request")
  2. public class FeignClientEncodingProperties {
  3.         /**
  4.          * The list of supported mime types.
  5.          */
  6.         private String[] mimeTypes = new String[] { "text/xml", "application/xml", "application/json" };
  7.         /**
  8.          * The minimum threshold content size.
  9.          */
  10.         private int minRequestSize = 2048;
  11.         // 其他的代码省略.....
复制代码
2、查找使用minRequestSize 地方相关的代码


通过上面的代码引用我们可以看出来,入口是通过FeignContentGzipEncodingInterceptor来使用哀求体巨细这个配置参数的。
3、使用地方代码

  1. # org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingInterceptor#requiresCompression
  2. private boolean requiresCompression(RequestTemplate template) {
  3.                 final Map<String, Collection<String>> headers = template.headers();
  4.                 return matchesMimeType(headers.get(HttpEncoding.CONTENT_TYPE))
  5.                                 && contentLengthExceedThreshold(headers.get(HttpEncoding.CONTENT_LENGTH));
  6.         }
  7.         private boolean contentLengthExceedThreshold(Collection<String> contentLength) {
  8.                 try {
  9.                         if (contentLength == null || contentLength.size() != 1) {
  10.                                 return false;
  11.                         }
  12.                         final String strLen = contentLength.iterator().next();
  13.                         final long length = Long.parseLong(strLen);
  14.                         return length > getProperties().getMinRequestSize();
  15.                 }
  16.                 catch (NumberFormatException ex) {
  17.                         return false;
  18.                 }
  19.         }
  20.         private boolean matchesMimeType(Collection<String> contentTypes) {
  21.                 if (contentTypes == null || contentTypes.size() == 0) {
  22.                         return false;
  23.                 }
  24.                 if (getProperties().getMimeTypes() == null || getProperties().getMimeTypes().length == 0) {
  25.                         // no specific mime types has been set - matching everything
  26.                         return true;
  27.                 }
  28.                 for (String mimeType : getProperties().getMimeTypes()) {
  29.                         if (contentTypes.contains(mimeType)) {
  30.                                 return true;
  31.                         }
  32.                 }
  33.                 return false;
  34.         }
复制代码
FeignContentGzipEncodingInterceptor是一个用于 Feign 客户端的拦截器,其紧张作用是对 哀求体 进行 Gzip 压缩。在调用长途服务时,拦截器会检测哀求体的巨细或特定条件,如果需要压缩,则会将哀求体进行 Gzip 编码,并在哀求头中添加 Content-Encoding: gzip,告知服务器哀求体是经过 Gzip 压缩的。
上面是我们见名知意认为FeignContentGzipEncodingInterceptor应该要具备的功能,但是其在最终实现中只是在哀求头中添加Content-Encoding:gzip。最终的实现交给了FeignBlockingLoadBalancerClient来进行处理。
4、使用FeignBlockingLoadBalancerClient来对数据进行真正压缩及处理

feign.Client.Default#convertAndSend
  1. final URL url = new URL(request.url());
  2.                 // 省略部分代码  ....
  3.       boolean gzipEncodedRequest = this.isGzip(contentEncodingValues);
  4.       boolean deflateEncodedRequest = this.isDeflate(contentEncodingValues);
  5.                 //省略部分代码  ....
  6.         connection.setDoOutput(true);
  7.         OutputStream out = connection.getOutputStream();
  8.         if (gzipEncodedRequest) {
  9.           out = new GZIPOutputStream(out);
  10.         } else if (deflateEncodedRequest) {
  11.           out = new DeflaterOutputStream(out);
  12.         }
  13.         try {
  14.           out.write(request.body());
  15.         } finally {
  16.              // 省略部分代码  ....
  17.       }
  18.       return connection;
复制代码
上面代码的作用的是在哀求头中包罗了Content-Encoding:gzip的时候,使用GZIPOutputStream来输出对应的数据。
5、问题办理方案

从上面的分析已经可以看出来,当长度凌驾了2048的时候,而且哀求类型为"text/xml", “application/xml”, "application/json"三个内里中一种的时候,将会对哀求进行压缩。而此时由于服务B不支持Gzip类型的压缩哀求,导致传递过去的哀求体无法被正确处理。因此,此处有两种办理方案。

  1. feign:
  2.   compression:
  3.     request:
  4.       mime-types: ""
复制代码
将mime-types设置为空,就会对所有类型的哀求都不再进行压缩。设置后,进行测试发现当哀求体凌驾2048的时候,能够正常访问

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.*;
/**

  1. import javax.servlet.FilterConfig;
  2. import javax.servlet.ServletException;
  3. import javax.servlet.ServletRequest;
  4. import javax.servlet.ServletResponse;
  5. import javax.servlet.http.HttpServletRequest;
  6. import javax.servlet.*;
  7. import java.io.IOException;
  8. import java.util.zip.GZIPInputStream;
  9. public class GzipDecompressionFilter implements Filter {
  10.     @Override
  11.     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
  12.         HttpServletRequest httpServletRequest = (HttpServletRequest) request;
  13.         // 检查请求头中的 Content-Encoding 是否为 gzip
  14.         if ("gzip".equalsIgnoreCase(httpServletRequest.getHeader("Content-Encoding"))) {
  15.             // 使用 GZIPInputStream 解压缩请求体
  16.             GZIPInputStream gzipInputStream = new GZIPInputStream(httpServletRequest.getInputStream());
  17.             ServletRequest wrapper = new GzipRequestWrapper(httpServletRequest, gzipInputStream);
  18.             chain.doFilter(wrapper, response);
  19.         } else {
  20.             chain.doFilter(request, response);
  21.         }
  22.     }
  23.     @Override
  24.     public void init(FilterConfig filterConfig) {
  25.     }
  26.     @Override
  27.     public void destroy() {
  28.     }
  29. }
复制代码
GzipRequestWrapper的代码如下:
  1. import javax.servlet.ReadListener;
  2. import javax.servlet.ServletInputStream;
  3. import javax.servlet.http.HttpServletRequest;
  4. import javax.servlet.http.HttpServletRequestWrapper;
  5. import java.io.IOException;
  6. import java.io.InputStream;
  7. public class GzipRequestWrapper extends HttpServletRequestWrapper {
  8.     private InputStream inputStream;
  9.     public GzipRequestWrapper(HttpServletRequest request, InputStream inputStream) {
  10.         super(request);
  11.         this.inputStream = inputStream;
  12.     }
  13.     @Override
  14.     public ServletInputStream getInputStream() throws IOException {
  15.         return new ServletInputStream() {
  16.             @Override
  17.             public boolean isFinished() {
  18.                 return false;
  19.             }
  20.             @Override
  21.             public boolean isReady() {
  22.                 return true;
  23.             }
  24.             @Override
  25.             public void setReadListener(ReadListener readListener) {
  26.             }
  27.             @Override
  28.             public int read() throws IOException {
  29.                 return inputStream.read();
  30.             }
  31.         };
  32.     }
  33. }
复制代码
注册GzipDecompressionFilter
FilterConfig
  1. @Configuration
  2. public class FilterConfig {
  3.     @Bean
  4.     public Filter gzipDecompressionFilter() {
  5.         return new GzipDecompressionFilter();
  6.     }
  7. }
复制代码
通过此种方式也可以办理对应的问题。
3、总结

本篇文章紧张定位分析了Feign在长途调用的时候,为什么在哀求体过大的时候,导致调用失败的原因,以及办理方案。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4