Skip to content

Commit 5818738

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 77fd2c9 commit 5818738

File tree

11 files changed

+359
-69
lines changed

11 files changed

+359
-69
lines changed

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.util.Arrays;
2424
import java.util.NoSuchElementException;
2525

26+
import static feign.Util.checkState;
27+
2628
/**
2729
* Static methods for working with types.
2830
*
@@ -325,6 +327,40 @@ 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+
* <p/>
334+
* Implementation copied from {@code
335+
* retrofit.RestMethodInfo}.
336+
*
337+
* @param genericContext Ex. {@link java.lang.reflect.Field#getGenericType()}
338+
* @param supertype Ex. {@code Decoder.class}
339+
* @return in the example above, the type parameter of {@code Decoder}.
340+
* @throws IllegalStateException if {@code supertype} cannot be resolved into a parameterized type
341+
* using {@code context}.
342+
*/
343+
public static Type resolveLastTypeParameter(Type genericContext, Class<?> supertype)
344+
throws IllegalStateException {
345+
Type resolvedSuperType =
346+
Types.getSupertype(genericContext, Types.getRawType(genericContext), supertype);
347+
checkState(resolvedSuperType instanceof ParameterizedType,
348+
"could not resolve %s into a parameterized type %s",
349+
genericContext, supertype);
350+
Type[] types = ParameterizedType.class.cast(resolvedSuperType).getActualTypeArguments();
351+
for (int i = 0; i < types.length; i++) {
352+
Type type = types[i];
353+
if (type instanceof WildcardType) {
354+
types[i] = ((WildcardType) type).getUpperBounds()[0];
355+
}
356+
}
357+
return types[types.length - 1];
358+
}
359+
360+
public static ParameterizedType parameterize(Class<?> rawClass, Type... typeArguments) {
361+
return new ParameterizedTypeImpl(rawClass.getEnclosingClass(), rawClass, typeArguments);
362+
}
363+
328364
static final class ParameterizedTypeImpl implements ParameterizedType {
329365

330366
private final Type ownerType;

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

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -215,33 +215,12 @@ public static void ensureClosed(Closeable closeable) {
215215
}
216216

217217
/**
218-
* Resolves the last type parameter of the parameterized {@code supertype}, based on the {@code
219-
* genericContext}, into its upper bounds.
220-
* <p/>
221-
* Implementation copied from {@code
222-
* retrofit.RestMethodInfo}.
223-
*
224-
* @param genericContext Ex. {@link java.lang.reflect.Field#getGenericType()}
225-
* @param supertype Ex. {@code Decoder.class}
226-
* @return in the example above, the type parameter of {@code Decoder}.
227-
* @throws IllegalStateException if {@code supertype} cannot be resolved into a parameterized type
228-
* using {@code context}.
218+
* Moved to {@code feign.Types.resolveLastTypeParameter}
229219
*/
220+
@Deprecated
230221
public static Type resolveLastTypeParameter(Type genericContext, Class<?> supertype)
231222
throws IllegalStateException {
232-
Type resolvedSuperType =
233-
Types.getSupertype(genericContext, Types.getRawType(genericContext), supertype);
234-
checkState(resolvedSuperType instanceof ParameterizedType,
235-
"could not resolve %s into a parameterized type %s",
236-
genericContext, supertype);
237-
Type[] types = ParameterizedType.class.cast(resolvedSuperType).getActualTypeArguments();
238-
for (int i = 0; i < types.length; i++) {
239-
Type type = types[i];
240-
if (type instanceof WildcardType) {
241-
types[i] = ((WildcardType) type).getUpperBounds()[0];
242-
}
243-
}
244-
return types[types.length - 1];
223+
return Types.resolveLastTypeParameter(genericContext, supertype);
245224
}
246225

247226
/**

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
@@ -55,7 +55,7 @@ public List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType) {
5555
throw new IllegalArgumentException(
5656
"Streams are not supported when using Reactive Wrappers");
5757
}
58-
metadata.returnType(actualTypes[0]);
58+
metadata.returnType(type);
5959
}
6060
}
6161

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

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

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

0 commit comments

Comments
 (0)