Adds a RibbonRequestCustomizer.

This allows developers to customize the request builders for zuul requests to inject headers or modify the request in general. Each zuul request type (RestClient, Apache HttpClient and OkHttp) is supported. fixes gh-1141
parent 390999e0
......@@ -16,6 +16,8 @@
package org.springframework.cloud.netflix.ribbon.apache;
import static org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer.Runner.customize;
import java.net.URI;
import java.util.List;
......@@ -69,6 +71,8 @@ public class RibbonApacheHttpRequest extends ContextAwareRequest implements Clon
builder.setEntity(entity);
}
customize(this.context.getRequestCustomizers(), builder);
builder.setConfig(requestConfig);
return builder.build();
}
......
......@@ -16,6 +16,8 @@
package org.springframework.cloud.netflix.ribbon.okhttp;
import static org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer.Runner.customize;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
......@@ -72,11 +74,14 @@ public class OkHttpRibbonRequest extends ContextAwareRequest implements Cloneabl
requestBody = new InputStreamRequestBody(this.context.getRequestEntity(), mediaType, this.context.getContentLength());
}
return new Request.Builder()
Request.Builder builder = new Request.Builder()
.url(url.build())
.headers(headers.build())
.method(this.context.getMethod(), requestBody)
.build();
.method(this.context.getMethod(), requestBody);
customize(this.context.getRequestCustomizers(), builder);
return builder.build();
}
public OkHttpRibbonRequest withNewUri(final URI uri) {
......
......@@ -41,8 +41,8 @@ public abstract class ContextAwareRequest extends ClientRequest {
protected RibbonCommandContext newContext(URI uri) {
RibbonCommandContext commandContext = new RibbonCommandContext(this.context.getServiceId(),
this.context.getMethod(), uri.toString(), this.context.getRetryable(),
this.context.getHeaders(), this.context.getParams(), this.context.getRequestEntity());
commandContext.setContentLength(this.context.getContentLength());
this.context.getHeaders(), this.context.getParams(), this.context.getRequestEntity(),
this.context.getRequestCustomizers(), this.context.getContentLength());
return commandContext;
}
}
/*
* Copyright 2013-2016 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.netflix.ribbon.support;
import java.util.List;
/**
* @author Spencer Gibb
*/
public interface RibbonRequestCustomizer<B> {
boolean accepts(Class builderClass);
void customize(B builder);
class Runner {
@SuppressWarnings("unchecked")
public static void customize(List<RibbonRequestCustomizer> customizers, Object builder) {
for (RibbonRequestCustomizer customizer : customizers) {
if (customizer.accepts(builder.getClass())) {
customizer.customize(builder);
}
}
}
}
}
......@@ -16,6 +16,9 @@
package org.springframework.cloud.netflix.zuul;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.trace.TraceRepository;
......@@ -30,6 +33,7 @@ import org.springframework.cloud.client.discovery.event.HeartbeatMonitor;
import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent;
import org.springframework.cloud.client.discovery.event.ParentHeartbeatEvent;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer;
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.cloud.netflix.zuul.filters.TraceProxyRequestHelper;
......@@ -38,12 +42,12 @@ import org.springframework.cloud.netflix.zuul.filters.discovery.DiscoveryClientR
import org.springframework.cloud.netflix.zuul.filters.discovery.ServiceRouteMapper;
import org.springframework.cloud.netflix.zuul.filters.discovery.SimpleServiceRouteMapper;
import org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter;
import org.springframework.cloud.netflix.zuul.filters.route.okhttp.OkHttpRibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.RestClientRibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter;
import org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter;
import org.springframework.cloud.netflix.zuul.filters.route.apache.HttpClientRibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.okhttp.OkHttpRibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
......@@ -57,6 +61,9 @@ import org.springframework.context.annotation.Configuration;
@Configuration
public class ZuulProxyConfiguration extends ZuulConfiguration {
@Autowired(required = false)
private List<RibbonRequestCustomizer> requestCustomizers = Collections.emptyList();
@Autowired
private DiscoveryClient discovery;
......@@ -122,7 +129,7 @@ public class ZuulProxyConfiguration extends ZuulConfiguration {
public RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper,
RibbonCommandFactory<?> ribbonCommandFactory) {
RibbonRoutingFilter filter = new RibbonRoutingFilter(helper,
ribbonCommandFactory);
ribbonCommandFactory, this.requestCustomizers);
return filter;
}
......
......@@ -17,16 +17,19 @@
package org.springframework.cloud.netflix.zuul.filters.route;
import com.netflix.client.http.HttpRequest;
import com.netflix.client.http.HttpResponse;
import com.netflix.niws.client.http.RestClient;
import org.springframework.cloud.netflix.zuul.filters.route.support.AbstractRibbonCommand;
import org.springframework.util.MultiValueMap;
import static org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer.Runner.customize;
import java.io.InputStream;
import java.net.URI;
import java.util.List;
import org.springframework.cloud.netflix.zuul.filters.route.support.AbstractRibbonCommand;
import org.springframework.util.MultiValueMap;
import com.netflix.client.http.HttpRequest;
import com.netflix.client.http.HttpResponse;
import com.netflix.niws.client.http.RestClient;
/**
* Hystrix wrapper around Eureka Ribbon command
*
......@@ -79,7 +82,7 @@ public class RestClientRibbonCommand extends AbstractRibbonCommand<RestClient, H
}
protected void customizeRequest(HttpRequest.Builder requestBuilder) {
// noop
customize(this.context.getRequestCustomizers(), requestBuilder);
}
@Deprecated
......
......@@ -19,26 +19,48 @@ package org.springframework.cloud.netflix.zuul.filters.route;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
/**
* @author Spencer Gibb
*/
@Data
@RequiredArgsConstructor
@AllArgsConstructor
public class RibbonCommandContext {
@NonNull
private final String serviceId;
@NonNull
private final String method;
@NonNull
private final String uri;
private final Boolean retryable;
@NonNull
private final MultiValueMap<String, String> headers;
@NonNull
private final MultiValueMap<String, String> params;
private final InputStream requestEntity;
@NonNull
private final List<RibbonRequestCustomizer> requestCustomizers;
private Long contentLength;
public RibbonCommandContext(String serviceId, String method, String uri, Boolean retryable,
MultiValueMap<String, String> headers, MultiValueMap<String, String> params,
InputStream requestEntity) {
this(serviceId, method, uri, retryable, headers, params, requestEntity,
new ArrayList<RibbonRequestCustomizer>(), null);
}
public URI uri() {
try {
return new URI(this.uri);
......
......@@ -18,11 +18,13 @@ package org.springframework.cloud.netflix.zuul.filters.route;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer;
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
......@@ -42,15 +44,18 @@ public class RibbonRoutingFilter extends ZuulFilter {
private static final String ERROR_STATUS_CODE = "error.status_code";
protected ProxyRequestHelper helper;
protected RibbonCommandFactory<?> ribbonCommandFactory;
protected List<RibbonRequestCustomizer> requestCustomizers;
public RibbonRoutingFilter(ProxyRequestHelper helper,
RibbonCommandFactory<?> ribbonCommandFactory) {
RibbonCommandFactory<?> ribbonCommandFactory,
List<RibbonRequestCustomizer> requestCustomizers) {
this.helper = helper;
this.ribbonCommandFactory = ribbonCommandFactory;
this.requestCustomizers = requestCustomizers;
}
public RibbonRoutingFilter(RibbonCommandFactory<?> ribbonCommandFactory) {
this(new ProxyRequestHelper(), ribbonCommandFactory);
this(new ProxyRequestHelper(), ribbonCommandFactory, null);
}
@Override
......@@ -115,7 +120,7 @@ public class RibbonRoutingFilter extends ZuulFilter {
uri = uri.replace("//", "/");
return new RibbonCommandContext(serviceId, verb, uri, retryable, headers, params,
requestEntity);
requestEntity, this.requestCustomizers);
}
protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
......
......@@ -29,12 +29,15 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Collections;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.junit.Test;
import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.StreamUtils;
......@@ -85,7 +88,21 @@ public class RibbonApacheHttpRequestTests {
headers.add("Content-Length", lengthString);
length = (long) entityValue.length();
}
RibbonCommandContext context = new RibbonCommandContext("example", method, uri.toString(), false, headers, new LinkedMultiValueMap<String, String>(), requestEntity);
RibbonRequestCustomizer requestCustomizer = new RibbonRequestCustomizer<RequestBuilder>() {
@Override
public boolean accepts(Class builderClass) {
return builderClass == RequestBuilder.class;
}
@Override
public void customize(RequestBuilder builder) {
builder.addHeader("from-customizer", "foo");
}
};
RibbonCommandContext context = new RibbonCommandContext("example", method,
uri.toString(), false, headers, new LinkedMultiValueMap<String, String>(),
requestEntity, Collections.singletonList(requestCustomizer));
context.setContentLength(length);
RibbonApacheHttpRequest httpRequest = new RibbonApacheHttpRequest(context);
......@@ -98,6 +115,9 @@ public class RibbonApacheHttpRequestTests {
assertThat("Content-Length is wrong", request.getFirstHeader("Content-Length").getValue(),
is(equalTo(lengthString)));
}
assertThat("from-customizer is missing", request.getFirstHeader("from-customizer"), is(notNullValue()));
assertThat("from-customizer is wrong", request.getFirstHeader("from-customizer").getValue(),
is(equalTo("foo")));
HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) request;
assertThat("entity is missing", entityRequest.getEntity(), is(notNullValue()));
......@@ -108,3 +128,4 @@ public class RibbonApacheHttpRequestTests {
assertThat("content is wrong", string, is(equalTo(entityValue)));
}
}
......@@ -26,8 +26,10 @@ import static org.junit.Assert.assertThat;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collections;
import org.junit.Test;
import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import org.springframework.util.LinkedMultiValueMap;
......@@ -80,8 +82,20 @@ public class OkHttpRibbonRequestTests {
headers.add("Content-Length", lengthString);
length = (long) entityValue.length();
}
RibbonRequestCustomizer requestCustomizer = new RibbonRequestCustomizer<Request.Builder>() {
@Override
public boolean accepts(Class builderClass) {
return builderClass == Request.Builder.class;
}
@Override
public void customize(Request.Builder builder) {
builder.addHeader("from-customizer", "foo");
}
};
RibbonCommandContext context = new RibbonCommandContext("example", method, uri, false,
headers, new LinkedMultiValueMap<String, String>(), requestEntity);
headers, new LinkedMultiValueMap<String, String>(), requestEntity, Collections.singletonList(requestCustomizer));
context.setContentLength(length);
OkHttpRibbonRequest httpRequest = new OkHttpRibbonRequest(context);
......@@ -92,6 +106,8 @@ public class OkHttpRibbonRequestTests {
assertThat("Content-Length is wrong", request.header("Content-Length"),
is(equalTo(lengthString)));
}
assertThat("from-customizer is wrong", request.header("from-customizer"),
is(equalTo("foo")));
if (!method.equalsIgnoreCase("get")) {
assertThat("body is null", request.body(), is(notNullValue()));
......@@ -104,3 +120,4 @@ public class OkHttpRibbonRequestTests {
}
}
}
/*
* Copyright 2013-2016 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.netflix.zuul.filters.route;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Collections;
import org.junit.Test;
import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.StreamUtils;
import com.netflix.client.http.HttpRequest;
/**
* @author Spencer Gibb
*/
public class RestClientRibbonCommandTests {
@Test
public void testNullEntity() throws Exception {
String uri = "http://example.com";
LinkedMultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("my-header", "my-value");
LinkedMultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("myparam", "myparamval");
RestClientRibbonCommand command = new RestClientRibbonCommand("cmd", null, new RibbonCommandContext("example", "GET", uri, false,
headers, params, null));
HttpRequest request = command.createRequest();
assertThat("uri is wrong", request.getUri().toString(), startsWith(uri));
assertThat("my-header is wrong", request.getHttpHeaders().getFirstValue("my-header"), is(equalTo("my-value")));
assertThat("myparam is missing", request.getQueryParams().get("myparam").iterator().next(), is(equalTo("myparamval")));
}
@Test
// this situation happens, see https://github.com/spring-cloud/spring-cloud-netflix/issues/1042#issuecomment-227723877
public void testEmptyEntityGet() throws Exception {
String entityValue = "";
testEntity(entityValue, new ByteArrayInputStream(entityValue.getBytes()), false, "GET");
}
@Test
public void testNonEmptyEntityPost() throws Exception {
String entityValue = "abcd";
testEntity(entityValue, new ByteArrayInputStream(entityValue.getBytes()), true, "POST");
}
void testEntity(String entityValue, ByteArrayInputStream requestEntity, boolean addContentLengthHeader, String method) throws Exception {
String lengthString = String.valueOf(entityValue.length());
Long length = null;
URI uri = URI.create("http://example.com");
LinkedMultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
if (addContentLengthHeader) {
headers.add("Content-Length", lengthString);
length = (long) entityValue.length();
}
RibbonRequestCustomizer requestCustomizer = new RibbonRequestCustomizer<HttpRequest.Builder>() {
@Override
public boolean accepts(Class builderClass) {
return builderClass == HttpRequest.Builder.class;
}
@Override
public void customize(HttpRequest.Builder builder) {
builder.header("from-customizer", "foo");
}
};
RibbonCommandContext context = new RibbonCommandContext("example", method,
uri.toString(), false, headers, new LinkedMultiValueMap<String, String>(),
requestEntity, Collections.singletonList(requestCustomizer));
context.setContentLength(length);
RestClientRibbonCommand command = new RestClientRibbonCommand("cmd", null, context);
HttpRequest request = command.createRequest();
assertThat("uri is wrong", request.getUri().toString(), startsWith(uri.toString()));
if (addContentLengthHeader) {
assertThat("Content-Length is wrong", request.getHttpHeaders().getFirstValue("Content-Length"),
is(equalTo(lengthString)));
}
assertThat("from-customizer is wrong", request.getHttpHeaders().getFirstValue("from-customizer"),
is(equalTo("foo")));
assertThat("entity is missing", request.getEntity(), is(notNullValue()));
assertThat("entity is wrong type", InputStream.class.isAssignableFrom(request.getEntity().getClass()), is(true));
InputStream entity = (InputStream) request.getEntity();
String string = StreamUtils.copyToString(entity, Charset.forName("UTF-8"));
assertThat("content is wrong", string, is(equalTo(entityValue)));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment