Skip to content

Commit 45ac829

Browse files
authored
Flux type response should be corresponding to List (#2199)
* Refactoring: Move resolveLastTypeParameter from Util to Types * Add ReactiveDecoder * Update code as per suggestions * Update code as per suggestions * Refactoring * Add tests
1 parent ecd666c commit 45ac829

File tree

11 files changed

+364
-75
lines changed

11 files changed

+364
-75
lines changed

core/src/main/java/feign/Types.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
*/
1414
package feign;
1515

16+
import static feign.Util.checkState;
17+
1618
import java.lang.reflect.Array;
1719
import java.lang.reflect.GenericArrayType;
1820
import java.lang.reflect.GenericDeclaration;
@@ -325,6 +327,41 @@ public static Type resolveReturnType(Type baseType, Type overridingType) {
325327
return baseType;
326328
}
327329

330+
/**
331+
* Resolves the last type parameter of the parameterized {@code supertype}, based on the {@code
332+
* genericContext}, into its upper bounds.
333+
*
334+
* <p>Implementation copied from {@code retrofit.RestMethodInfo}.
335+
*
336+
* @param genericContext Ex. {@link java.lang.reflect.Field#getGenericType()}
337+
* @param supertype Ex. {@code Decoder.class}
338+
* @return in the example above, the type parameter of {@code Decoder}.
339+
* @throws IllegalStateException if {@code supertype} cannot be resolved into a parameterized type
340+
* using {@code context}.
341+
*/
342+
public static Type resolveLastTypeParameter(Type genericContext, Class<?> supertype)
343+
throws IllegalStateException {
344+
Type resolvedSuperType =
345+
Types.getSupertype(genericContext, Types.getRawType(genericContext), supertype);
346+
checkState(
347+
resolvedSuperType instanceof ParameterizedType,
348+
"could not resolve %s into a parameterized type %s",
349+
genericContext,
350+
supertype);
351+
Type[] types = ParameterizedType.class.cast(resolvedSuperType).getActualTypeArguments();
352+
for (int i = 0; i < types.length; i++) {
353+
Type type = types[i];
354+
if (type instanceof WildcardType) {
355+
types[i] = ((WildcardType) type).getUpperBounds()[0];
356+
}
357+
}
358+
return types[types.length - 1];
359+
}
360+
361+
public static ParameterizedType parameterize(Class<?> rawClass, Type... typeArguments) {
362+
return new ParameterizedTypeImpl(rawClass.getEnclosingClass(), rawClass, typeArguments);
363+
}
364+
328365
static final class ParameterizedTypeImpl implements ParameterizedType {
329366

330367
private final Type ownerType;

core/src/main/java/feign/Util.java

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@
2626
import java.lang.reflect.Field;
2727
import java.lang.reflect.Method;
2828
import java.lang.reflect.Modifier;
29-
import java.lang.reflect.ParameterizedType;
3029
import java.lang.reflect.Type;
31-
import java.lang.reflect.WildcardType;
3230
import java.nio.Buffer;
3331
import java.nio.ByteBuffer;
3432
import java.nio.CharBuffer;
@@ -185,35 +183,11 @@ public static void ensureClosed(Closeable closeable) {
185183
}
186184
}
187185

188-
/**
189-
* Resolves the last type parameter of the parameterized {@code supertype}, based on the {@code
190-
* genericContext}, into its upper bounds.
191-
*
192-
* <p>Implementation copied from {@code retrofit.RestMethodInfo}.
193-
*
194-
* @param genericContext Ex. {@link java.lang.reflect.Field#getGenericType()}
195-
* @param supertype Ex. {@code Decoder.class}
196-
* @return in the example above, the type parameter of {@code Decoder}.
197-
* @throws IllegalStateException if {@code supertype} cannot be resolved into a parameterized type
198-
* using {@code context}.
199-
*/
186+
/** Moved to {@code feign.Types.resolveLastTypeParameter} */
187+
@Deprecated
200188
public static Type resolveLastTypeParameter(Type genericContext, Class<?> supertype)
201189
throws IllegalStateException {
202-
Type resolvedSuperType =
203-
Types.getSupertype(genericContext, Types.getRawType(genericContext), supertype);
204-
checkState(
205-
resolvedSuperType instanceof ParameterizedType,
206-
"could not resolve %s into a parameterized type %s",
207-
genericContext,
208-
supertype);
209-
Type[] types = ParameterizedType.class.cast(resolvedSuperType).getActualTypeArguments();
210-
for (int i = 0; i < types.length; i++) {
211-
Type type = types[i];
212-
if (type instanceof WildcardType) {
213-
types[i] = ((WildcardType) type).getUpperBounds()[0];
214-
}
215-
}
216-
return types[types.length - 1];
190+
return Types.resolveLastTypeParameter(genericContext, supertype);
217191
}
218192

219193
/**

reactive/README.md

Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,34 @@ implementation to your classpath. Then configure Feign to use the reactive stre
1414
public interface GitHubReactor {
1515

1616
@RequestLine("GET /repos/{owner}/{repo}/contributors")
17-
Flux<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
17+
Flux<Contributor> contributorsFlux(@Param("owner") String owner, @Param("repo") String repo);
18+
19+
@RequestLine("GET /repos/{owner}/{repo}/contributors")
20+
Mono<List<Contributor>> contributorsMono(@Param("owner") String owner, @Param("repo") String repo);
1821

1922
class Contributor {
20-
String login;
21-
22-
public Contributor(String login) {
23-
this.login = login;
24-
}
23+
String login;
24+
25+
public String getLogin() {
26+
return login;
27+
}
28+
29+
public void setLogin(String login) {
30+
this.login = login;
31+
}
2532
}
2633
}
2734

2835
public class ExampleReactor {
2936
public static void main(String args[]) {
30-
GitHubReactor gitHub = ReactorFeign.builder()
37+
GitHubReactor gitHub = ReactorFeign.builder()
38+
.decoder(new ReactorDecoder(new JacksonDecoder()))
3139
.target(GitHubReactor.class, "https://api.github.com");
3240

33-
List<Contributor> contributors = gitHub.contributors("OpenFeign", "feign")
34-
.collect(Collectors.toList())
41+
List<GitHubReactor.Contributor> contributorsFromFlux = gitHub.contributorsFlux("OpenFeign", "feign")
42+
.collectList()
43+
.block();
44+
List<GitHubReactor.Contributor> contributorsFromMono = gitHub.contributorsMono("OpenFeign", "feign")
3545
.block();
3646
}
3747
}
@@ -52,7 +62,8 @@ public interface GitHubReactiveX {
5262

5363
public class ExampleRxJava2 {
5464
public static void main(String args[]) {
55-
GitHubReactiveX gitHub = RxJavaFeign.builder()
65+
GitHubReactiveX gitHub = RxJavaFeign.builder()
66+
.decoder(new RxJavaDecoder(new JacksonDecoder()))
5667
.target(GitHub.class, "https://api.github.com");
5768

5869
List<Contributor> contributors = gitHub.contributors("OpenFeign", "feign")
@@ -79,33 +90,5 @@ the wrapped in the appropriate reactive wrappers.
7990
### Iterable and Collections responses
8091

8192
Due to the Synchronous nature of Feign requests, methods that return `Iterable` types must specify the collection
82-
in the `Publisher`. For `Reactor` types, this limits the use of `Flux` as a response type. If you
83-
want to use `Flux`, you will need to manually convert the `Mono` or `Iterable` response types into
84-
`Flux` using the `fromIterable` method.
85-
93+
in the `Publisher`. For `Reactor` types, this limits the use of `Flux` as a response type.
8694

87-
```java
88-
public interface GitHub {
89-
90-
@RequestLine("GET /repos/{owner}/{repo}/contributors")
91-
Mono<List<Contributor>> contributors(@Param("owner") String owner, @Param("repo") String repo);
92-
93-
class Contributor {
94-
String login;
95-
96-
public Contributor(String login) {
97-
this.login = login;
98-
}
99-
}
100-
}
101-
102-
public class ExampleApplication {
103-
public static void main(String[] args) {
104-
GitHub gitHub = ReactorFeign.builder()
105-
.target(GitHub.class, "https://api.github.com");
106-
107-
Mono<List<Contributor>> contributors = gitHub.contributors("OpenFeign", "feign");
108-
Flux<Contributor> contributorFlux = Flux.fromIterable(contributors.block());
109-
}
110-
}
111-
```

reactive/src/main/java/feign/reactive/ReactiveDelegatingContract.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType) {
5656
throw new IllegalArgumentException(
5757
"Streams are not supported when using Reactive Wrappers");
5858
}
59-
metadata.returnType(actualTypes[0]);
59+
metadata.returnType(type);
6060
}
6161
}
6262

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2012-2023 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.reactive;
15+
16+
import feign.FeignException;
17+
import feign.Response;
18+
import feign.Types;
19+
import feign.codec.Decoder;
20+
import java.io.IOException;
21+
import java.lang.reflect.Type;
22+
import java.util.List;
23+
import reactor.core.publisher.Flux;
24+
import reactor.core.publisher.Mono;
25+
26+
public class ReactorDecoder implements Decoder {
27+
28+
private final Decoder delegate;
29+
30+
public ReactorDecoder(Decoder decoder) {
31+
this.delegate = decoder;
32+
}
33+
34+
@Override
35+
public Object decode(Response response, Type type) throws IOException, FeignException {
36+
Class<?> rawType = Types.getRawType(type);
37+
if (rawType.isAssignableFrom(Mono.class)) {
38+
Type lastType = Types.resolveLastTypeParameter(type, Mono.class);
39+
return delegate.decode(response, lastType);
40+
}
41+
if (rawType.isAssignableFrom(Flux.class)) {
42+
Type lastType = Types.resolveLastTypeParameter(type, Flux.class);
43+
Type listType = Types.parameterize(List.class, lastType);
44+
return delegate.decode(response, listType);
45+
}
46+
47+
return delegate.decode(response, type);
48+
}
49+
}

reactive/src/main/java/feign/reactive/ReactorInvocationHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public class ReactorInvocationHandler extends ReactiveInvocationHandler {
3535
protected Publisher invoke(Method method, MethodHandler methodHandler, Object[] arguments) {
3636
Publisher<?> invocation = this.invokeMethod(methodHandler, arguments);
3737
if (Flux.class.isAssignableFrom(method.getReturnType())) {
38-
return Flux.from(invocation).subscribeOn(scheduler);
38+
return Flux.from(invocation).flatMapIterable(e -> (Iterable) e).subscribeOn(scheduler);
3939
} else if (Mono.class.isAssignableFrom(method.getReturnType())) {
4040
return Mono.from(invocation).subscribeOn(scheduler);
4141
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2012-2023 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.reactive;
15+
16+
import feign.FeignException;
17+
import feign.Response;
18+
import feign.Types;
19+
import feign.codec.Decoder;
20+
import io.reactivex.Flowable;
21+
import java.io.IOException;
22+
import java.lang.reflect.Type;
23+
24+
public class RxJavaDecoder implements Decoder {
25+
26+
private final Decoder delegate;
27+
28+
public RxJavaDecoder(Decoder decoder) {
29+
this.delegate = decoder;
30+
}
31+
32+
@Override
33+
public Object decode(Response response, Type type) throws IOException, FeignException {
34+
Class<?> rawType = Types.getRawType(type);
35+
if (rawType.isAssignableFrom(Flowable.class)) {
36+
Type lastType = Types.resolveLastTypeParameter(type, Flowable.class);
37+
return delegate.decode(response, lastType);
38+
}
39+
40+
return delegate.decode(response, type);
41+
}
42+
}

0 commit comments

Comments
 (0)