Skip to content

Commit 861bb80

Browse files
phymbertvelo
authored andcommitted
Support of Java 8 Stream Decoder and Jackson Iterator Decoder (OpenFeign#651)
* Support of Java 8 Stream Decoder and Jackson Iterator Decoder Signed-off-by: phymbert <pierrick.hymbert@gmail.com> * Removed class javadoc to make license plugin happy Signed-off-by: phymbert <pierrick.hymbert@gmail.com> * Fixed build failed cause of license missing Signed-off-by: phymbert <pierrick.hymbert@gmail.com> * - smaller highlighted changelog - make decoder type implementation final - change inner types and constructors visibility to package private - fix non static inner class - remove useless Factory inner class - unit test JacksonIterator Signed-off-by: phymbert <pierrick.hymbert@gmail.com> * - Revert deleted groupId tag in benchmark - Fix code style on StreamDecoder - Add unit test to verify iterator is closed if stream is closed - Remove any characteristics to the returned stream Signed-off-by: phymbert <pierrick.hymbert@gmail.com> * Benchmark: - updated with latest factory methods - do not duplicate groupId Signed-off-by: phymbert <pierrick.hymbert@gmail.com>
1 parent 9c72569 commit 861bb80

11 files changed

Lines changed: 830 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
### Version 9.6
22
* Feign builder now supports flag `doNotCloseAfterDecode` to support lazy iteration of responses.
3+
* Adds `JacksonIteratorDecoder` and `StreamDecoder` to decode responses as `java.util.Iterator` or `java.util.stream.Stream`.
34

45
### Version 9.5.1
56
* When specified, Content-Type header is now included on OkHttp requests lacking a body.

benchmark/pom.xml

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,41 @@
2424
<version>9</version>
2525
</parent>
2626

27-
<!-- TODO: change group id when 9.0 is released -->
28-
<groupId>com.netflix.feign</groupId>
27+
<groupId>io.github.openfeign</groupId>
2928
<artifactId>feign-benchmark</artifactId>
30-
<packaging>jar</packaging>
31-
<version>8.1.0-SNAPSHOT</version>
29+
<version>9.6.0-SNAPSHOT</version>
3230
<name>Feign Benchmark (JMH)</name>
3331

3432
<properties>
35-
<jmh.version>1.11.2</jmh.version>
33+
<jmh.version>1.20</jmh.version>
34+
<!-- override default bytecode version for src/main from parent pom -->
35+
<main.java.version>1.8</main.java.version>
36+
<main.signature.artifact>java18</main.signature.artifact>
37+
<maven.compiler.source>1.8</maven.compiler.source>
38+
<maven.compiler.target>1.8</maven.compiler.target>
3639
</properties>
3740

3841
<dependencies>
3942
<dependency>
40-
<groupId>com.netflix.feign</groupId>
43+
<groupId>${project.groupId}</groupId>
4144
<artifactId>feign-core</artifactId>
4245
<version>${project.version}</version>
4346
</dependency>
4447
<dependency>
45-
<groupId>com.netflix.feign</groupId>
48+
<groupId>${project.groupId}</groupId>
4649
<artifactId>feign-okhttp</artifactId>
4750
<version>${project.version}</version>
4851
</dependency>
52+
<dependency>
53+
<groupId>${project.groupId}</groupId>
54+
<artifactId>feign-jackson</artifactId>
55+
<version>${project.version}</version>
56+
</dependency>
57+
<dependency>
58+
<groupId>${project.groupId}</groupId>
59+
<artifactId>feign-java8</artifactId>
60+
<version>${project.version}</version>
61+
</dependency>
4962
<dependency>
5063
<groupId>com.squareup.okhttp</groupId>
5164
<artifactId>mockwebserver</artifactId>
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* Copyright 2012-2018 The Feign Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
package feign.benchmark;
15+
16+
import com.fasterxml.jackson.core.type.TypeReference;
17+
import feign.Response;
18+
import feign.Util;
19+
import feign.codec.Decoder;
20+
import feign.jackson.JacksonDecoder;
21+
import feign.jackson.JacksonIteratorDecoder;
22+
import feign.stream.StreamDecoder;
23+
import org.openjdk.jmh.annotations.*;
24+
25+
import java.lang.reflect.Type;
26+
import java.util.Collection;
27+
import java.util.Collections;
28+
import java.util.Iterator;
29+
import java.util.List;
30+
import java.util.concurrent.TimeUnit;
31+
import java.util.stream.Stream;
32+
33+
/**
34+
* This test shows up how fast different json array response processing implementations are.
35+
*/
36+
@State(Scope.Thread)
37+
public class DecoderIteratorsBenchmark {
38+
39+
@Param({"list", "iterator", "stream"})
40+
private String api;
41+
42+
@Param({"10", "100"})
43+
private String size;
44+
45+
private Response response;
46+
47+
private Decoder decoder;
48+
private Type type;
49+
50+
@Benchmark
51+
@Warmup(iterations = 5, time = 1)
52+
@Measurement(iterations = 10, time = 1)
53+
@Fork(3)
54+
@BenchmarkMode(Mode.AverageTime)
55+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
56+
public void decode() throws Exception {
57+
fetch(decoder.decode(response, type));
58+
}
59+
60+
@SuppressWarnings("unchecked")
61+
private void fetch(Object o) {
62+
Iterator<Car> cars;
63+
64+
if (o instanceof Collection) {
65+
cars = ((Collection<Car>) o).iterator();
66+
} else if (o instanceof Stream) {
67+
cars = ((Stream<Car>) o).iterator();
68+
} else {
69+
cars = (Iterator<Car>) o;
70+
}
71+
72+
while (cars.hasNext()) {
73+
cars.next();
74+
}
75+
}
76+
77+
@Setup(Level.Invocation)
78+
public void buildResponse() {
79+
response = Response.builder()
80+
.status(200)
81+
.reason("OK")
82+
.headers(Collections.emptyMap())
83+
.body(carsJson(Integer.valueOf(size)), Util.UTF_8)
84+
.build();
85+
}
86+
87+
@Setup(Level.Trial)
88+
public void buildDecoder() {
89+
switch (api) {
90+
case "list":
91+
decoder = new JacksonDecoder();
92+
type = new TypeReference<List<Car>>() {
93+
}.getType();
94+
break;
95+
case "iterator":
96+
decoder = JacksonIteratorDecoder.create();
97+
type = new TypeReference<Iterator<Car>>() {
98+
}.getType();
99+
break;
100+
case "stream":
101+
decoder = StreamDecoder.create(JacksonIteratorDecoder.create());
102+
type = new TypeReference<Stream<Car>>() {
103+
}.getType();
104+
break;
105+
default:
106+
throw new IllegalStateException("Unknown api: " + api);
107+
}
108+
}
109+
110+
private String carsJson(int count) {
111+
String car = "{\"name\":\"c4\",\"manufacturer\":\"Citroën\"}";
112+
StringBuilder builder = new StringBuilder("[");
113+
builder.append(car);
114+
for (int i = 1; i < count; i++) {
115+
builder.append(",").append(car);
116+
}
117+
return builder.append("]").toString();
118+
}
119+
120+
static class Car {
121+
public String name;
122+
public String manufacturer;
123+
}
124+
}

benchmark/src/main/java/feign/benchmark/RealRequestBenchmarks.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public rx.Observable handle(HttpServerRequest<ByteBuf> request,
6464
});
6565
server.start();
6666
client = new OkHttpClient();
67-
client.setRetryOnConnectionFailure(false);
67+
client.retryOnConnectionFailure();
6868
okFeign = Feign.builder()
6969
.client(new feign.okhttp.OkHttpClient(client))
7070
.target(FeignTestInterface.class, "http://localhost:" + SERVER_PORT);
@@ -82,8 +82,8 @@ public void tearDown() throws InterruptedException {
8282
* How fast can we execute get commands synchronously?
8383
*/
8484
@Benchmark
85-
public com.squareup.okhttp.Response query_baseCaseUsingOkHttp() throws IOException {
86-
com.squareup.okhttp.Response result = client.newCall(queryRequest).execute();
85+
public okhttp3.Response query_baseCaseUsingOkHttp() throws IOException {
86+
okhttp3.Response result = client.newCall(queryRequest).execute();
8787
result.body().close();
8888
return result;
8989
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* Copyright 2012-2018 The Feign Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
package feign.jackson;
15+
16+
import com.fasterxml.jackson.core.JsonParser;
17+
import com.fasterxml.jackson.core.JsonToken;
18+
import com.fasterxml.jackson.databind.*;
19+
import feign.Response;
20+
import feign.Util;
21+
import feign.codec.DecodeException;
22+
import feign.codec.Decoder;
23+
24+
import java.io.BufferedReader;
25+
import java.io.Closeable;
26+
import java.io.IOException;
27+
import java.io.Reader;
28+
import java.lang.reflect.ParameterizedType;
29+
import java.lang.reflect.Type;
30+
import java.util.Collections;
31+
import java.util.Iterator;
32+
33+
import static feign.Util.ensureClosed;
34+
35+
/**
36+
* Jackson decoder which return a closeable iterator.
37+
* Returned iterator auto-close the {@code Response} when it reached json array end or failed to parse stream.
38+
* If this iterator is not fetched till the end, it has to be casted to {@code Closeable} and explicity {@code Closeable#close} by the consumer.
39+
* <p>
40+
* <p>
41+
* <p>Example: <br>
42+
* <pre><code>
43+
* Feign.builder()
44+
* .decoder(JacksonIteratorDecoder.create())
45+
* .doNotCloseAfterDecode() // Required to fetch the iterator after the response is processed, need to be close
46+
* .target(GitHub.class, "https://api.github.com");
47+
* interface GitHub {
48+
* {@literal @}RequestLine("GET /repos/{owner}/{repo}/contributors")
49+
* Iterator<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
50+
* }</code></pre>
51+
*/
52+
public final class JacksonIteratorDecoder implements Decoder {
53+
54+
private final ObjectMapper mapper;
55+
56+
JacksonIteratorDecoder(ObjectMapper mapper) {
57+
this.mapper = mapper;
58+
}
59+
60+
@Override
61+
public Object decode(Response response, Type type) throws IOException {
62+
if (response.status() == 404) return Util.emptyValueOf(type);
63+
if (response.body() == null) return null;
64+
Reader reader = response.body().asReader();
65+
if (!reader.markSupported()) {
66+
reader = new BufferedReader(reader, 1);
67+
}
68+
try {
69+
// Read the first byte to see if we have any data
70+
reader.mark(1);
71+
if (reader.read() == -1) {
72+
return null; // Eagerly returning null avoids "No content to map due to end-of-input"
73+
}
74+
reader.reset();
75+
return new JacksonIterator<Object>(actualIteratorTypeArgument(type), mapper, response, reader);
76+
} catch (RuntimeJsonMappingException e) {
77+
if (e.getCause() != null && e.getCause() instanceof IOException) {
78+
throw IOException.class.cast(e.getCause());
79+
}
80+
throw e;
81+
}
82+
}
83+
84+
private static Type actualIteratorTypeArgument(Type type) {
85+
if (!(type instanceof ParameterizedType)) {
86+
throw new IllegalArgumentException("Not supported type " + type.toString());
87+
}
88+
ParameterizedType parameterizedType = (ParameterizedType) type;
89+
if (!Iterator.class.equals(parameterizedType.getRawType())) {
90+
throw new IllegalArgumentException("Not an iterator type " + parameterizedType.getRawType().toString());
91+
}
92+
return ((ParameterizedType) type).getActualTypeArguments()[0];
93+
}
94+
95+
public static JacksonIteratorDecoder create() {
96+
return create(Collections.<Module>emptyList());
97+
}
98+
99+
public static JacksonIteratorDecoder create(Iterable<Module> modules) {
100+
return new JacksonIteratorDecoder(new ObjectMapper()
101+
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
102+
.registerModules(modules));
103+
}
104+
105+
public static JacksonIteratorDecoder create(ObjectMapper objectMapper) {
106+
return new JacksonIteratorDecoder(objectMapper);
107+
}
108+
109+
static final class JacksonIterator<T> implements Iterator<T>, Closeable {
110+
private final Response response;
111+
private final JsonParser parser;
112+
private final ObjectReader objectReader;
113+
114+
private T current;
115+
116+
JacksonIterator(Type type, ObjectMapper mapper, Response response, Reader reader)
117+
throws IOException {
118+
this.response = response;
119+
this.parser = mapper.getFactory().createParser(reader);
120+
this.objectReader = mapper.reader().forType(mapper.constructType(type));
121+
}
122+
123+
@Override
124+
public boolean hasNext() {
125+
try {
126+
JsonToken jsonToken = parser.nextToken();
127+
if (jsonToken == null) {
128+
return false;
129+
}
130+
131+
if (jsonToken == JsonToken.START_ARRAY) {
132+
jsonToken = parser.nextToken();
133+
}
134+
135+
if (jsonToken == JsonToken.END_ARRAY) {
136+
current = null;
137+
ensureClosed(this);
138+
return false;
139+
}
140+
141+
current = objectReader.readValue(parser);
142+
} catch (IOException e) {
143+
// Input Stream closed automatically by parser
144+
throw new DecodeException(e.getMessage(), e);
145+
}
146+
return current != null;
147+
}
148+
149+
@Override
150+
public T next() {
151+
return current;
152+
}
153+
154+
@Override
155+
public void remove() {
156+
throw new UnsupportedOperationException();
157+
}
158+
159+
@Override
160+
public void close() throws IOException {
161+
ensureClosed(this.response);
162+
}
163+
}
164+
}

0 commit comments

Comments
 (0)