Browse Source

Migrate MVC module over from spring-cloud-function

pull/53/merge
Dave Syer 7 years ago
parent
commit
73eea9d8c0
  1. 1
      pom.xml
  2. 5
      spring-cloud-gateway-dependencies/pom.xml
  3. 0
      spring-cloud-gateway-mvc/.jdk8
  4. 33
      spring-cloud-gateway-mvc/pom.xml
  5. 644
      spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/ProxyExchange.java
  6. 83
      spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyExchangeArgumentResolver.java
  7. 70
      spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyProperties.java
  8. 89
      spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyResponseAutoConfiguration.java
  9. 5
      spring-cloud-gateway-mvc/src/main/resources/META-INF/spring.factories
  10. 445
      spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/function/web/gateway/ProductionConfigurationTests.java
  11. 4
      spring-cloud-gateway-mvc/src/test/resources/static/test.html

1
pom.xml

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

5
spring-cloud-gateway-dependencies/pom.xml

@ -21,6 +21,11 @@ @@ -21,6 +21,11 @@
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-mvc</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-core</artifactId>

0
spring-cloud-gateway-mvc/.jdk8

33
spring-cloud-gateway-mvc/pom.xml

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
<?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-mvc</artifactId>
<packaging>jar</packaging>
<name>spring-cloud-gateway-mvc</name>
<description>Spring Cloud Gateway MVC</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

644
spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/ProxyExchange.java

@ -0,0 +1,644 @@ @@ -0,0 +1,644 @@
/*
* 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.function.gateway;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import java.util.function.Function;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.springframework.core.Conventions;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterizedTypeReference;
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.http.converter.HttpMessageNotWritableException;
import org.springframework.util.ClassUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
import org.springframework.web.util.AbstractUriTemplateHandler;
/**
* 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 ResponseEntity&lt;?&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> or <code>Object</code>. 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 ResponseEntity&lt;Foo&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 RestTemplate} that is used in the backend calls (see the
* {@link ProxyExchange#ProxyExchange(RestTemplate, NativeWebRequest, ModelAndViewContainer, WebDataBinderFactory, Type)
* constructor} for details).
* </p>
* <p>
* As well as the HTTP methods for a backend call you can also use
* {@link #forward(String)} for a local in-container dispatch.
* </p>
*
* @author Dave Syer
*
*/
public class ProxyExchange<T> {
public static Set<String> DEFAULT_SENSITIVE = new HashSet<>(
Arrays.asList("cookie", "authorization"));
private URI uri;
private NestedTemplate rest;
private Object body;
private RequestResponseBodyMethodProcessor delegate;
private NativeWebRequest webRequest;
private ModelAndViewContainer mavContainer;
private WebDataBinderFactory binderFactory;
private Set<String> sensitive;
private HttpHeaders headers = new HttpHeaders();
private Type responseType;
public ProxyExchange(RestTemplate rest, NativeWebRequest webRequest,
ModelAndViewContainer mavContainer, WebDataBinderFactory binderFactory,
Type type) {
this.responseType = type;
this.rest = createTemplate(rest);
this.webRequest = webRequest;
this.mavContainer = mavContainer;
this.binderFactory = binderFactory;
this.delegate = new RequestResponseBodyMethodProcessor(
rest.getMessageConverters());
}
/**
* 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 = 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 (String) this.webRequest.getAttribute(
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE,
WebRequest.SCOPE_REQUEST);
}
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 void forward(String path) {
HttpServletRequest request = this.webRequest
.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = this.webRequest
.getNativeResponse(HttpServletResponse.class);
try {
request.getRequestDispatcher(path).forward(
new BodyForwardingHttpServletRequest(request, response), response);
}
catch (Exception e) {
throw new IllegalStateException("Cannot forward request", e);
}
}
public ResponseEntity<T> get() {
RequestEntity<?> requestEntity = headers((BodyBuilder) RequestEntity.get(uri))
.build();
return exchange(requestEntity);
}
public <S> ResponseEntity<S> get(
Function<ResponseEntity<T>, ResponseEntity<S>> converter) {
return converter.apply(get());
}
public ResponseEntity<T> head() {
RequestEntity<?> requestEntity = headers((BodyBuilder) RequestEntity.head(uri))
.build();
return exchange(requestEntity);
}
public <S> ResponseEntity<S> head(
Function<ResponseEntity<T>, ResponseEntity<S>> converter) {
return converter.apply(head());
}
public ResponseEntity<T> options() {
RequestEntity<?> requestEntity = headers((BodyBuilder) RequestEntity.options(uri))
.build();
return exchange(requestEntity);
}
public <S> ResponseEntity<S> options(
Function<ResponseEntity<T>, ResponseEntity<S>> converter) {
return converter.apply(options());
}
public ResponseEntity<T> post() {
RequestEntity<Object> requestEntity = headers(RequestEntity.post(uri))
.body(body());
return exchange(requestEntity);
}
public <S> ResponseEntity<S> post(
Function<ResponseEntity<T>, ResponseEntity<S>> converter) {
return converter.apply(post());
}
public ResponseEntity<T> delete() {
RequestEntity<Void> requestEntity = headers(
(BodyBuilder) RequestEntity.delete(uri)).build();
return exchange(requestEntity);
}
public <S> ResponseEntity<S> delete(
Function<ResponseEntity<T>, ResponseEntity<S>> converter) {
return converter.apply(delete());
}
public ResponseEntity<T> put() {
RequestEntity<Object> requestEntity = headers(RequestEntity.put(uri))
.body(body());
return exchange(requestEntity);
}
public <S> ResponseEntity<S> put(
Function<ResponseEntity<T>, ResponseEntity<S>> converter) {
return converter.apply(put());
}
public ResponseEntity<T> patch() {
RequestEntity<Object> requestEntity = headers(RequestEntity.patch(uri))
.body(body());
return exchange(requestEntity);
}
public <S> ResponseEntity<S> patch(
Function<ResponseEntity<T>, ResponseEntity<S>> converter) {
return converter.apply(patch());
}
private ResponseEntity<T> exchange(RequestEntity<?> requestEntity) {
Type type = this.responseType;
if (type instanceof TypeVariable || type instanceof WildcardType) {
type = Object.class;
}
RequestCallback requestCallback = rest.httpEntityCallback((Object) requestEntity,
type);
ResponseExtractor<ResponseEntity<T>> responseExtractor = rest
.responseEntityExtractor(type);
return rest.execute(requestEntity.getUrl(), requestEntity.getMethod(),
requestCallback, responseExtractor);
}
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() {
try {
URI uri = new URI(webRequest.getNativeRequest(HttpServletRequest.class)
.getRequestURL().toString());
appendForwarded(uri);
appendXForwarded(uri);
}
catch (URISyntaxException e) {
throw new IllegalStateException("Cannot create URI for request: " + webRequest
.getNativeRequest(HttpServletRequest.class).getRequestURL());
}
}
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 Object body() {
if (body != null) {
return body;
}
body = getRequestBody();
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 Object getRequestBody() {
for (String key : mavContainer.getModel().keySet()) {
if (key.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
BindingResult result = (BindingResult) mavContainer.getModel().get(key);
return result.getTarget();
}
}
MethodParameter input = new MethodParameter(
ClassUtils.getMethod(BodyGrabber.class, "body", Object.class), 0);
try {
delegate.resolveArgument(input, mavContainer, webRequest, binderFactory);
}
catch (Exception e) {
throw new IllegalStateException("Cannot resolve body", e);
}
String name = Conventions.getVariableNameForParameter(input);
BindingResult result = (BindingResult) mavContainer.getModel()
.get(BindingResult.MODEL_KEY_PREFIX + name);
return result.getTarget();
}
private NestedTemplate createTemplate(RestTemplate input) {
NestedTemplate rest = new NestedTemplate();
rest.setMessageConverters(input.getMessageConverters());
rest.setErrorHandler(input.getErrorHandler());
rest.setDefaultUriVariables(
((AbstractUriTemplateHandler) input.getUriTemplateHandler())
.getDefaultUriVariables());
rest.setRequestFactory(input.getRequestFactory());
rest.setInterceptors(input.getInterceptors());
return rest;
}
/**
* A special {@link RestTemplate} that knows about the {@link Type} of its response
* body explicitly (rather than through a {@link ParameterizedTypeReference}, which is
* the only way to access this feature in a regular template).
*
*/
class NestedTemplate extends RestTemplate {
@Override
protected <S> RequestCallback httpEntityCallback(Object requestBody,
Type responseType) {
return super.httpEntityCallback(requestBody, responseType);
}
@Override
protected <S> ResponseExtractor<ResponseEntity<S>> responseEntityExtractor(
Type responseType) {
return super.responseEntityExtractor(responseType);
}
}
/**
* A servlet request wrapper that can be safely passed downstream to an internal
* forward dispatch, caching its body, and making it available in converted form using
* Spring message converters.
*
*/
class BodyForwardingHttpServletRequest extends HttpServletRequestWrapper {
private HttpServletRequest request;
private HttpServletResponse response;
BodyForwardingHttpServletRequest(HttpServletRequest request,
HttpServletResponse response) {
super(request);
this.request = request;
this.response = response;
}
private List<String> header(String name) {
List<String> list = headers.get(name);
return list;
}
@Override
public ServletInputStream getInputStream() throws IOException {
Object body = body();
MethodParameter output = new MethodParameter(
ClassUtils.getMethod(BodySender.class, "body"), -1);
ServletOutputToInputConverter response = new ServletOutputToInputConverter(
this.response);
ServletWebRequest webRequest = new ServletWebRequest(this.request, response);
try {
delegate.handleReturnValue(body, output, mavContainer, webRequest);
}
catch (HttpMessageNotWritableException
| HttpMediaTypeNotAcceptableException e) {
throw new IllegalStateException("Cannot convert body", e);
}
return response.getInputStream();
}
@Override
public Enumeration<String> getHeaderNames() {
Set<String> names = headers.keySet();
if (names.isEmpty()) {
return super.getHeaderNames();
}
Set<String> result = new LinkedHashSet<>(names);
result.addAll(Collections.list(super.getHeaderNames()));
return new Vector<String>(result).elements();
}
@Override
public Enumeration<String> getHeaders(String name) {
List<String> list = header(name);
if (list != null) {
return new Vector<String>(list).elements();
}
return super.getHeaders(name);
}
@Override
public String getHeader(String name) {
List<String> list = header(name);
if (list != null && !list.isEmpty()) {
return list.iterator().next();
}
return super.getHeader(name);
}
}
protected static class BodyGrabber {
public Object body(@RequestBody Object body) {
return body;
}
}
protected static class BodySender {
@ResponseBody
public Object body() {
return null;
}
}
}
/**
* Convenience class that converts an incoming request input stream into a form that can
* be easily deserialized to a Java object using Spring message converters. It is only
* used in a local forward dispatch, in which case there is a danger that the request body
* will need to be read and analysed more than once. Apart from using the message
* converters the other main feature of this class is that the request body is cached and
* can be read repeatedly as necessary.
*
* @author Dave Syer
*
*/
class ServletOutputToInputConverter extends HttpServletResponseWrapper {
private StringBuilder builder = new StringBuilder();
public ServletOutputToInputConverter(HttpServletResponse response) {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new ServletOutputStream() {
@Override
public void write(int b) throws IOException {
builder.append(new Character((char) b));
}
@Override
public void setWriteListener(WriteListener listener) {
}
@Override
public boolean isReady() {
return true;
}
};
}
public ServletInputStream getInputStream() {
ByteArrayInputStream body = new ByteArrayInputStream(
builder.toString().getBytes());
return new ServletInputStream() {
@Override
public int read() throws IOException {
return body.read();
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public boolean isReady() {
return true;
}
@Override
public boolean isFinished() {
return body.available() <= 0;
}
};
}
}

83
spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyExchangeArgumentResolver.java

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
/*
* 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.function.gateway.config;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Set;
import org.springframework.cloud.function.gateway.ProxyExchange;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* @author Dave Syer
*
*/
public class ProxyExchangeArgumentResolver implements HandlerMethodArgumentResolver {
private RestTemplate rest;
private HttpHeaders headers;
private Set<String> sensitive;
public ProxyExchangeArgumentResolver(RestTemplate 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());
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
ProxyExchange<?> proxy = new ProxyExchange<>(rest, webRequest, mavContainer,
binderFactory, type(parameter));
proxy.headers(headers);
if (sensitive != null) {
proxy.sensitive(sensitive.toArray(new String[0]));
}
return proxy;
}
private Type type(MethodParameter parameter) {
Type type = parameter.getGenericParameterType();
if (type instanceof ParameterizedType) {
ParameterizedType param = (ParameterizedType) type;
type = param.getActualTypeArguments()[0];
}
return type;
}
}

70
spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyProperties.java

@ -0,0 +1,70 @@ @@ -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.function.gateway.config;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.function.gateway.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;
}
}

89
spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/function/gateway/config/ProxyResponseAutoConfiguration.java

@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
/*
* 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.function.gateway.config;
import java.io.IOException;
import java.util.List;
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.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.function.gateway.ProxyExchange;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* Autoconfiguration for the {@link ProxyExchange} argument handler in Spring MVC
* <code>@RequestMapping</code> methods.
*
* @author Dave Syer
*/
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ HandlerMethodReturnValueHandler.class })
@EnableConfigurationProperties(ProxyProperties.class)
public class ProxyResponseAutoConfiguration extends WebMvcConfigurerAdapter {
@Autowired
private ApplicationContext context;
@Bean
@ConditionalOnMissingBean
public ProxyExchangeArgumentResolver proxyExchangeArgumentResolver(
Optional<RestTemplateBuilder> optional, ProxyProperties proxy) {
RestTemplateBuilder builder = optional.orElse(new RestTemplateBuilder());
RestTemplate template = builder.build();
template.setErrorHandler(new NoOpResponseErrorHandler());
template.getMessageConverters().add(new ByteArrayHttpMessageConverter() {
@Override
public boolean supports(Class<?> clazz) {
return true;
}
});
ProxyExchangeArgumentResolver resolver = new ProxyExchangeArgumentResolver(
template);
resolver.setHeaders(proxy.convertHeaders());
resolver.setSensitive(proxy.getSensitive()); // can be null
return resolver;
}
@Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(context.getBean(ProxyExchangeArgumentResolver.class));
}
private static class NoOpResponseErrorHandler extends DefaultResponseErrorHandler {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
}
}
}

5
spring-cloud-gateway-mvc/src/main/resources/META-INF/spring.factories

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.function.gateway.config.ProxyResponseAutoConfiguration
org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc=\
org.springframework.cloud.function.gateway.config.ProxyResponseAutoConfiguration

445
spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/function/web/gateway/ProductionConfigurationTests.java

@ -0,0 +1,445 @@ @@ -0,0 +1,445 @@
/*
* 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.function.web.gateway;
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.context.embedded.LocalServerPort;
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.cloud.function.gateway.ProxyExchange;
import org.springframework.cloud.function.web.gateway.ProductionConfigurationTests.TestApplication;
import org.springframework.cloud.function.web.gateway.ProductionConfigurationTests.TestApplication.Bar;
import org.springframework.cloud.function.web.gateway.ProductionConfigurationTests.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.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;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(classes = TestApplication.class)
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 forward() throws Exception {
assertThat(rest.getForObject("/forward/foos/0", Foo.class).getName())
.isEqualTo("bye");
}
@Test
public void forwardHeader() throws Exception {
ResponseEntity<Foo> result = rest.getForEntity("/forward/special/foos/0",
Foo.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody().getName()).isEqualTo("FOO");
}
@Test
public void postForwardHeader() throws Exception {
ResponseEntity<List<Bar>> result = rest.exchange(
RequestEntity
.post(rest.getRestTemplate().getUriTemplateHandler().expand(
"/forward/special/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("FOOfoo");
}
@Test
public void postForwardBody() throws Exception {
ResponseEntity<String> result = rest.exchange(
RequestEntity
.post(rest.getRestTemplate().getUriTemplateHandler().expand(
"/forward/body/bars"))
.body(Collections.singletonList(Collections.singletonMap("name", "foo"))),
String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).contains("foo");
}
@Test
public void postForwardForgetBody() throws Exception {
ResponseEntity<String> result = rest.exchange(
RequestEntity
.post(rest.getRestTemplate().getUriTemplateHandler().expand(
"/forward/forget/bars"))
.body(Collections.singletonList(Collections.singletonMap("name", "foo"))),
String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).contains("foo");
}
@Test
public void postForwardBodyFoo() throws Exception {
ResponseEntity<List<Bar>> result = rest.exchange(
RequestEntity
.post(rest.getRestTemplate().getUriTemplateHandler().expand(
"/forward/body/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("foo");
}
@Test
public void list() throws Exception {
assertThat(rest.exchange(
RequestEntity
.post(rest.getRestTemplate().getUriTemplateHandler().expand(
"/proxy"))
.body(Collections.singletonList(Collections.singletonMap("name", "foo"))),
new ParameterizedTypeReference<List<Bar>>() {
}).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 ResponseEntity<?> proxyFoos(@PathVariable Integer id,
ProxyExchange<?> proxy) throws Exception {
return proxy.uri(home.toString() + "/foos/" + id).get();
}
@GetMapping("/proxy/path/**")
public ResponseEntity<?> proxyPath(ProxyExchange<?> proxy,
UriComponentsBuilder uri) throws Exception {
String path = proxy.path("/proxy/path/");
return proxy.uri(home.toString() + "/foos/" + path).get();
}
@GetMapping("/proxy/html/**")
public 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 ResponseEntity<?> proxyTypeless(ProxyExchange<?> proxy,
UriComponentsBuilder uri) throws Exception {
String path = proxy.path("/proxy/typeless");
return proxy.uri(home.toString() + path).get();
}
@GetMapping("/proxy/missing/{id}")
public ResponseEntity<?> proxyMissing(@PathVariable Integer id,
ProxyExchange<?> proxy) throws Exception {
return proxy.uri(home.toString() + "/missing/" + id).get();
}
@GetMapping("/proxy")
public ResponseEntity<?> proxyUri(ProxyExchange<?> proxy) throws Exception {
return proxy.uri(home.toString() + "/foos").get();
}
@PostMapping("/proxy/{id}")
public ResponseEntity<?> 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 ResponseEntity<?> barsWithNoBody(ProxyExchange<?> proxy)
throws Exception {
return proxy.uri(home.toString() + "/bars").post();
}
@PostMapping("/proxy/entity")
public ResponseEntity<?> explicitEntity(@RequestBody Foo foo,
ProxyExchange<?> proxy) throws Exception {
return proxy.uri(home.toString() + "/bars").body(Arrays.asList(foo))
.post();
}
@PostMapping("/proxy/type")
public ResponseEntity<List<Bar>> explicitEntityWithType(@RequestBody Foo foo,
ProxyExchange<List<Bar>> proxy) throws Exception {
return proxy.uri(home.toString() + "/bars").body(Arrays.asList(foo))
.post();
}
@PostMapping("/proxy/single")
public ResponseEntity<?> implicitEntity(@RequestBody Foo foo,
ProxyExchange<List<Object>> proxy) throws Exception {
return proxy.uri(home.toString() + "/bars").body(Arrays.asList(foo))
.post(this::first);
}
@PostMapping("/proxy/converter")
public 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()));
}
@GetMapping("/forward/**")
public void forward(ProxyExchange<?> proxy) throws Exception {
String path = proxy.path("/forward");
if (path.startsWith("/special")) {
proxy.header("X-Custom", "FOO");
path = proxy.path("/forward/special");
}
proxy.forward(path);
}
@PostMapping("/forward/**")
public void postForward(ProxyExchange<?> proxy) throws Exception {
String path = proxy.path("/forward");
if (path.startsWith("/special")) {
proxy.header("X-Custom", "FOO");
path = proxy.path("/forward/special");
}
proxy.forward(path);
}
@PostMapping("/forward/body/**")
public void postForwardBody(@RequestBody byte[] body, ProxyExchange<?> proxy)
throws Exception {
String path = proxy.path("/forward/body");
proxy.body(body).forward(path);
}
@PostMapping("/forward/forget/**")
public void postForwardForgetBody(@RequestBody byte[] body,
ProxyExchange<?> proxy) throws Exception {
String path = proxy.path("/forward/forget");
proxy.forward(path);
}
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;
}
}
}
}

4
spring-cloud-gateway-mvc/src/test/resources/static/test.html

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
<html>
<body>Test
</body>
</html>
Loading…
Cancel
Save