【问题标题】:Spring Feign Not Compressing ResponseSpring Feign 不压缩响应
【发布时间】:2020-01-09 21:41:54
【问题描述】:

我正在使用spring feign来压缩请求和响应

在服务器端:

server:
  servlet:
    context-path: /api/v1/
  compression:
    enabled: true
    min-response-size: 1024

当我从 chrome 中点击 api 时,我看到它添加了 'Accept-Encoding': "gzip, deflate, br"

在客户端:

    server:
      port: 8192
      servlet:
        context-path: /api/demo



feign.compression.response.enabled: true

feign.client.config.default.loggerLevel: HEADERS

logging.level.com.example.feigndemo.ManagementApiService: DEBUG

eureka:
  client:
    enabled: false

management-api:
  ribbon:
    listOfServers: localhost:8080

当我看到请求标头通过时,feign 正在传递两个标头。

Accept-Encoding: deflate
Accept-Encoding: gzip

gradle 文件

plugins {
        id 'org.springframework.boot' version '2.1.8.RELEASE'
        id 'io.spring.dependency-management' version '1.0.8.RELEASE'
        id 'java'
    }

    group = 'com.example'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '1.8'

    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }

    repositories {
        mavenCentral()
    }

    ext {
        set('springCloudVersion', "Greenwich.SR2")
    }

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        compile ('org.springframework.cloud:spring-cloud-starter-netflix-ribbon')
        compile('org.springframework.cloud:spring-cloud-starter-openfeign')
    // https://mvnrepository.com/artifact/io.github.openfeign/feign-httpclient
    // https://mvnrepository.com/artifact/io.github.openfeign/feign-httpclient
        //compile group: 'io.github.openfeign', name: 'feign-httpclient', version: '9.5.0'

        compileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }

    dependencyManagement {
        imports {
            mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
        }
    }

响应未压缩。我所看到的是 Spring feign 将“Accept-Encoding”作为两个不同的值发送

如果这里有问题请告诉我

【问题讨论】:

    标签: spring spring-boot spring-cloud-feign


    【解决方案1】:

    这实际上是 Tomcat 和 Jetty 中的一个例外 - 上面给出的多个编码标头是合法的并且应该可以工作,但是 Tomcat 和 Jetty 有一个错误会阻止它们被读取。

    这个bug已经在spring boot githubhere上报告了。 并在tomcat中here供参考。

    在 Tomcat 中,问题已在 9.0.25 中修复,因此如果您可以更新到该问题,则可以解决问题。如果做不到这一点,您可以采取以下解决方法来解决它:

    您需要创建自己的请求拦截器来协调您的 gzip,将标头压缩为单个标头。

    需要将此拦截器添加到 FeignClient 配置中,并将该配置添加到您的 feign 客户端。

    import feign.RequestInterceptor;
    import feign.RequestTemplate;
    import feign.template.HeaderTemplate;
    import java.lang.reflect.Field;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.Map;
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * This is a workaround interceptor based on a known bug in Tomcat and Jetty where
     * the requests are unable to perform gzip compression if the headers are in collection format.
     * This is fixed in tomcat 9.0.25 - once we reach this version we can remove this class
     */
    @Slf4j
    public class GzipRequestInterceptor implements RequestInterceptor {
        @Override
        public void apply(RequestTemplate template) {
            // don't add encoding to all requests - only to the ones with the incorrect header format
            if (requestHasDualEncodingHeaders(template)) {
                replaceTemplateHeader(template, "Accept-Encoding", Collections.singletonList("gzip,deflate"));
            }
        }
    
        private boolean requestHasDualEncodingHeaders(RequestTemplate template) {
            return template.headers().get("Accept-Encoding").contains("deflate")
                    && template.headers().get("Accept-Encoding").contains("gzip");
        }
    
        /** Because request template is immutable, we have to do some workarounds to get to the headers */
        private void replaceTemplateHeader(RequestTemplate template, String key, Collection<String> value) {
            try {
                Field headerField = RequestTemplate.class.getDeclaredField("headers");
                headerField.setAccessible(true);
                ((Map)headerField.get(template)).remove(key);
                HeaderTemplate newEncodingHeaderTemplate = HeaderTemplate.create(key, value);
                ((Map)headerField.get(template)).put(key, newEncodingHeaderTemplate);
            } catch (NoSuchFieldException e) {
                LOGGER.error("exception when trying to access the field [headers] via reflection");
            } catch (IllegalAccessException e) {
                LOGGER.error("exception when trying to get properties from the template headers");
            }
        }
    }
    

    我知道上面看起来有点过头了,但是因为模板头是unmodifiable,所以我们只是使用一点反射来修改它们到我们想要的样子。

    将上述拦截器添加到您的配置 bean

    import feign.RequestInterceptor;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class FeignGzipEncodingConfiguration {
    
        @Bean
        public RequestInterceptor gzipRequestInterceptor() {
    
            return new GzipRequestInterceptor();
        }
    }
    

    你终于可以用配置注解参数把它添加到你的 feign 客户端了

    @FeignClient(name = "feign-client", configuration = FeignGzipEncodingConfiguration.class)
    public interface FeignClient {
        ...
    }
    

    现在,当您发送 feign-client 请求以获取 gzip 压缩信息时,应该会命中请求拦截器。这将擦除双标头,并以gzip,deflate 的形式将可接受的字符串连接起来写入

    【讨论】:

      【解决方案2】:

      几周前我遇到了同样的问题,我开始知道没有有效/直截了当的方法来做这件事。我还知道,当@patan 向 spring 社区 @patan reported issue1@patan reported issue2 报告问题时,为 tomcat 端创建了一张票以尝试解决问题 (issue link)。 Jetty 一侧也有一张与此相关的票 (ticket link)。最初,我计划使用github 中建议的方法,但后来知道该库已经合并到org.springframework.cloud.openfeign.encoding 包下的spring-cloud-openfeign-core jar 中。然而,我们无法按预期实现压缩,并面临以下两个挑战:

      1. 当我们通过设置启用 feign 压缩时,org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingInterceptor (code-link) 类添加了 Accept-Encoding 标头,其值为 gzipdeflate 但由于问题 (ticket) 导致 tomcat 服务器无法将其解释为压缩信号的标志。作为解决方案,我们必须添加手动 Feign 解释器来覆盖
        FeignAcceptGzipEncodingInterceptor 功能并连接标头。
      2. Feign 的默认压缩设置在最简单的场景下完美工作,但是当出现Client calling microservice and that microservice calling another microservice through feign 的情况时,feign 无法处理压缩响应,因为 Spring cloud open feign 解码器默认不解压缩响应(default spring open feign decoder ) 最终以问题结束 (issue link)。所以我们必须自己编写解码器来实现解压。

      我终于找到了基于各种可用资源的解决方案,所以只需按照spring feign压缩的步骤即可:

      application.yml

      spring:
        http:
          encoding:
            enabled: true
      
      #to enable server side compression
      server:
        compression:
          enabled: true
          mime-types:
            - application/json
          min-response-size: 2048
      
      #to enable feign side request/response compression
      feign:
        httpclient:
          enabled: true
        compression:
          request:
            enabled: true
            mime-types:
              - application/json
            min-request-size: 2048
          response:
            enabled: true
      

      注意:以上 feign 配置我默认启用压缩到所有 feign 客户端。

      CustomFeignDecoder

      
      import feign.Response;
      import feign.Util;
      import feign.codec.Decoder;
      import org.springframework.cloud.openfeign.encoding.HttpEncoding;
      
      import java.io.BufferedReader;
      import java.io.ByteArrayInputStream;
      import java.io.IOException;
      import java.io.InputStreamReader;
      import java.lang.reflect.Type;
      import java.nio.charset.StandardCharsets;
      import java.util.Collection;
      import java.util.Objects;
      import java.util.zip.GZIPInputStream;
      
      public class CustomGZIPResponseDecoder implements Decoder {
      
          final Decoder delegate;
      
          public CustomGZIPResponseDecoder(Decoder delegate) {
              Objects.requireNonNull(delegate, "Decoder must not be null. ");
              this.delegate = delegate;
          }
      
          @Override
          public Object decode(Response response, Type type) throws IOException {
              Collection<String> values = response.headers().get(HttpEncoding.CONTENT_ENCODING_HEADER);
              if(Objects.nonNull(values) && !values.isEmpty() && values.contains(HttpEncoding.GZIP_ENCODING)){
                  byte[] compressed = Util.toByteArray(response.body().asInputStream());
                  if ((compressed == null) || (compressed.length == 0)) {
                     return delegate.decode(response, type);
                  }
                  //decompression part
                  //after decompress we are delegating the decompressed response to default 
                  //decoder
                  if (isCompressed(compressed)) {
                      final StringBuilder output = new StringBuilder();
                      final GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(compressed));
                      final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(gis, StandardCharsets.UTF_8));
                      String line;
                      while ((line = bufferedReader.readLine()) != null) {
                          output.append(line);
                      }
                      Response uncompressedResponse = response.toBuilder().body(output.toString().getBytes()).build();
                      return delegate.decode(uncompressedResponse, type);
                  }else{
                      return delegate.decode(response, type);
                  }
              }else{
                  return delegate.decode(response, type);
              }
          }
      
          private static boolean isCompressed(final byte[] compressed) {
              return (compressed[0] == (byte) (GZIPInputStream.GZIP_MAGIC)) && (compressed[1] == (byte) (GZIPInputStream.GZIP_MAGIC >> 8));
          }
      }
      

      FeignCustomConfiguration

      import feign.RequestInterceptor;
      import feign.RequestTemplate;
      import feign.optionals.OptionalDecoder;
      import org.springframework.beans.factory.ObjectFactory;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
      import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
      import org.springframework.cloud.openfeign.support.SpringDecoder;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      @Configuration
      public class CustomFeignConfiguration {
      
      
          @Autowired
          private ObjectFactory<HttpMessageConverters> messageConverters;
      
          //concatenating headers because of https://github.com/spring-projects/spring-boot/issues/18176
          @Bean
          public RequestInterceptor gzipInterceptor() {
              return new RequestInterceptor() {
                  @Override
                  public void apply(RequestTemplate template) {
                      template.header("Accept-Encoding", "gzip, deflate");
                  }
              };
          }
      
          @Bean
          public CustomGZIPResponseDecoder customGZIPResponseDecoder() {
              OptionalDecoder feignDecoder = new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
              return new CustomGZIPResponseDecoder(feignDecoder);
          }
      }
      
      

      其他提示

      如果您打算仅使用 feign-core 库构建 CustomDecoder

      
      import com.fasterxml.jackson.databind.DeserializationFeature;
      import com.fasterxml.jackson.databind.JavaType;
      import com.fasterxml.jackson.databind.ObjectMapper;
      import com.fasterxml.jackson.databind.type.TypeFactory;
      import feign.Response;
      import feign.Util;
      import feign.codec.DecodeException;
      import feign.codec.Decoder;
      import org.springframework.http.HttpEntity;
      import org.springframework.http.HttpHeaders;
      import org.springframework.http.HttpStatus;
      import org.springframework.http.ResponseEntity;
      import org.springframework.http.client.ClientHttpResponse;
      import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
      import org.springframework.util.LinkedMultiValueMap;
      import org.springframework.util.MultiValueMap;
      import org.springframework.util.StringUtils;
      import org.springframework.web.client.HttpMessageConverterExtractor;
      
      import java.io.BufferedReader;
      import java.io.ByteArrayInputStream;
      import java.io.IOException;
      import java.io.InputStream;
      import java.io.InputStreamReader;
      import java.lang.reflect.ParameterizedType;
      import java.lang.reflect.Type;
      import java.lang.reflect.WildcardType;
      import java.nio.charset.StandardCharsets;
      import java.util.ArrayList;
      import java.util.Collection;
      import java.util.Collections;
      import java.util.LinkedList;
      import java.util.Map;
      import java.util.Objects;
      import java.util.zip.GZIPInputStream;
      
      import static java.util.zip.GZIPInputStream.GZIP_MAGIC;
      
      public class CustomGZIPResponseDecoder implements Decoder {
      
          private final Decoder delegate;
      
          public CustomGZIPResponseDecoder(Decoder delegate) {
              Objects.requireNonNull(delegate, "Decoder must not be null. ");
              this.delegate = delegate;
          }
      
          @Override
          public Object decode(Response response, Type type) throws IOException {
              Collection<String> values = response.headers().get("Content-Encoding");
              if (Objects.nonNull(values) && !values.isEmpty() && values.contains("gzip")) {
                  byte[] compressed = Util.toByteArray(response.body().asInputStream());
                  if ((compressed == null) || (compressed.length == 0)) {
                      return delegate.decode(response, type);
                  }
                  if (isCompressed(compressed)) {
                      Response uncompressedResponse = getDecompressedResponse(response, compressed);
                      return getObject(type, uncompressedResponse);
                  } else {
                      return getObject(type, response);
                  }
              } else {
                  return getObject(type, response);
              }
          }
      
          private Object getObject(Type type, Response response) throws IOException {
              ObjectMapper mapper = new ObjectMapper();
              mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
              if (response.status() == 404 || response.status() == 204)
                  return Util.emptyValueOf(type);
              if (Objects.isNull(response.body()))
                  return null;
              if (byte[].class.equals(type))
                  return Util.toByteArray(response.body().asInputStream());
              if (isParameterizeHttpEntity(type)) {
                  type = ((ParameterizedType) type).getActualTypeArguments()[0];
                  if (type instanceof Class || type instanceof ParameterizedType
                          || type instanceof WildcardType) {
                      @SuppressWarnings({"unchecked", "rawtypes"})
                      HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor(
                              type, Collections.singletonList(new MappingJackson2HttpMessageConverter(mapper)));
                      Object decodedObject = extractor.extractData(new FeignResponseAdapter(response));
                      return createResponse(decodedObject, response);
                  }
                  throw new DecodeException(HttpStatus.INTERNAL_SERVER_ERROR.value(),
                          "type is not an instance of Class or ParameterizedType: " + type);
              } else if (isHttpEntity(type)) {
                  return delegate.decode(response, type);
              } else if (String.class.equals(type)) {
                  String responseValue = Util.toString(response.body().asReader());
                  return StringUtils.isEmpty(responseValue) ? Util.emptyValueOf(type) : responseValue;
              } else {
                  String s = Util.toString(response.body().asReader());
                  JavaType javaType = TypeFactory.defaultInstance().constructType(type);
                  return !StringUtils.isEmpty(s) ? mapper.readValue(s, javaType) : Util.emptyValueOf(type);
              }
          }
      
          public static boolean isCompressed(final byte[] compressed) {
              return (compressed[0] == (byte) (GZIP_MAGIC)) && (compressed[1] == (byte) (GZIP_MAGIC >> 8));
          }
      
          public static Response getDecompressedResponse(Response response, byte[] compressed) throws IOException {
              final StringBuilder output = new StringBuilder();
              final GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(compressed));
              final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(gis, StandardCharsets.UTF_8));
              String line;
              while ((line = bufferedReader.readLine()) != null) {
                  output.append(line);
              }
              return response.toBuilder().body(output.toString().getBytes()).build();
          }
      
          public static String getDecompressedResponseAsString(byte[] compressed) throws IOException {
              final StringBuilder output = new StringBuilder();
              final GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(compressed));
              final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(gis, StandardCharsets.UTF_8));
              String line;
              while ((line = bufferedReader.readLine()) != null) {
                  output.append(line);
              }
              return output.toString();
          }
      
          private boolean isParameterizeHttpEntity(Type type) {
              if (type instanceof ParameterizedType) {
                  return isHttpEntity(((ParameterizedType) type).getRawType());
              }
              return false;
          }
      
          private boolean isHttpEntity(Type type) {
              if (type instanceof Class) {
                  Class c = (Class) type;
                  return HttpEntity.class.isAssignableFrom(c);
              }
              return false;
          }
      
          private <T> ResponseEntity<T> createResponse(Object instance, Response response) {
      
              MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
              for (String key : response.headers().keySet()) {
                  headers.put(key, new LinkedList<>(response.headers().get(key)));
              }
      
              return new ResponseEntity<>((T) instance, headers, HttpStatus.valueOf(response
                      .status()));
          }
      
          private class FeignResponseAdapter implements ClientHttpResponse {
      
              private final Response response;
      
              private FeignResponseAdapter(Response response) {
                  this.response = response;
              }
      
              @Override
              public HttpStatus getStatusCode() throws IOException {
                  return HttpStatus.valueOf(this.response.status());
              }
      
              @Override
              public int getRawStatusCode() throws IOException {
                  return this.response.status();
              }
      
              @Override
              public String getStatusText() throws IOException {
                  return this.response.reason();
              }
      
              @Override
              public void close() {
                  try {
                      this.response.body().close();
                  } catch (IOException ex) {
                      // Ignore exception on close...
                  }
              }
      
              @Override
              public InputStream getBody() throws IOException {
                  return this.response.body().asInputStream();
              }
      
              @Override
              public HttpHeaders getHeaders() {
                  return getHttpHeaders(this.response.headers());
              }
      
              private HttpHeaders getHttpHeaders(Map<String, Collection<String>> headers) {
                  HttpHeaders httpHeaders = new HttpHeaders();
                  for (Map.Entry<String, Collection<String>> entry : headers.entrySet()) {
                      httpHeaders.put(entry.getKey(), new ArrayList<>(entry.getValue()));
                  }
                  return httpHeaders;
              }
          }
      
      }
      
      

      如果你打算构建自己的 Feign 构建器,那么你可以像下面这样配置

       Feign.builder().decoder(new CustomGZIPResponseDecoder(new feign.optionals.OptionalDecoder(new feign.codec.StringDecoder())))
                       .target(SomeFeignClient.class, "someurl");
      
      

      更新上述答案: 如果您计划将spring-cloud-openfeign-core 的依赖版本更新为'org.springframework.cloud:spring-cloud-openfeign-core:2.2.5.RELEASE',请注意FeignContentGzipEncodingAutoConfiguration class 中的以下更改。 在FeignContentGzipEncodingAutoConfiguration 类中,ConditionalOnProperty 注释的签名从 @ConditionalOnProperty("feign.compression.request.enabled", matchIfMissing = false)@ConditionalOnProperty(value = "feign.compression.request.enabled"),所以默认情况下 FeignContentGzipEncodingInterceptor bean 将被注入到 spring 容器中,如果你的环境中有应用程序属性 feign.request.compression=true 并在默认/配置的大小限制超过时压缩请求正文。如果您的服务器没有处理压缩请求的机制,这会导致问题,在这种情况下添加/修改属性为feign.request.compression=false

      【讨论】:

      • 非常感谢,这是我经过数小时搜索后找到的最终解决方案。我遇到的问题是:feign.codec.DecodeException ... ... JSON 解析错误:非法字符((CTRL-CHAR,code31)):之间只允许常规空格(\r,\n,\t)令牌
      【解决方案3】:

      如果您使用的是最新的 Spring Boot 版本,那么它会提供默认的 Gzip 解码器,因此无需编写您的自定义解码器。请改用以下属性:-

      feign:
        compression:
          response:
            enabled: true
            useGzipDecoder: true
      

      【讨论】:

      • 我试过这个,但运气不好,不确定它是否与我的 spring-boot 版本有关,无论如何,@Prasanth_Rajendran 的答案是完美的。
      猜你喜欢
      • 2016-11-20
      • 2012-05-18
      • 2021-05-24
      • 1970-01-01
      • 1970-01-01
      • 2012-02-12
      • 2012-06-12
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多