Skip to content

Commit 57f588b

Browse files
kdavisk6velo
authored andcommitted
Reactive Wrapper Support (OpenFeign#795)
Adding support for Reactive Streams `Publisher` return types. Support is provided through the `ReactiveInvocationHandler` and follows a similar pattern used by `feign-hystrix`. Each method invocation is wrapped in a `Callable`, which is then wrapped into the appropriate Reactive Streams `Publisher`, as defined in the builder and the return type of the method. This approach is not "reactive all the way down". The requests are still executed via a regular `Client` and are synchronous. However, it is possible to still take advantage of the backpressure, scheduling, and functional support provided by the library implementations. Limitations: Streams are not supported and Iterable responses are not treated reactively. Iterables must be explicitly cast into a reactive type. Reworked Builders and removed the need for the enumerator
1 parent 40d1973 commit 57f588b

14 files changed

Lines changed: 1172 additions & 2 deletions

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@
2929
* @author Bob Lee
3030
* @author Jesse Wilson
3131
*/
32-
final class Types {
32+
public final class Types {
3333

3434
private static final Type[] EMPTY_TYPE_ARRAY = new Type[0];
3535

3636
private Types() {
3737
// No instances.
3838
}
3939

40-
static Class<?> getRawType(Type type) {
40+
public static Class<?> getRawType(Type type) {
4141
if (type instanceof Class<?>) {
4242
// Type is a normal class.
4343
return (Class<?>) type;

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
<module>ribbon</module>
7474
<module>sax</module>
7575
<module>slf4j</module>
76+
<module>reactive</module>
7677
<module>example-github</module>
7778
<module>example-wikipedia</module>
7879
<!-- remove after feign 10.0 release -->

reactive/README.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
Reactive Streams Wrapper
2+
---
3+
4+
This module wraps Feign's http requests in a [Reactive Streams](https://reactive-streams.org)
5+
Publisher, enabling the use of Reactive Stream `Publisher` return types. Supported Reactive Streams implementations are:
6+
7+
* [Reactor](https://project-reactor.org) (`Mono` and `Flux`)
8+
* [ReactiveX (RxJava)](https://reactivex.io) (`Flowable` only)
9+
10+
To use these wrappers, add the `feign-reactive-wrappers` module, and your desired `reactive-streams`
11+
implementation to your classpath. Then configure Feign to use the reactive streams wrappers.
12+
13+
```java
14+
public interface GitHubReactor {
15+
16+
@RequestLine("GET /repos/{owner}/{repo}/contributors")
17+
Flux<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
18+
19+
class Contributor {
20+
String login;
21+
22+
public Contributor(String login) {
23+
this.login = login;
24+
}
25+
}
26+
}
27+
28+
public class ExampleReactor {
29+
public static void main(String args[]) {
30+
GitHubReactor gitHub = ReactorFeign.builder()
31+
.target(GitHubReactor.class, "https://api.github.com");
32+
33+
List<Contributor> contributors = gitHub.contributors("OpenFeign", "feign")
34+
.map(Contributor::new)
35+
.collect(Collectors.toList())
36+
.block();
37+
}
38+
}
39+
40+
public interface GitHubReactiveX {
41+
42+
@RequestLine("GET /repos/{owner}/{repo}/contributors")
43+
Flowable<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
44+
45+
class Contributor {
46+
String login;
47+
48+
public Contributor(String login) {
49+
this.login = login;
50+
}
51+
}
52+
}
53+
54+
public class ExampleRxJava2 {
55+
public static void main(String args[]) {
56+
GitHubReactiveX gitHub = RxJavaFeign.builder()
57+
.target(GitHub.class, "https://api.github.com");
58+
59+
List<Contributor> contributors = gitHub.contributors("OpenFeign", "feign")
60+
.map(Contributor::new)
61+
.collect(Collectors.toList())
62+
.block();
63+
}
64+
}
65+
66+
```
67+
68+
Considerations
69+
---
70+
71+
These wrappers are not *reactive all the way down*, given that Feign generated requests are
72+
synchronous. Requests still block, but execution is controlled by the `Publisher` and their
73+
related `Scheduler`. While this may not be ideal in terms of a fully reactive application, providing these
74+
wrappers provide an intermediate upgrade path for Feign.
75+
76+
### Streaming
77+
78+
Methods that return `java.util.streams` Types are not supported. Responses are read fully,
79+
the wrapped in the appropriate reactive wrappers.
80+
81+
### Iterable and Collections responses
82+
83+
Due to the Synchronous nature of Feign requests, methods that return `Iterable` types must specify the collection
84+
in the `Publisher`. For `Reactor` types, this limits the use of `Flux` as a response type. If you
85+
want to use `Flux`, you will need to manually convert the `Mono` or `Iterable` response types into
86+
`Flux` using the `fromIterable` method.
87+
88+
89+
```java
90+
public interface GitHub {
91+
92+
@RequestLine("GET /repos/{owner}/{repo}/contributors")
93+
Mono<List<Contributor>> contributors(@Param("owner") String owner, @Param("repo") String repo);
94+
95+
class Contributor {
96+
String login;
97+
98+
public Contributor(String login) {
99+
this.login = login;
100+
}
101+
}
102+
}
103+
104+
public class ExampleApplication {
105+
public static void main(String[] args) {
106+
GitHub gitHub = ReactorFeign.builder()
107+
.target(GitHub.class, "https://api.github.com");
108+
109+
Mono<List<Contributor>> contributors = gitHub.contributors("OpenFeign", "feign");
110+
Flux<Contributor> contributorFlux = Flux.fromIterable(contributors.block());
111+
}
112+
}
113+
```

reactive/pom.xml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
4+
Copyright 2012-2018 The Feign Authors
5+
6+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7+
in compliance with the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software distributed under the License
12+
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13+
or implied. See the License for the specific language governing permissions and limitations under
14+
the License.
15+
16+
-->
17+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
18+
<modelVersion>4.0.0</modelVersion>
19+
<parent>
20+
<groupId>io.github.openfeign</groupId>
21+
<artifactId>parent</artifactId>
22+
<version>10.0.2-SNAPSHOT</version>
23+
</parent>
24+
<artifactId>feign-reactive-wrappers</artifactId>
25+
26+
<name>Feign Reactive Wrappers</name>
27+
<description>Reactive Wrapper for Feign Clients</description>
28+
29+
<properties>
30+
<main.basedir>${project.basedir}/..</main.basedir>
31+
<reactor.version>3.1.8.RELEASE</reactor.version>
32+
<reactive.streams.version>1.0.2</reactive.streams.version>
33+
<reactivex.version>2.2.2</reactivex.version>
34+
<mockito.version>1.9.5</mockito.version>
35+
</properties>
36+
37+
<dependencies>
38+
<dependency>
39+
<groupId>io.github.openfeign</groupId>
40+
<artifactId>feign-core</artifactId>
41+
<version>${project.version}</version>
42+
</dependency>
43+
<dependency>
44+
<groupId>org.reactivestreams</groupId>
45+
<artifactId>reactive-streams</artifactId>
46+
<version>${reactive.streams.version}</version>
47+
</dependency>
48+
<dependency>
49+
<groupId>io.projectreactor</groupId>
50+
<artifactId>reactor-core</artifactId>
51+
<version>${reactor.version}</version>
52+
<scope>provided</scope>
53+
<optional>true</optional>
54+
</dependency>
55+
<dependency>
56+
<groupId>io.reactivex.rxjava2</groupId>
57+
<artifactId>rxjava</artifactId>
58+
<version>${reactivex.version}</version>
59+
<scope>provided</scope>
60+
<optional>true</optional>
61+
</dependency>
62+
<dependency>
63+
<groupId>org.mockito</groupId>
64+
<artifactId>mockito-all</artifactId>
65+
<version>${mockito.version}</version>
66+
<scope>test</scope>
67+
</dependency>
68+
<dependency>
69+
<groupId>io.github.openfeign</groupId>
70+
<artifactId>feign-jackson</artifactId>
71+
<version>${project.version}</version>
72+
<scope>test</scope>
73+
</dependency>
74+
<dependency>
75+
<groupId>io.github.openfeign</groupId>
76+
<artifactId>feign-okhttp</artifactId>
77+
<version>${project.version}</version>
78+
<scope>test</scope>
79+
</dependency>
80+
<dependency>
81+
<groupId>io.github.openfeign</groupId>
82+
<artifactId>feign-jaxrs</artifactId>
83+
<version>${project.version}</version>
84+
<scope>test</scope>
85+
</dependency>
86+
<dependency>
87+
<groupId>com.squareup.okhttp3</groupId>
88+
<artifactId>mockwebserver</artifactId>
89+
<scope>test</scope>
90+
</dependency>
91+
</dependencies>
92+
93+
</project>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Copyright 2012-2018 The Feign Authors
3+
*
4+
* <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5+
* except in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* <p>http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* <p>Unless required by applicable law or agreed to in writing, software distributed under the
10+
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
* express or implied. See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package feign.reactive;
15+
16+
import feign.Contract;
17+
import feign.MethodMetadata;
18+
import feign.Types;
19+
import java.lang.reflect.ParameterizedType;
20+
import java.lang.reflect.Type;
21+
import java.util.Arrays;
22+
import java.util.List;
23+
import java.util.stream.Stream;
24+
import org.reactivestreams.Publisher;
25+
26+
public class ReactiveDelegatingContract implements Contract {
27+
28+
private final Contract delegate;
29+
30+
ReactiveDelegatingContract(Contract delegate) {
31+
this.delegate = delegate;
32+
}
33+
34+
@Override
35+
public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
36+
List<MethodMetadata> methodsMetadata = this.delegate.parseAndValidatateMetadata(targetType);
37+
38+
for (final MethodMetadata metadata : methodsMetadata) {
39+
final Type type = metadata.returnType();
40+
if (!isReactive(type)) {
41+
throw new IllegalArgumentException(
42+
String.format(
43+
"Method %s of contract %s doesn't returns a org.reactivestreams.Publisher",
44+
metadata.configKey(), targetType.getSimpleName()));
45+
}
46+
47+
/*
48+
* we will need to change the return type of the method to match the return type contained
49+
* within the Publisher
50+
*/
51+
Type[] actualTypes = ((ParameterizedType) type).getActualTypeArguments();
52+
if (actualTypes.length > 1) {
53+
throw new IllegalStateException("Expected only one contained type.");
54+
} else {
55+
Class<?> actual = Types.getRawType(actualTypes[0]);
56+
if (Stream.class.isAssignableFrom(actual)) {
57+
throw new IllegalArgumentException(
58+
"Streams are not supported when using Reactive Wrappers");
59+
}
60+
metadata.returnType(actualTypes[0]);
61+
}
62+
}
63+
64+
return methodsMetadata;
65+
}
66+
67+
/**
68+
* Ensure that the type provided implements a Reactive Streams Publisher.
69+
*
70+
* @param type to inspect.
71+
* @return true if the type implements the Reactive Streams Publisher specification.
72+
*/
73+
private boolean isReactive(Type type) {
74+
if (!ParameterizedType.class.isAssignableFrom(type.getClass())) {
75+
return false;
76+
}
77+
ParameterizedType parameterizedType = (ParameterizedType) type;
78+
Type raw = parameterizedType.getRawType();
79+
return Arrays.asList(((Class) raw).getInterfaces()).contains(Publisher.class);
80+
}
81+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Copyright 2012-2018 The Feign Authors
3+
*
4+
* <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5+
* except in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* <p>http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* <p>Unless required by applicable law or agreed to in writing, software distributed under the
10+
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
* express or implied. See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package feign.reactive;
15+
16+
import feign.Contract;
17+
import feign.Feign;
18+
19+
abstract class ReactiveFeign {
20+
21+
public static class Builder extends Feign.Builder {
22+
23+
private Contract contract = new Contract.Default();
24+
25+
/**
26+
* Extend the current contract to support Reactive Stream return types.
27+
*
28+
* @param contract to extend.
29+
* @return a Builder for chaining.
30+
*/
31+
@Override
32+
public Builder contract(Contract contract) {
33+
this.contract = contract;
34+
return this;
35+
}
36+
37+
/**
38+
* Build the Feign instance.
39+
*
40+
* @return a new Feign Instance.
41+
*/
42+
@Override
43+
public Feign build() {
44+
if (!(this.contract instanceof ReactiveDelegatingContract)) {
45+
super.contract(new ReactiveDelegatingContract(this.contract));
46+
} else {
47+
super.contract(this.contract);
48+
}
49+
return super.build();
50+
}
51+
52+
@Override
53+
public Feign.Builder doNotCloseAfterDecode() {
54+
throw new UnsupportedOperationException("Streaming Decoding is not supported.");
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)