Dave Syer
7 years ago
11 changed files with 1379 additions and 0 deletions
@ -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> |
@ -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> |
||||
* @GetMapping("/proxy/{id}") |
||||
* public ResponseEntity<?> proxy(@PathVariable Integer id, ProxyExchange<?> 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<T></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> |
||||
* @PostMapping("/proxy") |
||||
* public ResponseEntity<Foo> proxy(ProxyExchange<Foo> 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; |
||||
} |
||||
}; |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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 { |
||||
} |
||||
|
||||
} |
||||
} |
@ -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 |
@ -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; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue