守听 发表于 2024-12-5 14:27:23

【Feign调用】如何办理哀求参数过大,Feign启用Gzip压缩后,被调服务无法正

问题配景

通过Postman发送一个哀求到服务A,服务A正确接收到参数后,通过Feign进行长途调用服务服务B。在哀求的长度不凌驾2048的场景下,能够正常处理哀求,当哀求体长度凌驾2048的时候,就会发生JSON的反序列化失败。
https://i-blog.csdnimg.cn/direct/ba5ebae5cd334e958e42d78433b64d14.png
抛出如下异常信息如下:
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
// .. 省略其他的异常输出
Caused by: com.fasterxml.jackson.core.JsonParseException: Illegal character ((CTRL-CHAR, code 31)): only regular white space (\r, \n, \t) is allowed between tokens
at
        at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:2391)
        at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:735)
        at com.fasterxml.jackson.core.base.ParserMinimalBase._throwInvalidSpace(ParserMinimalBase.java:713)
        at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._skipWSOrEnd(UTF8StreamJsonParser.java:3057)
        at com.fasterxml.jackson.core.json.UTF8StreamJsonParser.nextToken(UTF8StreamJsonParser.java:756)
        at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:4761)
        at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4667)
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3682)
        at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:378)
        ... 55 more
原因分析

1、可能原因猜测

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

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

那么我们查找Feign跟哀求体巨细相关的设置,在org.springframework.cloud.openfeign.encoding.FeignClientEncodingProperties中找到有关的配置项。
FeignClientEncodingProperties中源码如下:
@ConfigurationProperties("feign.compression.request")
public class FeignClientEncodingProperties {

        /**
       * The list of supported mime types.
       */
        private String[] mimeTypes = new String[] { "text/xml", "application/xml", "application/json" };

        /**
       * The minimum threshold content size.
       */
        private int minRequestSize = 2048;
        // 其他的代码省略.....
2、查找使用minRequestSize 地方相关的代码

https://i-blog.csdnimg.cn/direct/11f52633252146cdadf6c6e2da2def51.png
通过上面的代码引用我们可以看出来,入口是通过FeignContentGzipEncodingInterceptor来使用哀求体巨细这个配置参数的。
3、使用地方代码

# org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingInterceptor#requiresCompression
private boolean requiresCompression(RequestTemplate template) {

                final Map<String, Collection<String>> headers = template.headers();
                return matchesMimeType(headers.get(HttpEncoding.CONTENT_TYPE))
                                && contentLengthExceedThreshold(headers.get(HttpEncoding.CONTENT_LENGTH));
        }
        private boolean contentLengthExceedThreshold(Collection<String> contentLength) {

                try {
                        if (contentLength == null || contentLength.size() != 1) {
                                return false;
                        }

                        final String strLen = contentLength.iterator().next();
                        final long length = Long.parseLong(strLen);
                        return length > getProperties().getMinRequestSize();
                }
                catch (NumberFormatException ex) {
                        return false;
                }
        }
        private boolean matchesMimeType(Collection<String> contentTypes) {
                if (contentTypes == null || contentTypes.size() == 0) {
                        return false;
                }

                if (getProperties().getMimeTypes() == null || getProperties().getMimeTypes().length == 0) {
                        // no specific mime types has been set - matching everything
                        return true;
                }

                for (String mimeType : getProperties().getMimeTypes()) {
                        if (contentTypes.contains(mimeType)) {
                                return true;
                        }
                }

                return false;
        }
FeignContentGzipEncodingInterceptor是一个用于 Feign 客户端的拦截器,其紧张作用是对 哀求体 进行 Gzip 压缩。在调用长途服务时,拦截器会检测哀求体的巨细或特定条件,如果需要压缩,则会将哀求体进行 Gzip 编码,并在哀求头中添加 Content-Encoding: gzip,告知服务器哀求体是经过 Gzip 压缩的。
上面是我们见名知意认为FeignContentGzipEncodingInterceptor应该要具备的功能,但是其在最终实现中只是在哀求头中添加Content-Encoding:gzip。最终的实现交给了FeignBlockingLoadBalancerClient来进行处理。
4、使用FeignBlockingLoadBalancerClient来对数据进行真正压缩及处理

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

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


[*]1、客户端禁止Gzip压缩
feign:
compression:
    request:
      mime-types: ""
将mime-types设置为空,就会对所有类型的哀求都不再进行压缩。设置后,进行测试发现当哀求体凌驾2048的时候,能够正常访问


[*]2、服务端支持Gzip压缩
package com.gw.gzip;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.*;
/**


[*]是服务端支持Gzip压缩
*/
新建一个GzipDecompressionFilter类,来支持Gzip压缩类型的哀求。

import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.*;
import java.io.IOException;
import java.util.zip.GZIPInputStream;

public class GzipDecompressionFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
      HttpServletRequest httpServletRequest = (HttpServletRequest) request;

      // 检查请求头中的 Content-Encoding 是否为 gzip
      if ("gzip".equalsIgnoreCase(httpServletRequest.getHeader("Content-Encoding"))) {
            // 使用 GZIPInputStream 解压缩请求体
            GZIPInputStream gzipInputStream = new GZIPInputStream(httpServletRequest.getInputStream());
            ServletRequest wrapper = new GzipRequestWrapper(httpServletRequest, gzipInputStream);
            chain.doFilter(wrapper, response);
      } else {
            chain.doFilter(request, response);
      }
    }

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void destroy() {
    }
}
GzipRequestWrapper的代码如下:
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.io.InputStream;

public class GzipRequestWrapper extends HttpServletRequestWrapper {
    private InputStream inputStream;

    public GzipRequestWrapper(HttpServletRequest request, InputStream inputStream) {
      super(request);
      this.inputStream = inputStream;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
      return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }

            @Override
            public int read() throws IOException {
                return inputStream.read();
            }
      };
    }
}

注册GzipDecompressionFilter
FilterConfig
@Configuration
public class FilterConfig {
    @Bean
    public Filter gzipDecompressionFilter() {
      return new GzipDecompressionFilter();
    }
}
通过此种方式也可以办理对应的问题。
3、总结

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【Feign调用】如何办理哀求参数过大,Feign启用Gzip压缩后,被调服务无法正