Spring Cloud Netflix Feign content encoding
A Spring Cloud Feign extension for request/response compression.
Features
Enables Feign request and response GZIP compression.
Setup
Add the Spring Cloud starter to your project:
<dependency>
<groupId>com.github.jmnarloch</groupId>
<artifactId>feign-encoding-spring-cloud-starter</artifactId>
<version>1.1.1</version>
</dependency>
Usage
To hint the server that client supports GZIP content encoded responses add @EnableFeignAcceptGzipEncoding
@EnableFeignAcceptGzipEncoding
@EnableFeignClients
@Configuration
public static class Application {
}
Remember that this does not make automatically every response compressed and relies entirely on the server configuration. It may be selective and compress only specific media type or generally responses above specific threshold size.
You may also want to compress the request payloads. In order to do this annotate your configuration class with @EnableFeignContentGzipEncoding
.
Properties
Starting from release 1.1 you gain more control over client side request compression. If you have used @EnableFeignContentGzipEncoding
annotation the below settings will allow you to control the request that should be compressed in order not to penalize performance of compressing too small requests.
feign.compression.min-request-size=2048 # the minimum request size
feign.compression.mime-types=text/xml,application/xml,application/json # the request mime types
Server side setup
Spring Boot 1.2.x
As long as you are using Spring Boot 1.2.x enabling GZIP compression varies depending on which embedded server you use: http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#how-to-enable-http-response-compression
For Tomcat you need to specify fallowing properties:
server:
tomcat:
compression: 1024
compressableMimeTypes: application/xml,application/json
If you use Undertow or Jetty you can use Jetty's GzipFilter instead. Although be aware that the GzipFilter has been removed from jetty-servlets somewhere around version 9.3+
Spring Boot 1.3+
Due to the removal of the Jetty GzipFilter Spring Boot 1.3 will bring it's own internal GZIP support:
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-1.3.0-M2-Release-Notes
The new properties will work with any underlying application server.
server:
compression:
enabled: true
min-response-size: 1024
mime-types: application/xml,application/json
Know issues
If you are using Spring Cloud 1.0.3 (Spring Cloud Netlfix 1.0.1) you may experience fallowing error: https://github.com/spring-cloud/spring-cloud-netflix/issues/489
feign.codec.DecodeException: Could not read JSON: Illegal character ((CTRL-CHAR, code 31)): only regular white space (\r, \n, \t) is allowed between tokens
at [Source: java.io.PushbackInputStream@c6b2dd9; line: 1, column: 2]; 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
at [Source: java.io.PushbackInputStream@c6b2dd9; line: 1, column: 2]
at feign.SynchronousMethodHandler.decode(SynchronousMethodHandler.java:150)
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:118)
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:71)
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:94)
at com.sun.proxy.$Proxy64.getInvoices(Unknown Source)
at com.github.jmnarloch.spring.cloud.feign.FeignAcceptEncodingTest.compressedResponse(FeignAcceptEncodingTest.java:64)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:73)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:82)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:73)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:224)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:83)
... 40 more
Caused by: org.springframework.http.converter.HttpMessageNotReadableException: Could not read JSON: Illegal character ((CTRL-CHAR, code 31)): only regular white space (\r, \n, \t) is allowed between tokens
at [Source: java.io.PushbackInputStream@c6b2dd9; line: 1, column: 2]; 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
at [Source: java.io.PushbackInputStream@c6b2dd9; line: 1, column: 2]
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:208)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:200)
at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:97)
at org.springframework.cloud.netflix.feign.support.SpringDecoder.decode(SpringDecoder.java:57)
at org.springframework.cloud.netflix.feign.support.ResponseEntityDecoder.decode(ResponseEntityDecoder.java:40)
at feign.SynchronousMethodHandler.decode(SynchronousMethodHandler.java:146)
... 45 more
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: java.io.PushbackInputStream@c6b2dd9; line: 1, column: 2]
at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:1419)
at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:508)
at com.fasterxml.jackson.core.base.ParserMinimalBase._throwInvalidSpace(ParserMinimalBase.java:459)
at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._skipWSOrEnd(UTF8StreamJsonParser.java:2625)
at com.fasterxml.jackson.core.json.UTF8StreamJsonParser.nextToken(UTF8StreamJsonParser.java:645)
at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:3105)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3051)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2221)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:205)
... 50 more
In order to fix that you have to enable Feign Apache HttpClient. Add this two classes to your component scan packages:
@Configuration
@ConditionalOnClass({Feign.class})
public class FeignApacheHttpClientAutoConfiguration {
@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
public static class HttpClientConfiguration {
@Autowired(required = false)
private HttpClient httpClient;
@Bean
public Client feignClient() {
if (httpClient != null) {
return new ApacheHttpClient(httpClient);
}
return new ApacheHttpClient();
}
}
}
@Configuration
@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignRibbonClientAutoConfiguration {
@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
public static class HttpClientConfiguration {
@Autowired(required = false)
private HttpClient httpClient;
@Resource(name = "cachingLBClientFactory")
private LBClientFactory lbClientFactory;
@Bean
public Client feignClient() {
feign.ribbon.RibbonClient.Builder builder = feign.ribbon.RibbonClient.builder();
if (httpClient != null) {
builder.delegate(new ApacheHttpClient(httpClient));
} else {
builder.delegate(new ApacheHttpClient());
}
if (lbClientFactory != null) {
builder.lbClientFactory(lbClientFactory);
}
return builder.build();
}
}
}
Spring Cloud 1.1 will do this for you automatically.
License
Apache 2.0