Add ProxyExchange feature for Webflux

This commit is contained in:
Dave Syer
2018-05-02 09:03:55 -04:00
parent b2db1e230d
commit 1656bbcb38
12 changed files with 1331 additions and 6 deletions

View File

@@ -844,11 +844,11 @@ TODO: document writing Custom Global Filters
TODO: document writing Custom Route Locators and Writers
== Building a Simple Gateway Using Spring MVC
== Building a Simple Gateway Using Spring MVC or Webflux
Spring Cloud Gateway provides a utility object called `ProxyExchange` which you can use inside a regular Spring MVC handler as a method parameter. It supports basic downstream HTTP exchanges via methods that mirror the HTTP verbs, or forwarding to a local handler via the `forward()` method.
Spring Cloud Gateway provides a utility object called `ProxyExchange` which you can use inside a regular Spring web handler as a method parameter. It supports basic downstream HTTP exchanges via methods that mirror the HTTP verbs. With MVC it also supports forwarding to a local handler via the `forward()` method. To use the `ProxyExchange` just include the right module in your classpath (either `spring-cloud-gateway-mvc` or `spring-cloud-gateway-webflux`).
Example (proxying a request to "/test" downstream to a remote server):
MVC example (proxying a request to "/test" downstream to a remote server):
```java
@RestController
@@ -859,7 +859,25 @@ public class GatewaySampleApplication {
private URI home;
@GetMapping("/test")
public ResponseEntity<?> proxy(ProxyExchange<Object> proxy) throws Exception {
public ResponseEntity<?> proxy(ProxyExchange<byte[]> proxy) throws Exception {
return proxy.uri(home.toString() + "/image/png").get();
}
}
```
The same thing with Webflux:
```java
@RestController
@SpringBootApplication
public class GatewaySampleApplication {
@Value("${remote.home}")
private URI home;
@GetMapping("/test")
public Mono<ResponseEntity<?>> proxy(ProxyExchange<byte[]> proxy) throws Exception {
return proxy.uri(home.toString() + "/image/png").get();
}
@@ -870,13 +888,13 @@ There are convenience methods on the `ProxyExchange` to enable the handler metho
```java
@GetMapping("/proxy/path/**")
public ResponseEntity<?> proxyPath(ProxyExchange<?> proxy) throws Exception {
public ResponseEntity<?> proxyPath(ProxyExchange<byte[]> proxy) throws Exception {
String path = proxy.path("/proxy/path/");
return proxy.uri(home.toString() + "/foos/" + path).get();
}
```
All the features of Spring MVC are available to Gateway handler methods. So you can inject request headers and query parameters, for instance, and you can constrain the incoming requests with declarations in the mapping annotation. See the documentation for `@RequestMapping` in Spring MVC for more details of those features.
All the features of Spring MVC or Webflux are available to Gateway handler methods. So you can inject request headers and query parameters, for instance, and you can constrain the incoming requests with declarations in the mapping annotation. See the documentation for `@RequestMapping` in Spring MVC for more details of those features.
Headers can be added to the downstream response using the `header()` methods on `ProxyExchange`.

View File

@@ -117,6 +117,7 @@
<modules>
<module>spring-cloud-gateway-dependencies</module>
<module>spring-cloud-gateway-mvc</module>
<module>spring-cloud-gateway-webflux</module>
<module>spring-cloud-gateway-core</module>
<module>spring-cloud-starter-gateway</module>
<module>spring-cloud-gateway-sample</module>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-gateway-webflux</artifactId>
<name>spring-cloud-gateway-webflux</name>
<description>Spring Cloud Gateway Webflux</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,446 @@
/*
* Copyright 2016-2017 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
*
* http://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.gateway.webflux;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* A <code>@RequestMapping</code> argument type that can proxy the request to a backend.
* Spring will inject one of these into your MVC handler method, and you get return a
* <code>ResponseEntity</code> that you get from one of the HTTP methods {@link #get()},
* {@link #post()}, {@link #put()}, {@link #patch()}, {@link #delete()} etc. Example:
*
* <pre>
* &#64;GetMapping("/proxy/{id}")
* public Mono&lt;ResponseEntity&lt;?&gt;&gt; proxy(@PathVariable Integer id, ProxyExchange&lt;?&gt; proxy)
* throws Exception {
* return proxy.uri("http://localhost:9000/foos/" + id).get();
* }
* </pre>
*
* <p>
* By default the incoming request body and headers are sent intact to the downstream
* service (with the exception of "sensitive" headers). To manipulate the downstream
* request there are "builder" style methods in {@link ProxyExchange}, but only the
* {@link #uri(String)} is mandatory. You can change the sensitive headers by calling the
* {@link #sensitive(String...)} method (Authorization and Cookie are sensitive by
* default).
* </p>
* <p>
* The type parameter <code>T</code> in <code>ProxyExchange&lt;T&gt;</code> is the type of
* the response body, so it comes out in the {@link ResponseEntity} that you return from
* your <code>@RequestMapping</code>. If you don't care about the type of the request and
* response body (e.g. if it's just a passthru) then use a wildcard, or
* <code>byte[]</code> (<code>Object</code> probably won't work unless you provide a converter).
* Use a concrete type if you want to
* transform or manipulate the response, or if you want to assert that it is convertible
* to the type you declare.
* </p>
* <p>
* To manipulate the response use the overloaded HTTP methods with a <code>Function</code>
* argument and pass in code to transform the response. E.g.
*
* <pre>
* &#64;PostMapping("/proxy")
* public Mono&lt;ResponseEntity&lt;Foo&gt;&gt; proxy(ProxyExchange&lt;Foo&gt; proxy) throws Exception {
* return proxy.uri("http://localhost:9000/foos/") //
* .post(response -> ResponseEntity.status(response.getStatusCode()) //
* .headers(response.getHeaders()) //
* .header("X-Custom", "MyCustomHeader") //
* .body(response.getBody()) //
* );
* }
*
* </pre>
*
* </p>
* <p>
* The full machinery of Spring {@link HttpMessageConverter message converters} is applied
* to the incoming request and response and also to the backend request. If you need
* additional converters then they need to be added upstream in the MVC configuration and
* also to the {@link WebClient} that is used in the backend calls (see the
* {@link ProxyExchange#ProxyExchange(WebClient, ServerWebExchange, BindingContext, Type)
* constructor} for details).
* </p>
*
* @author Dave Syer
*
*/
public class ProxyExchange<T> {
public static Set<String> DEFAULT_SENSITIVE = new HashSet<>(
Arrays.asList("cookie", "authorization"));
private URI uri;
private WebClient rest;
private Publisher<Object> body;
private boolean hasBody = false;
private ServerWebExchange exchange;
private BindingContext bindingContext;
private Set<String> sensitive;
private HttpHeaders headers = new HttpHeaders();
private Type responseType;
public ProxyExchange(WebClient rest, ServerWebExchange exchange,
BindingContext bindingContext, Type type) {
this.exchange = exchange;
this.bindingContext = bindingContext;
this.responseType = type;
this.rest = rest;
}
/**
* Sets the body for the downstream request (if using {@link #post()}, {@link #put()}
* or {@link #patch()}). The body can be omitted if you just want to pass the incoming
* request downstream without changing it. If you want to transform the incoming
* request you can declare it as a <code>@RequestBody</code> in your
* <code>@RequestMapping</code> in the usual Spring MVC way.
*
* @param body the request body to send downstream
* @return this for convenience
*/
public ProxyExchange<T> body(Object body) {
this.body = Mono.just(body);
return this;
}
/**
* Sets the body for the downstream request (if using {@link #post()}, {@link #put()}
* or {@link #patch()}). The body can be omitted if you just want to pass the incoming
* request downstream without changing it. If you want to transform the incoming
* request you can declare it as a <code>@RequestBody</code> in your
* <code>@RequestMapping</code> in the usual Spring MVC way.
*
* @param body the request body to send downstream
* @return this for convenience
*/
@SuppressWarnings("unchecked")
public ProxyExchange<T> body(Publisher<?> body) {
this.body = (Publisher<Object>) body;
return this;
}
/**
* Sets a header for the downstream call.
*
* @param name
* @param value
* @return this for convenience
*/
public ProxyExchange<T> header(String name, String... value) {
this.headers.put(name, Arrays.asList(value));
return this;
}
/**
* Additional headers, or overrides of the incoming ones, to be used in the downstream
* call.
*
* @param headers the http headers to use in the downstream call
* @return this for convenience
*/
public ProxyExchange<T> headers(HttpHeaders headers) {
this.headers.putAll(headers);
return this;
}
/**
* Sets the names of sensitive headers that are not passed downstream to the backend
* service.
*
* @param names the names of sensitive headers
* @return this for convenience
*/
public ProxyExchange<T> sensitive(String... names) {
if (this.sensitive == null) {
this.sensitive = new HashSet<>();
}
for (String name : names) {
this.sensitive.add(name.toLowerCase());
}
return this;
}
/**
* Sets the uri for the backend call when triggered by the HTTP methods.
*
* @param uri the backend uri to send the request to
* @return this for convenience
*/
public ProxyExchange<T> uri(String uri) {
try {
this.uri = new URI(uri);
}
catch (URISyntaxException e) {
throw new IllegalStateException("Cannot create URI", e);
}
return this;
}
public String path() {
return exchange.getRequest().getPath().pathWithinApplication().value();
}
public String path(String prefix) {
String path = path();
if (!path.startsWith(prefix)) {
throw new IllegalArgumentException(
"Path does not start with prefix (" + prefix + "): " + path);
}
return path.substring(prefix.length());
}
public Mono<ResponseEntity<T>> get() {
RequestEntity<?> requestEntity = headers((BodyBuilder) RequestEntity.get(uri))
.build();
return exchange(requestEntity);
}
public <S> Mono<ResponseEntity<S>> get(
Function<ResponseEntity<T>, ResponseEntity<S>> converter) {
return get().map(converter::apply);
}
public Mono<ResponseEntity<T>> head() {
RequestEntity<?> requestEntity = headers((BodyBuilder) RequestEntity.head(uri))
.build();
return exchange(requestEntity);
}
public <S> Mono<ResponseEntity<S>> head(
Function<ResponseEntity<T>, ResponseEntity<S>> converter) {
return head().map(converter::apply);
}
public Mono<ResponseEntity<T>> options() {
RequestEntity<?> requestEntity = headers((BodyBuilder) RequestEntity.options(uri))
.build();
return exchange(requestEntity);
}
public <S> Mono<ResponseEntity<S>> options(
Function<ResponseEntity<T>, ResponseEntity<S>> converter) {
return options().map(converter::apply);
}
public Mono<ResponseEntity<T>> post() {
RequestEntity<Object> requestEntity = headers(RequestEntity.post(uri))
.body(body());
return exchange(requestEntity);
}
public <S> Mono<ResponseEntity<S>> post(
Function<ResponseEntity<T>, ResponseEntity<S>> converter) {
return post().map(converter::apply);
}
public Mono<ResponseEntity<T>> delete() {
RequestEntity<Void> requestEntity = headers(
(BodyBuilder) RequestEntity.delete(uri)).build();
return exchange(requestEntity);
}
public <S> Mono<ResponseEntity<S>> delete(
Function<ResponseEntity<T>, ResponseEntity<S>> converter) {
return delete().map(converter::apply);
}
public Mono<ResponseEntity<T>> put() {
RequestEntity<Object> requestEntity = headers(RequestEntity.put(uri))
.body(body());
return exchange(requestEntity);
}
public <S> Mono<ResponseEntity<S>> put(
Function<ResponseEntity<T>, ResponseEntity<S>> converter) {
return put().map(converter::apply);
}
public Mono<ResponseEntity<T>> patch() {
RequestEntity<Object> requestEntity = headers(RequestEntity.patch(uri))
.body(body());
return exchange(requestEntity);
}
public <S> Mono<ResponseEntity<S>> patch(
Function<ResponseEntity<T>, ResponseEntity<S>> converter) {
return patch().map(converter::apply);
}
private Mono<ResponseEntity<T>> exchange(RequestEntity<?> requestEntity) {
Type type = this.responseType;
RequestBodySpec builder = rest.method(requestEntity.getMethod())
.uri(requestEntity.getUrl())
.headers(headers -> headers.addAll(requestEntity.getHeaders()));
Mono<ClientResponse> result;
if (requestEntity.getBody() instanceof Publisher) {
@SuppressWarnings("unchecked")
Publisher<Object> publisher = (Publisher<Object>) requestEntity.getBody();
result = builder.body(publisher, Object.class).exchange();
}
else if (requestEntity.getBody() != null) {
result = builder.body(BodyInserters.fromObject(requestEntity.getBody()))
.exchange();
}
else {
if (hasBody) {
result = builder.headers(
headers -> headers.addAll(exchange.getRequest().getHeaders()))
.body(exchange.getRequest().getBody(), DataBuffer.class)
.exchange();
}
else {
result = builder.headers(
headers -> headers.addAll(exchange.getRequest().getHeaders()))
.exchange();
}
}
return result.flatMap(response -> response.toEntity(ParameterizedTypeReference.forType(type)));
}
private BodyBuilder headers(BodyBuilder builder) {
Set<String> sensitive = this.sensitive;
if (sensitive == null) {
sensitive = DEFAULT_SENSITIVE;
}
proxy();
for (String name : headers.keySet()) {
if (sensitive.contains(name.toLowerCase())) {
continue;
}
builder.header(name, headers.get(name).toArray(new String[0]));
}
return builder;
}
private void proxy() {
URI uri = exchange.getRequest().getURI();
appendForwarded(uri);
appendXForwarded(uri);
}
private void appendXForwarded(URI uri) {
// Append the legacy headers if they were already added upstream
String host = headers.getFirst("x-forwarded-host");
if (host == null) {
return;
}
host = host + "," + uri.getHost();
headers.set("x-forwarded-host", host);
String proto = headers.getFirst("x-forwarded-proto");
if (proto == null) {
return;
}
proto = proto + "," + uri.getScheme();
headers.set("x-forwarded-proto", proto);
}
private void appendForwarded(URI uri) {
String forwarded = headers.getFirst("forwarded");
if (forwarded != null) {
forwarded = forwarded + ",";
}
else {
forwarded = "";
}
forwarded = forwarded + forwarded(uri);
headers.set("forwarded", forwarded);
}
private String forwarded(URI uri) {
if ("http".equals(uri.getScheme())) {
return "host=" + uri.getHost();
}
return String.format("host=%s;proto=%s", uri.getHost(), uri.getScheme());
}
private Publisher<?> body() {
Publisher<?> body = this.body;
if (body != null) {
return body;
}
body = getRequestBody();
hasBody = true; // even if it's null
return body;
}
/**
* Search for the request body if it was already deserialized using
* <code>@RequestBody</code>. If it is not found then deserialize it in the same way
* that it would have been for a <code>@RequestBody</code>.
*
* @return the request body
*/
private Mono<Object> getRequestBody() {
for (String key : bindingContext.getModel().asMap().keySet()) {
if (key.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
BindingResult result = (BindingResult) bindingContext.getModel().asMap()
.get(key);
return Mono.just(result.getTarget());
}
}
return null;
}
protected static class BodyGrabber {
public Publisher<Object> body(@RequestBody Publisher<Object> body) {
return body;
}
}
protected static class BodySender {
@ResponseBody
public Publisher<Object> body() {
return null;
}
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2016-2017 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
*
* http://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.gateway.webflux.config;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.Set;
import org.springframework.cloud.gateway.webflux.ProxyExchange;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author Dave Syer
*
*/
public class ProxyExchangeArgumentResolver implements HandlerMethodArgumentResolver {
private WebClient rest;
private HttpHeaders headers;
private Set<String> sensitive;
public ProxyExchangeArgumentResolver(WebClient builder) {
this.rest = builder;
}
public void setHeaders(HttpHeaders headers) {
this.headers = headers;
}
public void setSensitive(Set<String> sensitive) {
this.sensitive = sensitive;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return ProxyExchange.class.isAssignableFrom(parameter.getParameterType());
}
private Type type(MethodParameter parameter) {
Type type = parameter.getGenericParameterType();
if (type instanceof ParameterizedType) {
ParameterizedType param = (ParameterizedType) type;
type = param.getActualTypeArguments()[0];
}
if (type instanceof TypeVariable || type instanceof WildcardType) {
type = Object.class;
}
return type;
}
@Override
public Mono<Object> resolveArgument(MethodParameter parameter,
BindingContext bindingContext, ServerWebExchange exchange) {
ProxyExchange<?> proxy = new ProxyExchange<>(rest, exchange, bindingContext,
type(parameter));
proxy.headers(headers);
if (sensitive != null) {
proxy.sensitive(sensitive.toArray(new String[0]));
}
return Mono.just(proxy);
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2016-2017 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
*
* http://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.gateway.webflux.config;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.webflux.ProxyExchange;
import org.springframework.http.HttpHeaders;
/**
* Configuration properties for the {@link ProxyExchange} argument handler in
* <code>@RequestMapping</code> methods.
* @author Dave Syer
*
*/
@ConfigurationProperties("spring.cloud.gateway.proxy")
public class ProxyProperties {
/**
* Fixed header values that will be added to all downstream requests.
*/
private Map<String, String> headers = new LinkedHashMap<>();
/**
* A set of sensitive header names that will not be sent downstream by default.
*/
private Set<String> sensitive = null;
public Map<String, String> getHeaders() {
return headers;
}
public void setHeaders(Map<String, String> headers) {
this.headers = headers;
}
public Set<String> getSensitive() {
return sensitive;
}
public void setSensitive(Set<String> sensitive) {
this.sensitive = sensitive;
}
public HttpHeaders convertHeaders() {
HttpHeaders headers = new HttpHeaders();
for (String key : this.headers.keySet()) {
headers.set(key, this.headers.get(key));
}
return headers;
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2016-2017 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
*
* http://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.gateway.webflux.config;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.webflux.ProxyExchange;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
/**
* Autoconfiguration for the {@link ProxyExchange} argument handler in Spring Webflux
* <code>@RequestMapping</code> methods.
*
* @author Dave Syer
*/
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ HandlerMethodReturnValueHandler.class })
@EnableConfigurationProperties(ProxyProperties.class)
public class ProxyResponseAutoConfiguration implements WebFluxConfigurer {
@Autowired
private ApplicationContext context;
@Bean
@ConditionalOnMissingBean
public ProxyExchangeArgumentResolver proxyExchangeArgumentResolver(
Optional<WebClient.Builder> optional, ProxyProperties proxy) {
WebClient.Builder builder = optional.orElse(WebClient.builder());
WebClient template = builder.build();
ProxyExchangeArgumentResolver resolver = new ProxyExchangeArgumentResolver(
template);
resolver.setHeaders(proxy.convertHeaders());
resolver.setSensitive(proxy.getSensitive()); // can be null
return resolver;
}
@Override
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
WebFluxConfigurer.super.configureArgumentResolvers(configurer);
configurer
.addCustomResolver(context.getBean(ProxyExchangeArgumentResolver.class));
}
}

View File

@@ -0,0 +1,5 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.gateway.webflux.config.ProxyResponseAutoConfiguration
org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebFlux=\
org.springframework.cloud.gateway.webflux.config.ProxyResponseAutoConfiguration

View File

@@ -0,0 +1,355 @@
/*
* Copyright 2016-2017 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
*
* http://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.gateway.webflux;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.gateway.webflux.ProductionConfigurationTests.TestApplication;
import org.springframework.cloud.gateway.webflux.ProductionConfigurationTests.TestApplication.Bar;
import org.springframework.cloud.gateway.webflux.ProductionConfigurationTests.TestApplication.Foo;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(classes = TestApplication.class)
@DirtiesContext
public class ProductionConfigurationTests {
@Autowired
private TestRestTemplate rest;
@Autowired
private TestApplication application;
@LocalServerPort
private int port;
@Before
public void init() throws Exception {
application.setHome(new URI("http://localhost:" + port));
}
@Test
public void get() throws Exception {
assertThat(rest.getForObject("/proxy/0", Foo.class).getName()).isEqualTo("bye");
}
@Test
public void path() throws Exception {
assertThat(rest.getForObject("/proxy/path/1", Foo.class).getName())
.isEqualTo("foo");
}
@Test
public void resource() throws Exception {
assertThat(rest.getForObject("/proxy/html/test.html", String.class))
.contains("<body>Test");
}
@Test
public void resourceWithNoType() throws Exception {
assertThat(rest.getForObject("/proxy/typeless/test.html", String.class))
.contains("<body>Test");
}
@Test
public void missing() throws Exception {
assertThat(rest.getForEntity("/proxy/missing/0", Foo.class).getStatusCode())
.isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void uri() throws Exception {
assertThat(rest.getForObject("/proxy/0", Foo.class).getName()).isEqualTo("bye");
}
@Test
public void post() throws Exception {
assertThat(rest.postForObject("/proxy/0", Collections.singletonMap("name", "foo"),
Bar.class).getName()).isEqualTo("host=localhost;foo");
}
@Test
public void list() throws Exception {
ResponseEntity<List<Bar>> result = rest.exchange(
RequestEntity
.post(rest.getRestTemplate().getUriTemplateHandler()
.expand("/proxy"))
.contentType(MediaType.APPLICATION_JSON)
.body(Collections
.singletonList(Collections.singletonMap("name", "foo"))),
new ParameterizedTypeReference<List<Bar>>() {
});
assertThat(result.getBody().iterator().next().getName()).isEqualTo("host=localhost;foo");
}
@Test
public void bodyless() throws Exception {
assertThat(rest.postForObject("/proxy/0", Collections.singletonMap("name", "foo"),
Bar.class).getName()).isEqualTo("host=localhost;foo");
}
@Test
public void entity() throws Exception {
assertThat(rest.exchange(
RequestEntity
.post(rest.getRestTemplate().getUriTemplateHandler()
.expand("/proxy/entity"))
.body(Collections.singletonMap("name", "foo")),
new ParameterizedTypeReference<List<Bar>>() {
}).getBody().iterator().next().getName()).isEqualTo("host=localhost;foo");
}
@Test
public void entityWithType() throws Exception {
assertThat(rest.exchange(
RequestEntity
.post(rest.getRestTemplate().getUriTemplateHandler()
.expand("/proxy/type"))
.body(Collections.singletonMap("name", "foo")),
new ParameterizedTypeReference<List<Bar>>() {
}).getBody().iterator().next().getName()).isEqualTo("host=localhost;foo");
}
@Test
public void single() throws Exception {
assertThat(rest.postForObject("/proxy/single",
Collections.singletonMap("name", "foobar"), Bar.class).getName())
.isEqualTo("host=localhost;foobar");
}
@Test
public void converter() throws Exception {
assertThat(rest.postForObject("/proxy/converter",
Collections.singletonMap("name", "foobar"), Bar.class).getName())
.isEqualTo("host=localhost;foobar");
}
@SpringBootApplication
static class TestApplication {
@RestController
static class ProxyController {
private URI home;
public void setHome(URI home) {
this.home = home;
}
@GetMapping("/proxy/{id}")
public Mono<ResponseEntity<Object>> proxyFoos(@PathVariable Integer id, ProxyExchange<Object> proxy)
throws Exception {
return proxy.uri(home.toString() + "/foos/" + id).get();
}
@GetMapping("/proxy/path/**")
public Mono<ResponseEntity<Object>> proxyPath(ProxyExchange<Object> proxy, UriComponentsBuilder uri)
throws Exception {
String path = proxy.path("/proxy/path/");
return proxy.uri(home.toString() + "/foos/" + path).get();
}
@GetMapping("/proxy/html/**")
public Mono<ResponseEntity<String>> proxyHtml(ProxyExchange<String> proxy,
UriComponentsBuilder uri) throws Exception {
String path = proxy.path("/proxy/html");
return proxy.uri(home.toString() + path).get();
}
@GetMapping("/proxy/typeless/**")
public Mono<ResponseEntity<byte[]>> proxyTypeless(ProxyExchange<byte[]> proxy, UriComponentsBuilder uri)
throws Exception {
String path = proxy.path("/proxy/typeless");
return proxy.uri(home.toString() + path).get();
}
@GetMapping("/proxy/missing/{id}")
public Mono<ResponseEntity<Object>> proxyMissing(@PathVariable Integer id, ProxyExchange<Object> proxy)
throws Exception {
return proxy.uri(home.toString() + "/missing/" + id).get();
}
@GetMapping("/proxy")
public Mono<ResponseEntity<Object>> proxyUri(ProxyExchange<Object> proxy) throws Exception {
return proxy.uri(home.toString() + "/foos").get();
}
@PostMapping("/proxy/{id}")
public Mono<ResponseEntity<Object>> proxyBars(@PathVariable Integer id,
@RequestBody Map<String, Object> body,
ProxyExchange<List<Object>> proxy) throws Exception {
body.put("id", id);
return proxy.uri(home.toString() + "/bars").body(Arrays.asList(body))
.post(this::first);
}
@PostMapping("/proxy")
public Mono<ResponseEntity<List<Object>>> barsWithNoBody(ProxyExchange<List<Object>> proxy) throws Exception {
return proxy.uri(home.toString() + "/bars").post();
}
@PostMapping("/proxy/entity")
public Mono<ResponseEntity<Object>> explicitEntity(@RequestBody Mono<Foo> foo,
ProxyExchange<Object> proxy) throws Exception {
return proxy.uri(home.toString() + "/bars").body(Flux.from(foo)).post();
}
@PostMapping("/proxy/type")
public Mono<ResponseEntity<List<Bar>>> explicitEntityWithType(
@RequestBody Mono<Foo> foo, ProxyExchange<List<Bar>> proxy)
throws Exception {
return proxy.uri(home.toString() + "/bars").body(Flux.from(foo)).post();
}
@PostMapping("/proxy/single")
public Mono<ResponseEntity<Object>> implicitEntity(@RequestBody Mono<Foo> foo,
ProxyExchange<List<Object>> proxy) throws Exception {
return proxy.uri(home.toString() + "/bars").body(Flux.from(foo))
.post(this::first);
}
@PostMapping("/proxy/converter")
public Mono<ResponseEntity<Bar>> implicitEntityWithConverter(
@RequestBody Foo foo, ProxyExchange<List<Bar>> proxy)
throws Exception {
return proxy.uri(home.toString() + "/bars").body(Arrays.asList(foo))
.post(response -> ResponseEntity.status(response.getStatusCode())
.headers(response.getHeaders())
.body(response.getBody().iterator().next()));
}
private <T> ResponseEntity<T> first(ResponseEntity<List<T>> response) {
return ResponseEntity.status(response.getStatusCode())
.headers(response.getHeaders())
.body(response.getBody().iterator().next());
}
}
@Autowired
private ProxyController controller;
public void setHome(URI home) {
controller.setHome(home);
}
@RestController
static class TestController {
@GetMapping("/foos")
public List<Foo> foos() {
return Arrays.asList(new Foo("hello"));
}
@GetMapping("/foos/{id}")
public Foo foo(@PathVariable Integer id, @RequestHeader HttpHeaders headers) {
String custom = headers.getFirst("X-Custom");
return new Foo(id == 1 ? "foo" : custom != null ? custom : "bye");
}
@PostMapping("/bars")
public List<Bar> bars(@RequestBody List<Foo> foos,
@RequestHeader HttpHeaders headers) {
String custom = headers.getFirst("X-Custom");
custom = custom == null ? "" : custom;
custom = headers.getFirst("forwarded") == null ? custom
: headers.getFirst("forwarded") + ";" + custom;
return Arrays.asList(new Bar(custom + foos.iterator().next().getName()));
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class Foo {
private String name;
public Foo() {
}
public Foo(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class Bar {
private String name;
public Bar() {
}
public Bar(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
}

View File

@@ -0,0 +1,230 @@
/*
* Copyright 2016-2017 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
*
* http://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.gateway.webflux;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.gateway.webflux.ReactiveTests.TestApplication;
import org.springframework.cloud.gateway.webflux.ReactiveTests.TestApplication.Bar;
import org.springframework.cloud.gateway.webflux.ReactiveTests.TestApplication.Foo;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(classes = TestApplication.class)
public class ReactiveTests {
@Autowired
private TestRestTemplate rest;
@LocalServerPort
private int port;
@Test
public void postBytes() throws Exception {
ResponseEntity<List<Foo>> result = rest.exchange(
RequestEntity.post(
rest.getRestTemplate().getUriTemplateHandler().expand("/bytes"))
.body("hello foo".getBytes()),
new ParameterizedTypeReference<List<Foo>>() {
});
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody().iterator().next().getName()).isEqualTo("hello foo");
}
@Test
public void post() throws Exception {
ResponseEntity<List<Bar>> result = rest.exchange(
RequestEntity
.post(rest.getRestTemplate().getUriTemplateHandler()
.expand("/bars"))
.body(Collections
.singletonList(Collections.singletonMap("name", "foo"))),
new ParameterizedTypeReference<List<Bar>>() {
});
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody().iterator().next().getName()).isEqualTo("hello foo");
}
@Test
public void postFlux() throws Exception {
ResponseEntity<List<Bar>> result = rest.exchange(
RequestEntity
.post(rest.getRestTemplate().getUriTemplateHandler()
.expand("/flux/bars"))
.body(Collections
.singletonList(Collections.singletonMap("name", "foo"))),
new ParameterizedTypeReference<List<Bar>>() {
});
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody().iterator().next().getName()).isEqualTo("hello foo");
}
@Test
public void get() throws Exception {
ResponseEntity<List<Foo>> result = rest.exchange(RequestEntity
.get(rest.getRestTemplate().getUriTemplateHandler().expand("/foos"))
.build(), new ParameterizedTypeReference<List<Foo>>() {
});
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody().iterator().next().getName()).isEqualTo("hello");
}
@Test
public void forward() throws Exception {
ResponseEntity<List<Foo>> result = rest.exchange(
RequestEntity.get(rest.getRestTemplate().getUriTemplateHandler()
.expand("/forward/foos")).build(),
new ParameterizedTypeReference<List<Foo>>() {
});
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody().iterator().next().getName()).isEqualTo("hello");
}
@SpringBootApplication
static class TestApplication {
@RestController
static class TestController {
@Autowired
private DispatcherHandler handler;
@PostMapping("/bars")
public List<Bar> bars(@RequestBody List<Foo> foos,
@RequestHeader HttpHeaders headers) {
String custom = "hello ";
return foos.stream().map(foo -> new Bar(custom + foo.getName()))
.collect(Collectors.toList());
}
@PostMapping("/flux/bars")
public Flux<Bar> fluxbars(@RequestBody Flux<Foo> foos,
@RequestHeader HttpHeaders headers) {
String custom = "hello ";
return foos.map(foo -> new Bar(custom + foo.getName()));
}
@GetMapping("/foos")
public Flux<Foo> foos() {
return Flux.just(new Foo("hello"));
}
@GetMapping("/forward/foos")
public Mono<Void> forwardFoos(ServerWebExchange exchange) {
return handler.handle(exchange.mutate()
.request(request -> request.path("/foos").build()).build());
}
@PostMapping("/bytes")
public Flux<Foo> forwardBars(@RequestBody Flux<byte[]> body) {
return Flux.from(body.reduce(this::concatenate)
.map(value -> new Foo(new String(value))));
}
byte[] concatenate(@Nullable byte[] array1, @Nullable byte[] array2) {
if (ObjectUtils.isEmpty(array1)) {
return array2;
}
if (ObjectUtils.isEmpty(array2)) {
return array1;
}
byte[] newArr = new byte[array1.length + array2.length];
System.arraycopy(array1, 0, newArr, 0, array1.length);
System.arraycopy(array2, 0, newArr, array1.length, array2.length);
return newArr;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class Foo {
private String name;
public Foo() {
}
public Foo(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class Bar {
private String name;
public Bar() {
}
public Bar(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
}

View File

@@ -0,0 +1 @@
logging.level.org.springframework.web.reactive=DEBUG

View File

@@ -0,0 +1,4 @@
<html>
<body>Test
</body>
</html>