Adding gzip support for Spring Decoder (#230)
* Adding gzip support for Spring Decoder * Restoring test config * Switch to medium resource class in circleci. (#231) * Adding gzip support for Spring Decoder * Restoring test config * Creating extra property for Gzip Decoder * Fixing requested changes and updating doc * Missed one change request * Removing some properties
This commit is contained in:
committed by
Olga Maciaszek-Sharma
parent
cb1ea20f3f
commit
bc10583bf3
@@ -46,9 +46,11 @@ import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionMa
|
||||
import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory;
|
||||
import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
|
||||
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
|
||||
import org.springframework.cloud.openfeign.support.DefaultGzipDecoderConfiguration;
|
||||
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
@@ -58,6 +60,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
@ConditionalOnClass(Feign.class)
|
||||
@EnableConfigurationProperties({ FeignClientProperties.class,
|
||||
FeignHttpClientProperties.class })
|
||||
@Import(DefaultGzipDecoderConfiguration.class)
|
||||
public class FeignAutoConfiguration {
|
||||
|
||||
@Autowired(required = false)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2013-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign.support;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Collection;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import feign.FeignException;
|
||||
import feign.Response;
|
||||
import feign.Util;
|
||||
import feign.codec.Decoder;
|
||||
|
||||
import org.springframework.cloud.openfeign.encoding.HttpEncoding;
|
||||
|
||||
/**
|
||||
* When response is compressed as gzip, this decompresses and uses {@link SpringDecoder}
|
||||
* to decode.
|
||||
*
|
||||
* @author Jaesik Kim
|
||||
*/
|
||||
public class DefaultGzipDecoder implements Decoder {
|
||||
|
||||
private Decoder decoder;
|
||||
|
||||
public DefaultGzipDecoder(Decoder decoder) {
|
||||
this.decoder = decoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(final Response response, Type type)
|
||||
throws IOException, FeignException {
|
||||
Collection<String> encoding = response.headers()
|
||||
.containsKey(HttpEncoding.CONTENT_ENCODING_HEADER)
|
||||
? response.headers().get(HttpEncoding.CONTENT_ENCODING_HEADER)
|
||||
: null;
|
||||
|
||||
if (encoding != null) {
|
||||
if (encoding.contains(HttpEncoding.GZIP_ENCODING)) {
|
||||
String decompressedBody = decompress(response);
|
||||
if (decompressedBody != null) {
|
||||
Response decompressedResponse = response.toBuilder()
|
||||
.body(decompressedBody.getBytes()).build();
|
||||
return decoder.decode(decompressedResponse, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
return decoder.decode(response, type);
|
||||
}
|
||||
|
||||
private String decompress(Response response) throws IOException {
|
||||
if (response.body() == null) {
|
||||
return null;
|
||||
}
|
||||
try (GZIPInputStream gzipInputStream = new GZIPInputStream(
|
||||
response.body().asInputStream());
|
||||
BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(gzipInputStream, Util.ISO_8859_1))) {
|
||||
String outputString = "";
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
outputString += line;
|
||||
}
|
||||
return outputString;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2013-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign.support;
|
||||
|
||||
import feign.codec.Decoder;
|
||||
import feign.optionals.OptionalDecoder;
|
||||
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
|
||||
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Configures Default Gzip Decoder.
|
||||
*
|
||||
* @author Jaesik Kim
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty("feign.compression.response.enabled")
|
||||
// The OK HTTP client uses "transparent" compression.
|
||||
// If the accept-encoding header is present, it disables transparent compression
|
||||
@ConditionalOnMissingBean(type = "okhttp3.OkHttpClient")
|
||||
@AutoConfigureAfter(FeignAutoConfiguration.class)
|
||||
public class DefaultGzipDecoderConfiguration {
|
||||
|
||||
private ObjectFactory<HttpMessageConverters> messageConverters;
|
||||
|
||||
public DefaultGzipDecoderConfiguration(
|
||||
ObjectFactory<HttpMessageConverters> messageConverters) {
|
||||
this.messageConverters = messageConverters;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty("feign.compression.response.useGzipDecoder")
|
||||
public Decoder defaultGzipDecoder() {
|
||||
return new OptionalDecoder(new ResponseEntityDecoder(
|
||||
new DefaultGzipDecoder(new SpringDecoder(messageConverters))));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
|
||||
import org.springframework.cloud.openfeign.encoding.HttpEncoding;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -68,7 +69,8 @@ public class SpringEncoder implements Encoder {
|
||||
// template.body(conversionService.convert(object, String.class));
|
||||
if (requestBody != null) {
|
||||
Class<?> requestType = requestBody.getClass();
|
||||
Collection<String> contentTypes = request.headers().get("Content-Type");
|
||||
Collection<String> contentTypes = request.headers()
|
||||
.get(HttpEncoding.CONTENT_TYPE);
|
||||
|
||||
MediaType requestContentType = null;
|
||||
if (contentTypes != null && !contentTypes.isEmpty()) {
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
"description": "Enables the response from Feign to be compressed.",
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"name": "feign.compression.response.useGzipDecoder",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Enables the default gzip decoder to be used.",
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"name": "feign.compression.request.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2013-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Jaesik Kim
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(classes = DefaultGzipDecoderTests.Application.class,
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
value = { "spring.application.name=defaultGzipDecoderTests",
|
||||
"feign.compression.response.enabled=true",
|
||||
"feign.compression.response.useGzipDecoder=true",
|
||||
"feign.client.config.default.loggerLevel=full",
|
||||
"logging.level.org.springframework.cloud.openfeign=DEBUG" })
|
||||
@DirtiesContext
|
||||
public class DefaultGzipDecoderTests extends FeignClientFactoryBean {
|
||||
|
||||
@Autowired
|
||||
FeignContext context;
|
||||
|
||||
@Value("${local.server.port}")
|
||||
private int port = 0;
|
||||
|
||||
public DefaultGzipDecoderTests() {
|
||||
setName("tests");
|
||||
setContextId("test");
|
||||
}
|
||||
|
||||
public TestClient testClient() {
|
||||
setType(this.getClass());
|
||||
return feign(context).target(TestClient.class, "http://localhost:" + this.port);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBodyDecompress() {
|
||||
ResponseEntity<Hello> response = testClient().getGzipResponse();
|
||||
assertThat(response).as("response was null").isNotNull();
|
||||
assertThat(response.getStatusCode()).as("wrong status code")
|
||||
.isEqualTo(HttpStatus.OK);
|
||||
Hello hello = response.getBody();
|
||||
assertThat(hello).as("hello was null").isNotNull();
|
||||
assertThat(hello).as("first hello didn't match")
|
||||
.isEqualTo(new Hello("hello world via response"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullBodyDecompress() {
|
||||
ResponseEntity<Hello> response = testClient().getNullResponse();
|
||||
assertThat(response).as("response was null").isNotNull();
|
||||
assertThat(response.getStatusCode()).as("wrong status code")
|
||||
.isEqualTo(HttpStatus.OK);
|
||||
Hello hello = response.getBody();
|
||||
assertThat(hello).as("hello was not null").isNull();
|
||||
assertThat(hello).as("null hello didn't match").isEqualTo(null);
|
||||
}
|
||||
|
||||
private static class Hello {
|
||||
|
||||
private String message;
|
||||
|
||||
Hello() {
|
||||
}
|
||||
|
||||
Hello(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Hello that = (Hello) o;
|
||||
return Objects.equals(message, that.message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected interface TestClient {
|
||||
|
||||
@GetMapping("/helloGzipResponse")
|
||||
ResponseEntity<Hello> getGzipResponse();
|
||||
|
||||
@GetMapping("/nullGzipResponse")
|
||||
ResponseEntity<Hello> getNullResponse();
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
@RestController
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
protected static class Application implements TestClient {
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Hello> getGzipResponse() {
|
||||
return ResponseEntity.ok(new Hello("hello world via response"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Hello> getNullResponse() {
|
||||
return ResponseEntity.ok(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user