问题配景
通过Postman发送一个哀求到服务A,服务A正确接收到参数后,通过Feign进行长途调用服务服务B。在哀求的长度不凌驾2048的场景下,能够正常处理哀求,当哀求体长度凌驾2048的时候,就会发生JSON的反序列化失败。
抛出如下异常信息如下:
- 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 [Source: (PushbackInputStream); line: 1, column: 2]
- 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 地方相关的代码
通过上面的代码引用我们可以看出来,入口是通过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类型的压缩哀求,导致传递过去的哀求体无法被正确处理。因此,此处有两种办理方案。
- 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企服之家,中国第一个企服评测及商务社交产业平台。 |