【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]