Adds zuul okhttp support & zuul refactor.

Adds a OkHttpRibbonCommand and accomanying classes. They should be
considered beta.

Refactors zuul ribbon command classes and tests.

fixes gh-1125
This commit is contained in:
Spencer Gibb
2016-06-22 15:47:13 -06:00
parent fd36a0cc72
commit 7ac81f8fd5
30 changed files with 1582 additions and 776 deletions

View File

@@ -16,7 +16,6 @@
package org.springframework.cloud.netflix.ribbon.apache;
import java.io.InputStream;
import java.net.URI;
import java.util.List;
@@ -24,9 +23,8 @@ import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.entity.BasicHttpEntity;
import org.springframework.util.MultiValueMap;
import com.netflix.client.ClientRequest;
import org.springframework.cloud.netflix.ribbon.support.ContextAwareRequest;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import lombok.Getter;
@@ -34,62 +32,38 @@ import lombok.Getter;
* @author Christian Lohmann
*/
@Getter
public class RibbonApacheHttpRequest extends ClientRequest implements Cloneable {
public class RibbonApacheHttpRequest extends ContextAwareRequest implements Cloneable {
private final String method;
private Long contentLength;
private final MultiValueMap<String, String> headers;
private final MultiValueMap<String, String> params;
private final InputStream requestEntity;
public RibbonApacheHttpRequest(final String method, final URI uri,
final Boolean retryable, final MultiValueMap<String, String> headers,
final MultiValueMap<String, String> params, final InputStream requestEntity) {
this(method, uri, retryable, headers, params, requestEntity, null);
}
public RibbonApacheHttpRequest(final String method, final URI uri,
final Boolean retryable, final MultiValueMap<String, String> headers,
final MultiValueMap<String, String> params, final InputStream requestEntity, Long contentLength) {
this.method = method;
this.contentLength = contentLength;
this.uri = uri;
this.isRetriable = retryable;
this.headers = headers;
this.params = params;
this.requestEntity = requestEntity;
public RibbonApacheHttpRequest(RibbonCommandContext context) {
super(context);
}
public HttpUriRequest toRequest(final RequestConfig requestConfig) {
final RequestBuilder builder = RequestBuilder.create(this.method);
final RequestBuilder builder = RequestBuilder.create(this.context.getMethod());
builder.setUri(this.uri);
for (final String name : this.headers.keySet()) {
final List<String> values = this.headers.get(name);
for (final String name : this.context.getHeaders().keySet()) {
final List<String> values = this.context.getHeaders().get(name);
for (final String value : values) {
builder.addHeader(name, value);
}
}
for (final String name : this.params.keySet()) {
final List<String> values = this.params.get(name);
for (final String name : this.context.getParams().keySet()) {
final List<String> values = this.context.getParams().get(name);
for (final String value : values) {
builder.addParameter(name, value);
}
}
if (this.requestEntity != null) {
if (this.context.getRequestEntity() != null) {
final BasicHttpEntity entity;
entity = new BasicHttpEntity();
entity.setContent(this.requestEntity);
entity.setContent(this.context.getRequestEntity());
// if the entity contentLength isn't set, transfer-encoding will be set
// to chunked in org.apache.http.protocol.RequestContent. See gh-1042
if (contentLength != null) {
entity.setContentLength(this.contentLength);
} else if ("GET".equals(this.method)) {
if (this.context.getContentLength() != null) {
entity.setContentLength(this.context.getContentLength());
} else if ("GET".equals(this.context.getMethod())) {
entity.setContentLength(0);
}
builder.setEntity(entity);
@@ -99,9 +73,8 @@ public class RibbonApacheHttpRequest extends ClientRequest implements Cloneable
return builder.build();
}
public RibbonApacheHttpRequest withNewUri(final URI uri) {
return new RibbonApacheHttpRequest(this.method, uri, this.isRetriable,
this.headers, this.params, this.requestEntity);
public RibbonApacheHttpRequest withNewUri(URI uri) {
return new RibbonApacheHttpRequest(newContext(uri));
}
}

View File

@@ -23,13 +23,10 @@ import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.cloud.netflix.ribbon.support.AbstractLoadBalancingClient;
import org.springframework.web.util.UriComponentsBuilder;
import com.netflix.client.AbstractLoadBalancerAwareClient;
import com.netflix.client.RequestSpecificRetryHandler;
import com.netflix.client.RetryHandler;
import com.netflix.client.config.CommonClientConfigKey;
import com.netflix.client.config.DefaultClientConfigImpl;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer;
@@ -38,64 +35,15 @@ import com.netflix.loadbalancer.ILoadBalancer;
*/
public class RibbonLoadBalancingHttpClient
extends
AbstractLoadBalancerAwareClient<RibbonApacheHttpRequest, RibbonApacheHttpResponse> {
AbstractLoadBalancingClient<RibbonApacheHttpRequest, RibbonApacheHttpResponse> {
private final HttpClient delegate = HttpClientBuilder.create().build();
private int connectTimeout;
private int readTimeout;
private boolean secure;
private boolean followRedirects;
private boolean okToRetryOnAllOperations;
public RibbonLoadBalancingHttpClient() {
super(null);
this.setRetryHandler(RetryHandler.DEFAULT);
super();
}
public RibbonLoadBalancingHttpClient(final ILoadBalancer lb) {
super(lb);
this.setRetryHandler(RetryHandler.DEFAULT);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
super.initWithNiwsConfig(clientConfig);
this.connectTimeout = clientConfig.getPropertyAsInteger(
CommonClientConfigKey.ConnectTimeout,
DefaultClientConfigImpl.DEFAULT_CONNECT_TIMEOUT);
this.readTimeout = clientConfig.getPropertyAsInteger(
CommonClientConfigKey.ReadTimeout,
DefaultClientConfigImpl.DEFAULT_READ_TIMEOUT);
this.secure = clientConfig.getPropertyAsBoolean(CommonClientConfigKey.IsSecure,
false);
this.followRedirects = clientConfig.getPropertyAsBoolean(
CommonClientConfigKey.FollowRedirects,
DefaultClientConfigImpl.DEFAULT_FOLLOW_REDIRECTS);
this.okToRetryOnAllOperations = clientConfig.getPropertyAsBoolean(
CommonClientConfigKey.OkToRetryOnAllOperations,
DefaultClientConfigImpl.DEFAULT_OK_TO_RETRY_ON_ALL_OPERATIONS);
}
@Override
public RequestSpecificRetryHandler getRequestSpecificRetryHandler(
final RibbonApacheHttpRequest request, final IClientConfig requestConfig) {
if (this.okToRetryOnAllOperations) {
return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(),
requestConfig);
}
if (!request.getMethod().equals("GET")) {
return new RequestSpecificRetryHandler(true, false, this.getRetryHandler(),
requestConfig);
}
else {
return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(),
requestConfig);
}
}
@Override
@@ -129,8 +77,4 @@ public class RibbonLoadBalancingHttpClient
return new RibbonApacheHttpResponse(httpResponse, httpUriRequest.getURI());
}
private boolean isSecure(final IClientConfig config) {
return (config != null) ? config.get(CommonClientConfigKey.IsSecure)
: this.secure;
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2013-2015 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.okhttp;
import java.net.URI;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.cloud.netflix.ribbon.support.AbstractLoadBalancingClient;
import org.springframework.web.util.UriComponentsBuilder;
import com.netflix.client.config.CommonClientConfigKey;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer;
/**
* @author Spencer Gibb
*/
public class OkHttpLoadBalancingClient
extends AbstractLoadBalancingClient<OkHttpRibbonRequest, OkHttpRibbonResponse> {
private final OkHttpClient delegate = new OkHttpClient();
public OkHttpLoadBalancingClient() {
super();
}
public OkHttpLoadBalancingClient(final ILoadBalancer lb) {
super(lb);
}
@Override
public OkHttpRibbonResponse execute(OkHttpRibbonRequest ribbonRequest,
final IClientConfig configOverride) throws Exception {
boolean secure = isSecure(configOverride);
if (secure) {
final URI secureUri = UriComponentsBuilder.fromUri(ribbonRequest.getUri())
.scheme("https").build().toUri();
ribbonRequest = ribbonRequest.withNewUri(secureUri);
}
OkHttpClient httpClient = getOkHttpClient(configOverride, secure);
final Request request = ribbonRequest.toRequest();
Response response = httpClient.newCall(request).execute();
return new OkHttpRibbonResponse(response, ribbonRequest.getUri());
}
OkHttpClient getOkHttpClient(IClientConfig configOverride, boolean secure) {
OkHttpClient.Builder builder = this.delegate.newBuilder();
if (configOverride != null) {
builder.connectTimeout(configOverride.get(
CommonClientConfigKey.ConnectTimeout, this.connectTimeout), TimeUnit.MILLISECONDS);
builder.readTimeout(configOverride.get(
CommonClientConfigKey.ReadTimeout, this.readTimeout), TimeUnit.MILLISECONDS);
builder.followRedirects(configOverride.get(
CommonClientConfigKey.FollowRedirects, this.followRedirects));
if (secure) {
builder.followSslRedirects(configOverride.get(
CommonClientConfigKey.FollowRedirects, this.followRedirects));
}
}
else {
builder.connectTimeout(this.connectTimeout, TimeUnit.MILLISECONDS);
builder.readTimeout(this.readTimeout, TimeUnit.MILLISECONDS);
builder.followRedirects(this.followRedirects);
if (secure) {
builder.followSslRedirects(this.followRedirects);
}
}
return builder.build();
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2013-2015 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.okhttp;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.List;
import org.springframework.cloud.netflix.ribbon.support.ContextAwareRequest;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import lombok.Getter;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.internal.http.HttpMethod;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
/**
* @author Spencer Gibb
*/
@Getter
public class OkHttpRibbonRequest extends ContextAwareRequest implements Cloneable {
public OkHttpRibbonRequest(RibbonCommandContext context) {
super(context);
}
public Request toRequest() {
Headers.Builder headers = new Headers.Builder();
for (String name : this.context.getHeaders().keySet()) {
List<String> values = this.context.getHeaders().get(name);
for (String value : values) {
headers.add(name, value);
}
}
HttpUrl.Builder url = HttpUrl.get(this.uri).newBuilder();
for (String name : this.context.getParams().keySet()) {
List<String> values = this.context.getParams().get(name);
for (String value : values) {
url.addQueryParameter(name, value);
}
}
RequestBody requestBody = null;
if (this.context.getRequestEntity() != null && HttpMethod.permitsRequestBody(this.context.getMethod())) {
MediaType mediaType = null;
if (headers.get("Content-Type") != null) {
mediaType = MediaType.parse(headers.get("Content-Type"));
}
requestBody = new InputStreamRequestBody(this.context.getRequestEntity(), mediaType, this.context.getContentLength());
}
return new Request.Builder()
.url(url.build())
.headers(headers.build())
.method(this.context.getMethod(), requestBody)
.build();
}
public OkHttpRibbonRequest withNewUri(final URI uri) {
return new OkHttpRibbonRequest(newContext(uri));
}
static class InputStreamRequestBody extends RequestBody {
private InputStream inputStream;
private MediaType mediaType;
private Long contentLength;
InputStreamRequestBody(InputStream inputStream, MediaType mediaType, Long contentLength) {
this.inputStream = inputStream;
this.mediaType = mediaType;
this.contentLength = contentLength;
}
@Override
public MediaType contentType() {
return mediaType;
}
@Override
public long contentLength() {
if (contentLength != null) {
return contentLength;
}
try {
return inputStream.available();
} catch (IOException e) {
return 0;
}
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
Source source = null;
try {
source = Okio.source(inputStream);
sink.writeAll(source);
} finally {
if (source != null) {
source.close();
}
}
}
}
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2013-2015 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.okhttp;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.util.Assert;
import com.google.common.reflect.TypeToken;
import com.netflix.client.ClientException;
import com.netflix.client.http.CaseInsensitiveMultiMap;
import com.netflix.client.http.HttpHeaders;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* @author Spencer Gibb
*/
public class OkHttpRibbonResponse implements com.netflix.client.http.HttpResponse {
private final ResponseBody body;
private URI uri;
private Response response;
public OkHttpRibbonResponse(Response response, URI uri) {
Assert.notNull(response, "response can not be null");
this.response = response;
this.body = response.body();
this.uri = uri;
}
@Override
public int getStatus() {
return this.response.code();
}
@Override
public String getStatusLine() {
return this.response.message();
}
@Override
public Object getPayload() throws ClientException {
if (!hasPayload()) {
return null;
}
return this.body.byteStream();
}
@Override
public boolean hasPayload() {
return this.body != null;
}
@Override
public boolean isSuccess() {
return this.response.isSuccessful();
}
@Override
public URI getRequestedURI() {
return this.uri;
}
@Override
public Map<String, Collection<String>> getHeaders() {
final Map<String, Collection<String>> headers = new HashMap<>();
for (Map.Entry<String,List<String>> entry : this.response.headers().toMultimap().entrySet()) {
String name = entry.getKey();
for (String value : entry.getValue()) {
if (headers.containsKey(name)) {
headers.get(name).add(value);
} else {
final List<String> values = new ArrayList<>();
values.add(value);
headers.put(name, values);
}
}
}
return headers;
}
@Override
public HttpHeaders getHttpHeaders() {
final CaseInsensitiveMultiMap headers = new CaseInsensitiveMultiMap();
for (Map.Entry<String,List<String>> entry : this.response.headers().toMultimap().entrySet()) {
for (String value : entry.getValue()) {
headers.addHeader(entry.getKey(), value);
}
}
return headers;
}
@Override
public void close() {
this.response.close();
}
@Override
public InputStream getInputStream() {
if (this.body == null) {
return null;
}
return this.body.byteStream();
}
@Override
public boolean hasEntity() {
return hasPayload();
}
@Override
public <T> T getEntity(Class<T> type) throws Exception {
return null;
}
@Override
public <T> T getEntity(Type type) throws Exception {
return null;
}
@Override
public <T> T getEntity(TypeToken<T> type) throws Exception {
return null;
}
}

View File

@@ -0,0 +1,97 @@
/*
* 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 com.netflix.client.AbstractLoadBalancerAwareClient;
import com.netflix.client.IResponse;
import com.netflix.client.RequestSpecificRetryHandler;
import com.netflix.client.RetryHandler;
import com.netflix.client.config.CommonClientConfigKey;
import com.netflix.client.config.DefaultClientConfigImpl;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer;
/**
* @author Spencer Gibb
*/
public abstract class AbstractLoadBalancingClient<S extends ContextAwareRequest, T extends IResponse> extends
AbstractLoadBalancerAwareClient<S, T> {
protected int connectTimeout;
protected int readTimeout;
protected boolean secure;
protected boolean followRedirects;
protected boolean okToRetryOnAllOperations;
public AbstractLoadBalancingClient() {
super(null);
this.setRetryHandler(RetryHandler.DEFAULT);
}
public AbstractLoadBalancingClient(final ILoadBalancer lb) {
super(lb);
this.setRetryHandler(RetryHandler.DEFAULT);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
super.initWithNiwsConfig(clientConfig);
this.connectTimeout = clientConfig.getPropertyAsInteger(
CommonClientConfigKey.ConnectTimeout,
DefaultClientConfigImpl.DEFAULT_CONNECT_TIMEOUT);
this.readTimeout = clientConfig.getPropertyAsInteger(
CommonClientConfigKey.ReadTimeout,
DefaultClientConfigImpl.DEFAULT_READ_TIMEOUT);
this.secure = clientConfig.getPropertyAsBoolean(CommonClientConfigKey.IsSecure,
false);
this.followRedirects = clientConfig.getPropertyAsBoolean(
CommonClientConfigKey.FollowRedirects,
DefaultClientConfigImpl.DEFAULT_FOLLOW_REDIRECTS);
this.okToRetryOnAllOperations = clientConfig.getPropertyAsBoolean(
CommonClientConfigKey.OkToRetryOnAllOperations,
DefaultClientConfigImpl.DEFAULT_OK_TO_RETRY_ON_ALL_OPERATIONS);
}
@Override
public RequestSpecificRetryHandler getRequestSpecificRetryHandler(
final S request, final IClientConfig requestConfig) {
if (this.okToRetryOnAllOperations) {
return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(),
requestConfig);
}
if (!request.getContext().getMethod().equals("GET")) {
return new RequestSpecificRetryHandler(true, false, this.getRetryHandler(),
requestConfig);
}
else {
return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(),
requestConfig);
}
}
protected boolean isSecure(final IClientConfig config) {
return (config != null) ? config.get(CommonClientConfigKey.IsSecure)
: this.secure;
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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 com.netflix.client.ClientRequest;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import java.net.URI;
/**
* @author Spencer Gibb
*/
public abstract class ContextAwareRequest extends ClientRequest {
protected final RibbonCommandContext context;
public ContextAwareRequest(RibbonCommandContext context) {
this.context = context;
this.uri = context.uri();
this.isRetriable = context.getRetryable();
}
public RibbonCommandContext getContext() {
return context;
}
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());
return commandContext;
}
}

View File

@@ -36,7 +36,8 @@ 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.RestClientRibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.okhttp.OkHttpRibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.restclient.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;
@@ -95,6 +96,17 @@ public class ZuulProxyConfiguration extends ZuulConfiguration {
}
}
@Configuration
@ConditionalOnProperty("zuul.ribbon.okhttp.enabled")
@ConditionalOnClass(name = "okhttp3.OkHttpClient")
protected static class OkHttpRibbonConfiguration {
@Bean
@ConditionalOnMissingBean
public RibbonCommandFactory<?> ribbonCommandFactory(SpringClientFactory clientFactory) {
return new OkHttpRibbonCommandFactory(clientFactory);
}
}
// pre filters
@Bean
public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator,

View File

@@ -249,14 +249,7 @@ public class ProxyRequestHelper {
Map<String, Object> input = new LinkedHashMap<>();
trace.put("request", input);
info.put("headers", trace);
for (Entry<String, List<String>> entry : headers.entrySet()) {
Collection<String> collection = entry.getValue();
Object value = collection;
if (collection.size() < 2) {
value = collection.isEmpty() ? "" : collection.iterator().next();
}
input.put(entry.getKey(), value);
}
transformHeaders(headers, input);
RequestContext ctx = RequestContext.getCurrentContext();
if (shouldDebugBody(ctx)) {
// Prevent input stream from being read if it needs to go downstream
@@ -287,20 +280,24 @@ public class ProxyRequestHelper {
if (this.traces != null) {
@SuppressWarnings("unchecked")
Map<String, Object> trace = (Map<String, Object>) info.get("headers");
Map<String, Object> output = new LinkedHashMap<String, Object>();
Map<String, Object> output = new LinkedHashMap<>();
trace.put("response", output);
for (Entry<String, List<String>> key : headers.entrySet()) {
Collection<String> collection = key.getValue();
Object value = collection;
if (collection.size() < 2) {
value = collection.isEmpty() ? "" : collection.iterator().next();
}
output.put(key.getKey(), value);
}
transformHeaders(headers, output);
output.put("status", "" + status);
}
}
void transformHeaders(MultiValueMap<String, String> headers, Map<String, Object> output) {
for (Entry<String, List<String>> key : headers.entrySet()) {
Collection<String> collection = key.getValue();
Object value = collection;
if (collection.size() < 2) {
value = collection.isEmpty() ? "" : collection.iterator().next();
}
output.put(key.getKey(), value);
}
}
private void debugRequestEntity(Map<String, Object> info, InputStream inputStream)
throws IOException {
if (RequestContext.getCurrentContext().isChunkedRequestBody()) {

View File

@@ -1,183 +0,0 @@
/*
* Copyright 2013-2015 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 java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import org.springframework.cloud.netflix.ribbon.RibbonHttpResponse;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.MultiValueMap;
import com.netflix.client.http.HttpRequest;
import com.netflix.client.http.HttpRequest.Builder;
import com.netflix.client.http.HttpRequest.Verb;
import com.netflix.client.http.HttpResponse;
import com.netflix.config.DynamicIntProperty;
import com.netflix.config.DynamicPropertyFactory;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.hystrix.HystrixCommandProperties.ExecutionIsolationStrategy;
import com.netflix.niws.client.http.RestClient;
import com.netflix.zuul.constants.ZuulConstants;
import com.netflix.zuul.context.RequestContext;
/**
* Hystrix wrapper around Eureka Ribbon command
*
* see original
* https://github.com/Netflix/zuul/blob/master/zuul-netflix/src/main/java/com/
* netflix/zuul/dependency/ribbon/hystrix/RibbonCommand.java
*/
@SuppressWarnings("deprecation")
public class RestClientRibbonCommand extends HystrixCommand<ClientHttpResponse> implements RibbonCommand {
private RestClient restClient;
private Verb verb;
private URI uri;
private Boolean retryable;
private MultiValueMap<String, String> headers;
private MultiValueMap<String, String> params;
private InputStream requestEntity;
public RestClientRibbonCommand(RestClient restClient, Verb verb, String uri,
Boolean retryable,
MultiValueMap<String, String> headers,
MultiValueMap<String, String> params, InputStream requestEntity)
throws URISyntaxException {
this("default", restClient, verb, uri, retryable , headers, params, requestEntity);
}
public RestClientRibbonCommand(String commandKey, RestClient restClient, Verb verb, String uri,
Boolean retryable,
MultiValueMap<String, String> headers,
MultiValueMap<String, String> params, InputStream requestEntity)
throws URISyntaxException {
super(getSetter(commandKey));
this.restClient = restClient;
this.verb = verb;
this.uri = new URI(uri);
this.retryable = retryable;
this.headers = headers;
this.params = params;
this.requestEntity = requestEntity;
}
protected static HystrixCommand.Setter getSetter(String commandKey) {
// we want to default to semaphore-isolation since this wraps
// 2 others commands that are already thread isolated
String name = ZuulConstants.ZUUL_EUREKA + commandKey + ".semaphore.maxSemaphores";
DynamicIntProperty value = DynamicPropertyFactory.getInstance().getIntProperty(
name, 100);
HystrixCommandProperties.Setter setter = HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)
.withExecutionIsolationSemaphoreMaxConcurrentRequests(value.get());
return Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RibbonCommand"))
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey))
.andCommandPropertiesDefaults(setter);
}
@Override
protected ClientHttpResponse run() throws Exception {
return forward();
}
protected ClientHttpResponse forward() throws Exception {
RequestContext context = RequestContext.getCurrentContext();
Builder builder = HttpRequest.newBuilder().verb(this.verb).uri(this.uri)
.entity(this.requestEntity);
if(this.retryable != null) {
builder.setRetriable(this.retryable);
}
for (String name : this.headers.keySet()) {
List<String> values = this.headers.get(name);
for (String value : values) {
builder.header(name, value);
}
}
for (String name : this.params.keySet()) {
List<String> values = this.params.get(name);
for (String value : values) {
builder.queryParams(name, value);
}
}
customizeRequest(builder);
HttpRequest httpClientRequest = builder.build();
HttpResponse response = this.restClient
.executeWithLoadBalancer(httpClientRequest);
context.set("ribbonResponse", response);
// Explicitly close the HttpResponse if the Hystrix command timed out to
// release the underlying HTTP connection held by the response.
//
if( this.isResponseTimedOut() ) {
if( response!= null ) {
response.close();
}
}
RibbonHttpResponse ribbonHttpResponse = new RibbonHttpResponse(response);
return ribbonHttpResponse;
}
protected void customizeRequest(Builder requestBuilder) {
}
protected MultiValueMap<String, String> getHeaders() {
return this.headers;
}
protected MultiValueMap<String, String> getParams() {
return this.params;
}
protected InputStream getRequestEntity() {
return this.requestEntity;
}
protected RestClient getRestClient() {
return this.restClient;
}
protected Boolean getRetryable() {
return this.retryable;
}
protected URI getUri() {
return this.uri;
}
protected Verb getVerb() {
return this.verb;
}
}

View File

@@ -1,63 +0,0 @@
/*
* Copyright 2013-2015 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 org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import com.netflix.client.http.HttpRequest;
import com.netflix.niws.client.http.RestClient;
import lombok.SneakyThrows;
/**
* @author Spencer Gibb
*/
public class RestClientRibbonCommandFactory implements RibbonCommandFactory<RestClientRibbonCommand> {
private final SpringClientFactory clientFactory;
public RestClientRibbonCommandFactory(SpringClientFactory clientFactory) {
this.clientFactory = clientFactory;
}
@Override
@SuppressWarnings("deprecation")
@SneakyThrows
public RestClientRibbonCommand create(RibbonCommandContext context) {
RestClient restClient = this.clientFactory.getClient(context.getServiceId(),
RestClient.class);
return new RestClientRibbonCommand(
context.getServiceId(), restClient, getVerb(context.getVerb()),
context.getUri(), context.getRetryable(), context.getHeaders(),
context.getParams(), context.getRequestEntity());
}
protected SpringClientFactory getClientFactory() {
return this.clientFactory;
}
protected static HttpRequest.Verb getVerb(String sMethod) {
if (sMethod == null)
return HttpRequest.Verb.GET;
try {
return HttpRequest.Verb.valueOf(sMethod.toUpperCase());
}
catch (IllegalArgumentException e) {
return HttpRequest.Verb.GET;
}
}
}

View File

@@ -16,21 +16,35 @@
package org.springframework.cloud.netflix.zuul.filters.route;
import lombok.Value;
import org.springframework.util.MultiValueMap;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
import lombok.Data;
/**
* @author Spencer Gibb
*/
@Value
@Data
public class RibbonCommandContext {
private final String serviceId;
private final String verb;
private final String method;
private final String uri;
private final Boolean retryable;
private final MultiValueMap<String, String> headers;
private final MultiValueMap<String, String> params;
private final InputStream requestEntity;
private Long contentLength;
public URI uri() {
try {
return new URI(this.uri);
} catch (URISyntaxException e) {
ReflectionUtils.rethrowRuntimeException(e);
}
return null;
}
}

View File

@@ -119,7 +119,7 @@ public class RibbonRoutingFilter extends ZuulFilter {
}
protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
Map<String, Object> info = this.helper.debug(context.getVerb(), context.getUri(),
Map<String, Object> info = this.helper.debug(context.getMethod(), context.getUri(),
context.getHeaders(), context.getParams(), context.getRequestEntity());
RibbonCommand command = this.ribbonCommandFactory.create(context);

View File

@@ -1,129 +0,0 @@
/*
* Copyright 2013-2015 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.apache;
import java.io.InputStream;
import java.net.URI;
import org.springframework.cloud.netflix.ribbon.RibbonHttpResponse;
import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpRequest;
import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpResponse;
import org.springframework.cloud.netflix.ribbon.apache.RibbonLoadBalancingHttpClient;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommand;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.MultiValueMap;
import com.netflix.config.DynamicIntProperty;
import com.netflix.config.DynamicPropertyFactory;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.zuul.constants.ZuulConstants;
import com.netflix.zuul.context.RequestContext;
import org.springframework.util.StringUtils;
/**
* @author Christian Lohmann
*/
public class HttpClientRibbonCommand extends HystrixCommand<ClientHttpResponse> implements
RibbonCommand {
private final RibbonLoadBalancingHttpClient client;
private final String method;
private final String uri;
private final MultiValueMap<String, String> headers;
private final MultiValueMap<String, String> params;
private final InputStream requestEntity;
private final Boolean retryable;
public HttpClientRibbonCommand(final RibbonLoadBalancingHttpClient client,
final String method, final String uri,
final MultiValueMap<String, String> headers,
final MultiValueMap<String, String> params, final InputStream requestEntity,
final Boolean retryable) {
this("default", client, method, uri, headers, params, requestEntity, retryable);
}
public HttpClientRibbonCommand(final String commandKey,
final RibbonLoadBalancingHttpClient client, final String method,
final String uri, final MultiValueMap<String, String> headers,
final MultiValueMap<String, String> params, final InputStream requestEntity,
final Boolean retryable) {
super(getSetter(commandKey));
this.client = client;
this.method = method;
this.uri = uri;
this.headers = headers;
this.params = params;
this.requestEntity = requestEntity;
this.retryable = retryable;
}
protected static Setter getSetter(final String commandKey) {
// we want to default to semaphore-isolation since this wraps
// 2 others commands that are already thread isolated
final String name = ZuulConstants.ZUUL_EUREKA + commandKey
+ ".semaphore.maxSemaphores";
final DynamicIntProperty value = DynamicPropertyFactory.getInstance()
.getIntProperty(name, 100);
final HystrixCommandProperties.Setter setter = HystrixCommandProperties
.Setter()
.withExecutionIsolationStrategy(
HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
.withExecutionIsolationSemaphoreMaxConcurrentRequests(value.get());
return Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RibbonCommand"))
.andCommandKey(
HystrixCommandKey.Factory.asKey(commandKey + "RibbonCommand"))
.andCommandPropertiesDefaults(setter);
}
@Override
protected ClientHttpResponse run() throws Exception {
return forward();
}
protected ClientHttpResponse forward() throws Exception {
final RequestContext context = RequestContext.getCurrentContext();
Long contentLength = null;
String contentLengthHeader = context.getRequest().getHeader("Content-Length");
if (StringUtils.hasText(contentLengthHeader)) {
contentLength = new Long(contentLengthHeader);
}
URI uriInstance = new URI(this.uri);
RibbonApacheHttpRequest request = new RibbonApacheHttpRequest(this.method,
uriInstance, this.retryable, this.headers, this.params,
this.requestEntity, contentLength);
final RibbonApacheHttpResponse response = this.client
.executeWithLoadBalancer(request);
context.set("ribbonResponse", response);
// Explicitly close the HttpResponse if the Hystrix command timed out to
// release the underlying HTTP connection held by the response.
//
if (this.isResponseTimedOut()) {
if (response != null) {
response.close();
}
}
return new RibbonHttpResponse(response);
}
}

View File

@@ -16,19 +16,22 @@
package org.springframework.cloud.netflix.zuul.filters.route.apache;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpRequest;
import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpResponse;
import org.springframework.cloud.netflix.ribbon.apache.RibbonLoadBalancingHttpClient;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.support.AbstractRibbonCommand;
import lombok.RequiredArgsConstructor;
/**
* @author Christian Lohmann
*/
@RequiredArgsConstructor
public class HttpClientRibbonCommandFactory implements
RibbonCommandFactory<HttpClientRibbonCommand> {
RibbonCommandFactory<HttpClientRibbonCommandFactory.HttpClientRibbonCommand> {
private final SpringClientFactory clientFactory;
@@ -39,10 +42,20 @@ public class HttpClientRibbonCommandFactory implements
serviceId, RibbonLoadBalancingHttpClient.class);
client.setLoadBalancer(this.clientFactory.getLoadBalancer(serviceId));
final HttpClientRibbonCommand httpClientRibbonCommand = new HttpClientRibbonCommand(
serviceId, client, context.getVerb(), context.getUri(),
context.getHeaders(), context.getParams(), context.getRequestEntity(),
context.getRetryable());
return httpClientRibbonCommand;
return new HttpClientRibbonCommand(serviceId, client, context);
}
class HttpClientRibbonCommand extends AbstractRibbonCommand<RibbonLoadBalancingHttpClient, RibbonApacheHttpRequest, RibbonApacheHttpResponse> {
public HttpClientRibbonCommand(final String commandKey,
final RibbonLoadBalancingHttpClient client, RibbonCommandContext context) {
super(commandKey, client, context);
}
@Override
protected RibbonApacheHttpRequest createRequest() throws Exception {
return new RibbonApacheHttpRequest(this.context);
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2013-2015 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.okhttp;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.netflix.ribbon.okhttp.OkHttpLoadBalancingClient;
import org.springframework.cloud.netflix.ribbon.okhttp.OkHttpRibbonRequest;
import org.springframework.cloud.netflix.ribbon.okhttp.OkHttpRibbonResponse;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.support.AbstractRibbonCommand;
import lombok.RequiredArgsConstructor;
/**
* @author Spencer Gibb
*/
@RequiredArgsConstructor
public class OkHttpRibbonCommandFactory implements
RibbonCommandFactory<OkHttpRibbonCommandFactory.OkHttpRibbonCommand> {
private final SpringClientFactory clientFactory;
@Override
public OkHttpRibbonCommand create(final RibbonCommandContext context) {
final String serviceId = context.getServiceId();
final OkHttpLoadBalancingClient client = this.clientFactory.getClient(
serviceId, OkHttpLoadBalancingClient.class);
client.setLoadBalancer(this.clientFactory.getLoadBalancer(serviceId));
return new OkHttpRibbonCommand(serviceId, client, context);
}
class OkHttpRibbonCommand extends AbstractRibbonCommand<OkHttpLoadBalancingClient, OkHttpRibbonRequest, OkHttpRibbonResponse> {
public OkHttpRibbonCommand(final String commandKey,
final OkHttpLoadBalancingClient client, RibbonCommandContext context) {
super(commandKey, client, context);
}
@Override
protected OkHttpRibbonRequest createRequest() throws Exception {
return new OkHttpRibbonRequest(this.context);
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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.restclient;
import com.netflix.client.http.HttpRequest;
import com.netflix.client.http.HttpResponse;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import com.netflix.niws.client.http.RestClient;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.support.AbstractRibbonCommand;
import java.util.List;
/**
* @author Spencer Gibb
*/
public class RestClientRibbonCommandFactory implements RibbonCommandFactory<RestClientRibbonCommandFactory.RestClientRibbonCommand> {
private final SpringClientFactory clientFactory;
public RestClientRibbonCommandFactory(SpringClientFactory clientFactory) {
this.clientFactory = clientFactory;
}
@Override
@SuppressWarnings("deprecation")
public RestClientRibbonCommand create(RibbonCommandContext context) {
RestClient restClient = this.clientFactory.getClient(context.getServiceId(),
RestClient.class);
return new RestClientRibbonCommand(context.getServiceId(), restClient, context);
}
/**
* Hystrix wrapper around Eureka Ribbon command
*
* see original
* https://github.com/Netflix/zuul/blob/master/zuul-netflix/src/main/java/com/
* netflix/zuul/dependency/ribbon/hystrix/RibbonCommand.java
*/
@SuppressWarnings("deprecation")
static class RestClientRibbonCommand extends AbstractRibbonCommand<RestClient, HttpRequest, HttpResponse> {
public RestClientRibbonCommand(String commandKey, RestClient client, RibbonCommandContext context) {
super(commandKey, client, context);
}
@Override
protected HttpRequest createRequest() throws Exception {
HttpRequest.Builder builder = HttpRequest.newBuilder()
.verb(getVerb(this.context.getMethod()))
.uri(this.context.uri())
.entity(this.context.getRequestEntity());
if(this.context.getRetryable() != null) {
builder.setRetriable(this.context.getRetryable());
}
for (String name : this.context.getHeaders().keySet()) {
List<String> values = this.context.getHeaders().get(name);
for (String value : values) {
builder.header(name, value);
}
}
for (String name : this.context.getParams().keySet()) {
List<String> values = this.context.getParams().get(name);
for (String value : values) {
builder.queryParams(name, value);
}
}
return builder.build();
}
}
static HttpRequest.Verb getVerb(String method) {
if (method == null)
return HttpRequest.Verb.GET;
try {
return HttpRequest.Verb.valueOf(method.toUpperCase());
}
catch (IllegalArgumentException e) {
return HttpRequest.Verb.GET;
}
}
}

View File

@@ -0,0 +1,100 @@
/*
* 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.support;
import org.springframework.cloud.netflix.ribbon.RibbonHttpResponse;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommand;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.StringUtils;
import com.netflix.client.AbstractLoadBalancerAwareClient;
import com.netflix.client.ClientRequest;
import com.netflix.client.http.HttpResponse;
import com.netflix.config.DynamicIntProperty;
import com.netflix.config.DynamicPropertyFactory;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.zuul.constants.ZuulConstants;
import com.netflix.zuul.context.RequestContext;
/**
* @author Spencer Gibb
*/
public abstract class AbstractRibbonCommand<LBC extends AbstractLoadBalancerAwareClient<RQ, RS>, RQ extends ClientRequest, RS extends HttpResponse> extends HystrixCommand<ClientHttpResponse> implements
RibbonCommand {
protected final LBC client;
protected RibbonCommandContext context;
public AbstractRibbonCommand(LBC client, RibbonCommandContext context) {
this("default", client, context);
}
public AbstractRibbonCommand(String commandKey, LBC client, RibbonCommandContext context) {
super(getSetter(commandKey));
this.client = client;
this.context = context;
}
protected static Setter getSetter(final String commandKey) {
// we want to default to semaphore-isolation since this wraps
// 2 others commands that are already thread isolated
// @formatter:off
final String name = ZuulConstants.ZUUL_EUREKA + commandKey + ".semaphore.maxSemaphores";
final DynamicIntProperty value = DynamicPropertyFactory.getInstance()
.getIntProperty(name, 100);
final HystrixCommandProperties.Setter setter = HystrixCommandProperties .Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
.withExecutionIsolationSemaphoreMaxConcurrentRequests(value.get());
return Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RibbonCommand"))
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey + "RibbonCommand"))
.andCommandPropertiesDefaults(setter);
// @formatter:on
}
@Override
protected ClientHttpResponse run() throws Exception {
final RequestContext context = RequestContext.getCurrentContext();
String contentLengthHeader = context.getRequest().getHeader("Content-Length");
if (StringUtils.hasText(contentLengthHeader)) {
this.context.setContentLength(new Long(contentLengthHeader));
}
RQ request = createRequest();
RS response = this.client.executeWithLoadBalancer(request);
context.set("ribbonResponse", response);
// Explicitly close the HttpResponse if the Hystrix command timed out to
// release the underlying HTTP connection held by the response.
//
if (this.isResponseTimedOut()) {
if (response != null) {
response.close();
}
}
return new RibbonHttpResponse(response);
}
protected abstract RQ createRequest() throws Exception;
}

View File

@@ -20,6 +20,7 @@ import org.junit.Ignore;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
import org.springframework.cloud.netflix.zuul.filters.route.restclient.RestClientRibbonCommandIntegrationTests;
/**
* A test suite for probing weird ordering problems in the tests.
@@ -29,7 +30,7 @@ import org.junit.runners.Suite.SuiteClasses;
@RunWith(Suite.class)
@SuiteClasses({
org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelperTests.class,
org.springframework.cloud.netflix.zuul.SampleZuulProxyApplicationTests.class,
RestClientRibbonCommandIntegrationTests.class,
org.springframework.cloud.netflix.zuul.FormZuulProxyApplicationTests.class })
@Ignore
public class AdhocTestSuite {

View File

@@ -35,6 +35,7 @@ import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpUriRequest;
import org.junit.Test;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.StreamUtils;
@@ -45,18 +46,18 @@ public class RibbonApacheHttpRequestTests {
@Test
public void testNullEntity() throws Exception {
URI uri = URI.create("http://example.com");
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");
RibbonApacheHttpRequest httpRequest = new RibbonApacheHttpRequest("GET", uri, false,
headers, params, null);
RibbonApacheHttpRequest httpRequest = new RibbonApacheHttpRequest(new RibbonCommandContext("example", "GET", uri, false,
headers, params, null));
HttpUriRequest request = httpRequest.toRequest(RequestConfig.custom().build());
assertThat("request is wrong type", request, is(not(instanceOf(HttpEntityEnclosingRequest.class))));
assertThat("uri is wrong", request.getURI().toString(), startsWith(uri.toString()));
assertThat("uri is wrong", request.getURI().toString(), startsWith(uri));
assertThat("my-header is missing", request.getFirstHeader("my-header"), is(notNullValue()));
assertThat("my-header is wrong", request.getFirstHeader("my-header").getValue(), is(equalTo("my-value")));
assertThat("myparam is missing", request.getURI().getQuery(), is(equalTo("myparam=myparamval")));
@@ -84,9 +85,9 @@ public class RibbonApacheHttpRequestTests {
headers.add("Content-Length", lengthString);
length = (long) entityValue.length();
}
RibbonApacheHttpRequest httpRequest = new RibbonApacheHttpRequest(method, uri, false,
headers, new LinkedMultiValueMap<String, String>(), requestEntity,
length);
RibbonCommandContext context = new RibbonCommandContext("example", method, uri.toString(), false, headers, new LinkedMultiValueMap<String, String>(), requestEntity);
context.setContentLength(length);
RibbonApacheHttpRequest httpRequest = new RibbonApacheHttpRequest(context);
HttpUriRequest request = httpRequest.toRequest(RequestConfig.custom().build());

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2013-2015 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.okhttp;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import org.junit.Test;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.netflix.client.config.CommonClientConfigKey;
import com.netflix.client.config.DefaultClientConfigImpl;
import com.netflix.client.config.IClientConfig;
import okhttp3.OkHttpClient;
/**
* @author Spencer Gibb
*/
public class OkHttpLoadBalancingClientTests {
@Test
public void testOkHttpClientUseDefaultsNoOverride() throws Exception {
OkHttpClient result = getHttpClient(UseDefaults.class, null);
assertThat(result.followRedirects(), is(false));
}
@Test
public void testOkHttpClientDoNotFollowRedirectsNoOverride() throws Exception {
OkHttpClient result = getHttpClient(DoNotFollowRedirects.class, null);
assertThat(result.followRedirects(), is(false));
}
@Test
public void testOkHttpClientFollowRedirectsNoOverride() throws Exception {
OkHttpClient result = getHttpClient(FollowRedirects.class, null);
assertThat(result.followRedirects(), is(true));
}
@Test
public void testOkHttpClientDoNotFollowRedirectsOverrideWithFollowRedirects()
throws Exception {
DefaultClientConfigImpl override = new DefaultClientConfigImpl();
override.set(CommonClientConfigKey.FollowRedirects, true);
override.set(CommonClientConfigKey.IsSecure, false);
OkHttpClient result = getHttpClient(DoNotFollowRedirects.class, override);
assertThat(result.followRedirects(), is(true));
}
@Test
public void testOkHttpClientFollowRedirectsOverrideWithDoNotFollowRedirects()
throws Exception {
DefaultClientConfigImpl override = new DefaultClientConfigImpl();
override.set(CommonClientConfigKey.FollowRedirects, false);
override.set(CommonClientConfigKey.IsSecure, false);
OkHttpClient result = getHttpClient(FollowRedirects.class, override);
assertThat(result.followRedirects(), is(false));
}
private OkHttpClient getHttpClient(Class<?> defaultConfigurationClass,
IClientConfig configOverride) throws Exception {
SpringClientFactory factory = new SpringClientFactory();
factory.setApplicationContext(new AnnotationConfigApplicationContext(
defaultConfigurationClass));
OkHttpLoadBalancingClient client = factory.getClient("service",
OkHttpLoadBalancingClient.class);
return client.getOkHttpClient(configOverride, false);
}
@Configuration
protected static class UseDefaults {
}
@Configuration
protected static class FollowRedirects {
@Bean
public IClientConfig clientConfig() {
DefaultClientConfigImpl config = new DefaultClientConfigImpl();
config.set(CommonClientConfigKey.FollowRedirects, true);
return config;
}
}
@Configuration
protected static class DoNotFollowRedirects {
@Bean
public IClientConfig clientConfig() {
DefaultClientConfigImpl config = new DefaultClientConfigImpl();
config.set(CommonClientConfigKey.FollowRedirects, false);
return config;
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* 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.okhttp;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import org.junit.Test;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import org.springframework.util.LinkedMultiValueMap;
import okhttp3.Request;
import okhttp3.RequestBody;
import okio.Buffer;
/**
* @author Spencer Gibb
*/
public class OkHttpRibbonRequestTests {
@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");
RibbonCommandContext context = new RibbonCommandContext("example", "GET", uri, false, headers, params, null);
OkHttpRibbonRequest httpRequest = new OkHttpRibbonRequest(context);
Request request = httpRequest.toRequest();
assertThat("body is not null", request.body(), is(nullValue()));
assertThat("uri is wrong", request.url().toString(), startsWith(uri));
assertThat("my-header is wrong", request.header("my-header"), is(equalTo("my-value")));
assertThat("myparam is missing", request.url().queryParameter("myparam"), 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 IOException {
String lengthString = String.valueOf(entityValue.length());
Long length = null;
String uri = "http://example.com";
LinkedMultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
if (addContentLengthHeader) {
headers.add("Content-Length", lengthString);
length = (long) entityValue.length();
}
RibbonCommandContext context = new RibbonCommandContext("example", method, uri, false,
headers, new LinkedMultiValueMap<String, String>(), requestEntity);
context.setContentLength(length);
OkHttpRibbonRequest httpRequest = new OkHttpRibbonRequest(context);
Request request = httpRequest.toRequest();
assertThat("uri is wrong", request.url().toString(), startsWith(uri));
if (addContentLengthHeader) {
assertThat("Content-Length is wrong", request.header("Content-Length"),
is(equalTo(lengthString)));
}
if (!method.equalsIgnoreCase("get")) {
assertThat("body is null", request.body(), is(notNullValue()));
RequestBody body = request.body();
assertThat("contentLength is wrong", body.contentLength(), is(equalTo((long) entityValue.length())));
Buffer content = new Buffer();
body.writeTo(content);
String string = content.readByteString().utf8();
assertThat("content is wrong", string, is(equalTo(entityValue)));
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2013-2015 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.okhttp;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import java.net.URI;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* @author Spencer Gibb
*/
public class OkHttpRibbonResponseTests {
@Test
public void testNullEntity() throws Exception {
URI uri = URI.create("http://example.com");
Response response = response(uri).build();
OkHttpRibbonResponse httpResponse = new OkHttpRibbonResponse(response, uri);
assertThat(httpResponse.isSuccess(), is(true));
assertThat(httpResponse.hasPayload(), is(false));
assertThat(httpResponse.getPayload(), is(nullValue()));
assertThat(httpResponse.getInputStream(), is(nullValue()));
}
@Test
public void testNotNullEntity() throws Exception {
URI uri = URI.create("http://example.com");
Response response = response(uri)
.body(ResponseBody.create(MediaType.parse("text/plain"), "abcd"))
.build();
OkHttpRibbonResponse httpResponse = new OkHttpRibbonResponse(response, uri);
assertThat(httpResponse.isSuccess(), is(true));
assertThat(httpResponse.hasPayload(), is(true));
assertThat(httpResponse.getPayload(), is(notNullValue()));
assertThat(httpResponse.getInputStream(), is(notNullValue()));
}
Response.Builder response(URI uri) {
return new Response.Builder()
.request(new Request.Builder().url(HttpUrl.get(uri)).build())
.protocol(Protocol.HTTP_1_1)
.code(HttpStatus.OK.value());
}
}

View File

@@ -16,12 +16,13 @@
package org.springframework.cloud.netflix.zuul;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
@@ -43,8 +44,6 @@ import org.springframework.web.bind.annotation.RestController;
import com.netflix.zuul.context.RequestContext;
import static org.junit.Assert.assertEquals;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = ContextPathZuulProxyApplication.class)
@WebAppConfiguration
@@ -105,8 +104,4 @@ class ContextPathZuulProxyApplication {
return "Gotten " + id + "!";
}
public static void main(String[] args) {
SpringApplication.run(SampleZuulProxyApplication.class, args);
}
}

View File

@@ -115,10 +115,6 @@ class RetryableZuulProxyApplication {
};
}
public static void main(String[] args) {
SpringApplication.run(SampleZuulProxyApplication.class, args);
}
}
// Load balancer with fixed server list for "simple" pointing to localhost
@@ -133,4 +129,4 @@ class RetryableRibbonClientConfiguration {
return new StaticServerList<>(new Server("localhost", this.port),
new Server("failed-localhost", this.port));
}
}
}

View File

@@ -16,12 +16,13 @@
package org.springframework.cloud.netflix.zuul;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
@@ -43,10 +44,8 @@ import org.springframework.web.bind.annotation.RestController;
import com.netflix.zuul.context.RequestContext;
import static org.junit.Assert.assertEquals;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = ServletPathZuulProxyApplication.class)
@SpringApplicationConfiguration(classes = ServletPathZuulProxyApplicationTests.ServletPathZuulProxyApplication.class)
@WebAppConfiguration
@IntegrationTest({ "server.port: 0", "server.servletPath: /app" })
@DirtiesContext
@@ -91,22 +90,18 @@ public class ServletPathZuulProxyApplicationTests {
assertEquals("Gotten strip!", result.getBody());
}
}
// Don't use @SpringBootApplication because we don't want to component scan
@Configuration
@EnableAutoConfiguration
@RestController
@EnableZuulProxy
class ServletPathZuulProxyApplication {
// Don't use @SpringBootApplication because we don't want to component scan
@Configuration
@EnableAutoConfiguration
@RestController
@EnableZuulProxy
static class ServletPathZuulProxyApplication {
@RequestMapping(value = "/local/{id}", method = RequestMethod.GET)
public String get(@PathVariable String id) {
return "Gotten " + id + "!";
}
@RequestMapping(value = "/local/{id}", method = RequestMethod.GET)
public String get(@PathVariable String id) {
return "Gotten " + id + "!";
}
public static void main(String[] args) {
SpringApplication.run(SampleZuulProxyApplication.class, args);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2015 the original author or authors.
* 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.
@@ -12,14 +12,14 @@
* 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;
package org.springframework.cloud.netflix.zuul.filters.route.apache;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.test.SpringApplicationConfiguration;
@@ -28,8 +28,9 @@ import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.apache.HttpClientRibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.support.ZuulProxyTestBase;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpEntity;
@@ -47,13 +48,21 @@ import org.springframework.web.bind.annotation.RestController;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* @author Spencer Gibb
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SampleHttpClientZuulProxyApplication.class)
@SpringApplicationConfiguration(classes = HttpClientRibbonCommandIntegrationTests.TestConfig.class)
@WebIntegrationTest(randomPort = true, value = {
"zuul.routes.other: /test/**=http://localhost:7777/local",
"zuul.routes.another: /another/twolevel/**", "zuul.routes.simple: /simple/**" })
@DirtiesContext
public class SampleZuulProxyWithHttpClientTests extends ZuulProxyTestBase {
public class HttpClientRibbonCommandIntegrationTests extends ZuulProxyTestBase {
@Override
protected boolean supportsPatch() {
return true;
}
@Before
public void init() {
@@ -87,47 +96,35 @@ public class SampleZuulProxyWithHttpClientTests extends ZuulProxyTestBase {
assertEquals("Deleted 1!", result.getBody());
}
@Test
public void patchOnSelfViaSimpleHostRoutingFilter() {
this.routes.addRoute("/self/**", "http://localhost:" + this.port + "/local");
this.endpoint.reset();
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/self/1", HttpMethod.PATCH,
new HttpEntity<>("TestPatch"), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Patched 1!", result.getBody());
}
@Test
public void ribbonCommandFactoryOverridden() {
assertTrue("ribbonCommandFactory not a HttpClientRibbonCommandFactory",
this.ribbonCommandFactory instanceof HttpClientRibbonCommandFactory);
}
// Don't use @SpringBootApplication because we don't want to component scan
@Configuration
@EnableAutoConfiguration
@RestController
@EnableZuulProxy
@RibbonClients({
@RibbonClient(name = "simple", configuration = ZuulProxyTestBase.SimpleRibbonClientConfiguration.class),
@RibbonClient(name = "another", configuration = ZuulProxyTestBase.AnotherRibbonClientConfiguration.class) })
static class TestConfig extends ZuulProxyTestBase.AbstractZuulProxyApplication {
}
@RequestMapping(value = "/local/{id}", method = RequestMethod.PATCH)
public String patch(@PathVariable final String id, @RequestBody final String body) {
return "Patched " + id + "!";
}
// Don't use @SpringBootApplication because we don't want to component scan
@Configuration
@EnableAutoConfiguration
@RestController
@EnableZuulProxy
@RibbonClients({
@RibbonClient(name = "simple", configuration = SimpleRibbonClientConfiguration.class),
@RibbonClient(name = "another", configuration = AnotherRibbonClientConfiguration.class) })
class SampleHttpClientZuulProxyApplication extends ZuulProxyTestBase.AbstractZuulProxyApplication {
@Bean
public RibbonCommandFactory<?> ribbonCommandFactory(
final SpringClientFactory clientFactory) {
return new HttpClientRibbonCommandFactory(clientFactory);
}
public static void main(final String[] args) {
SpringApplication.run(SampleZuulProxyApplication.class, args);
}
@RequestMapping(value = "/local/{id}", method = RequestMethod.PATCH)
public String patch(@PathVariable final String id, @RequestBody final String body) {
return "Patched " + id + "!";
}
@Bean
public MyErrorController myErrorController(ErrorAttributes errorAttributes) {
return new MyErrorController(errorAttributes);
@Bean
public ZuulProxyTestBase.MyErrorController myErrorController(ErrorAttributes errorAttributes) {
return new ZuulProxyTestBase.MyErrorController(errorAttributes);
}
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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.okhttp;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.support.ZuulProxyTestBase;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.bind.annotation.RestController;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = OkHttpRibbonCommandIntegrationTests.TestConfig.class)
@WebIntegrationTest(randomPort = true, value = {
"zuul.routes.other: /test/**=http://localhost:7777/local",
"zuul.routes.another: /another/twolevel/**", "zuul.routes.simple: /simple/**" })
@DirtiesContext
public class OkHttpRibbonCommandIntegrationTests extends ZuulProxyTestBase {
@Override
protected boolean supportsPatch() {
return true;
}
@Before
public void init() {
super.setTestRequestcontext();
}
@Test
public void patchOnSelfViaRibbonRoutingFilter() {
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/simple/local/1", HttpMethod.PATCH,
new HttpEntity<>("TestPatch"), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Patched 1!", result.getBody());
}
@Test
public void postOnSelfViaRibbonRoutingFilter() {
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/simple/local/1", HttpMethod.POST,
new HttpEntity<>("TestPost"), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Posted 1!", result.getBody());
}
@Test
public void deleteOnSelfViaRibbonRoutingFilter() {
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/simple/local/1", HttpMethod.DELETE,
new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Deleted 1!", result.getBody());
}
@Test
public void ribbonCommandFactoryOverridden() {
assertTrue("ribbonCommandFactory not a OkHttpRibbonCommandFactory",
this.ribbonCommandFactory instanceof OkHttpRibbonCommandFactory);
}
// Don't use @SpringBootApplication because we don't want to component scan
@Configuration
@EnableAutoConfiguration
@RestController
@EnableZuulProxy
@RibbonClients({
@RibbonClient(name = "simple", configuration = SimpleRibbonClientConfiguration.class),
@RibbonClient(name = "another", configuration = AnotherRibbonClientConfiguration.class) })
static class TestConfig extends ZuulProxyTestBase.AbstractZuulProxyApplication {
@Bean
public RibbonCommandFactory<?> ribbonCommandFactory(
final SpringClientFactory clientFactory) {
return new OkHttpRibbonCommandFactory(clientFactory);
}
@Bean
public MyErrorController myErrorController(ErrorAttributes errorAttributes) {
return new MyErrorController(errorAttributes);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2015 the original author or authors.
* 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.
@@ -12,12 +12,18 @@
* 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;
package org.springframework.cloud.netflix.zuul.filters.route.restclient;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
@@ -36,13 +42,13 @@ import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.netflix.ribbon.StaticServerList;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.support.ZuulProxyTestBase;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.discovery.DiscoveryClientRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.route.RestClientRibbonCommand;
import org.springframework.cloud.netflix.zuul.filters.route.RestClientRibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpEntity;
@@ -55,7 +61,6 @@ import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.MatrixVariable;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -63,22 +68,14 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.netflix.client.ClientException;
import com.netflix.client.http.HttpRequest;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerList;
import com.netflix.niws.client.http.RestClient;
import lombok.SneakyThrows;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SampleZuulProxyApplication.class)
@SpringApplicationConfiguration(classes = RestClientRibbonCommandIntegrationTests.TestConfig.class)
@WebAppConfiguration
@IntegrationTest({ "server.port: 0",
"zuul.routes.other: /test/**=http://localhost:7777/local",
@@ -87,11 +84,16 @@ import static org.junit.Assert.assertTrue;
"zuul.routes.rnd: /rnd/**", "rnd.ribbon.listOfServers: ${random.value}",
"zuul.removeSemicolonContent: false" })
@DirtiesContext
public class SampleZuulProxyApplicationTests extends ZuulProxyTestBase {
public class RestClientRibbonCommandIntegrationTests extends ZuulProxyTestBase {
@Autowired
RouteLocator routeLocator;
@Override
protected boolean supportsPatch() {
return false;
}
@Test
public void simpleHostRouteWithTrailingSlash() {
this.routes.addRoute("/self/**", "http://localhost:" + this.port + "/");
@@ -223,144 +225,141 @@ public class SampleZuulProxyApplicationTests extends ZuulProxyTestBase {
@Test
public void ribbonCommandFactoryOverridden() {
assertTrue("ribbonCommandFactory not a MyRibbonCommandFactory",
this.ribbonCommandFactory instanceof SampleZuulProxyApplication.MyRibbonCommandFactory);
this.ribbonCommandFactory instanceof TestConfig.MyRibbonCommandFactory);
}
@Test
public void routeLocatorOverridden() {
assertTrue("routeLocator not a MyRouteLocator",
this.routeLocator instanceof SampleZuulProxyApplication.MyRouteLocator);
this.routeLocator instanceof TestConfig.MyRouteLocator);
}
}
// Don't use @SpringBootApplication because we don't want to component scan
@Configuration
@EnableAutoConfiguration
@RestController
@EnableZuulProxy
@RibbonClients({
@RibbonClient(name = "badhost", configuration = SampleZuulProxyApplication.BadHostRibbonClientConfiguration.class),
@RibbonClient(name = "simple", configuration = SimpleRibbonClientConfiguration.class),
@RibbonClient(name = "another", configuration = AnotherRibbonClientConfiguration.class) })
class SampleZuulProxyApplication extends ZuulProxyTestBase.AbstractZuulProxyApplication {
@RequestMapping("/trailing-slash")
public String trailingSlash(HttpServletRequest request) {
return request.getRequestURI();
}
@RequestMapping("/content-type")
public String contentType(HttpServletRequest request) {
String header = request.getHeader("Content-Type");
return header == null ? "<NONE>" : header;
}
@RequestMapping("/add-header")
public ResponseEntity<String> addHeader(HttpServletRequest request) {
HttpHeaders headers = new HttpHeaders();
headers.set("X-Header", "FOO");
ResponseEntity<String> result = new ResponseEntity<String>(
request.getRequestURI(), headers, HttpStatus.OK);
return result;
}
@RequestMapping("/query")
public String addQuery(HttpServletRequest request, @RequestParam String foo) {
return request.getRequestURI() + "?foo=" + foo;
}
@RequestMapping("/matrix/{name}/{another}")
public String matrix(@PathVariable("name") String name,
@MatrixVariable(value = "p", pathVar = "name") int p,
@MatrixVariable(value = "q", pathVar = "name") int q,
@PathVariable("another") String another,
@MatrixVariable(value = "x", pathVar = "another") int x) {
return name + "=" + p + "-" + q + ";" + another + "=" + x;
}
@Bean
public RibbonCommandFactory<?> ribbonCommandFactory(
SpringClientFactory clientFactory) {
return new MyRibbonCommandFactory(clientFactory);
}
@Bean
public RouteLocator routeLocator(DiscoveryClient discoveryClient, ZuulProperties zuulProperties) {
return new MyRouteLocator("/", discoveryClient, zuulProperties);
}
@Bean
public MyErrorController myErrorController(ErrorAttributes errorAttributes) {
return new MyErrorController(errorAttributes);
}
public static void main(String[] args) {
SpringApplication.run(SampleZuulProxyApplication.class, args);
}
public static class MyRibbonCommandFactory extends RestClientRibbonCommandFactory {
public MyRibbonCommandFactory(SpringClientFactory clientFactory) {
super(clientFactory);
}
@Override
@SuppressWarnings("deprecation")
@SneakyThrows
public RestClientRibbonCommand create(RibbonCommandContext context) {
String uri = context.getUri();
if (uri.startsWith("/throwexception/")) {
String code = uri.replace("/throwexception/", "");
RestClient restClient = getClientFactory()
.getClient(context.getServiceId(), RestClient.class);
return new MyCommand(Integer.parseInt(code), context.getServiceId(),
restClient, getVerb(context.getVerb()), context.getUri(),
context.getRetryable(), context.getHeaders(), context.getParams(),
context.getRequestEntity());
}
return super.create(context);
}
}
static class MyCommand extends RestClientRibbonCommand {
private int errorCode;
public MyCommand(int errorCode, String commandKey, RestClient restClient,
HttpRequest.Verb verb, String uri, Boolean retryable,
MultiValueMap<String, String> headers,
MultiValueMap<String, String> params, InputStream requestEntity)
throws URISyntaxException {
super(commandKey, restClient, verb, uri, retryable, headers, params,
requestEntity);
this.errorCode = errorCode;
}
@Override
protected ClientHttpResponse forward() throws Exception {
if (this.errorCode == 503) {
throw new ClientException(ClientException.ErrorType.SERVER_THROTTLED);
}
return new MockClientHttpResponse((byte[]) null,
HttpStatus.valueOf(this.errorCode));
}
}
// Load balancer with fixed server list for "simple" pointing to bad host
// Don't use @SpringBootApplication because we don't want to component scan
@Configuration
static class BadHostRibbonClientConfiguration {
@Bean
public ServerList<Server> ribbonServerList() {
return new StaticServerList<>(new Server(UUID.randomUUID().toString(), 4322));
@EnableAutoConfiguration
@RestController
@EnableZuulProxy
@RibbonClients({
@RibbonClient(name = "badhost", configuration = TestConfig.BadHostRibbonClientConfiguration.class),
@RibbonClient(name = "simple", configuration = ZuulProxyTestBase.SimpleRibbonClientConfiguration.class),
@RibbonClient(name = "another", configuration = ZuulProxyTestBase.AnotherRibbonClientConfiguration.class) })
static class TestConfig extends ZuulProxyTestBase.AbstractZuulProxyApplication {
@RequestMapping("/trailing-slash")
public String trailingSlash(HttpServletRequest request) {
return request.getRequestURI();
}
@RequestMapping("/content-type")
public String contentType(HttpServletRequest request) {
String header = request.getHeader("Content-Type");
return header == null ? "<NONE>" : header;
}
}
@RequestMapping("/add-header")
public ResponseEntity<String> addHeader(HttpServletRequest request) {
HttpHeaders headers = new HttpHeaders();
headers.set("X-Header", "FOO");
ResponseEntity<String> result = new ResponseEntity<String>(
request.getRequestURI(), headers, HttpStatus.OK);
return result;
}
static class MyRouteLocator extends DiscoveryClientRouteLocator {
@RequestMapping("/query")
public String addQuery(HttpServletRequest request, @RequestParam String foo) {
return request.getRequestURI() + "?foo=" + foo;
}
public MyRouteLocator(String servletPath, DiscoveryClient discovery, ZuulProperties properties) {
super(servletPath, discovery, properties);
@RequestMapping("/matrix/{name}/{another}")
public String matrix(@PathVariable("name") String name,
@MatrixVariable(value = "p", pathVar = "name") int p,
@MatrixVariable(value = "q", pathVar = "name") int q,
@PathVariable("another") String another,
@MatrixVariable(value = "x", pathVar = "another") int x) {
return name + "=" + p + "-" + q + ";" + another + "=" + x;
}
@Bean
public RibbonCommandFactory<?> ribbonCommandFactory(
SpringClientFactory clientFactory) {
return new MyRibbonCommandFactory(clientFactory);
}
@Bean
public RouteLocator routeLocator(DiscoveryClient discoveryClient, ZuulProperties zuulProperties) {
return new MyRouteLocator("/", discoveryClient, zuulProperties);
}
@Bean
public MyErrorController myErrorController(ErrorAttributes errorAttributes) {
return new MyErrorController(errorAttributes);
}
public static void main(String[] args) {
SpringApplication.run(TestConfig.class, args);
}
public static class MyRibbonCommandFactory extends RestClientRibbonCommandFactory {
private SpringClientFactory clientFactory;
public MyRibbonCommandFactory(SpringClientFactory clientFactory) {
super(clientFactory);
this.clientFactory = clientFactory;
}
@Override
@SuppressWarnings("deprecation")
@SneakyThrows
public RestClientRibbonCommandFactory.RestClientRibbonCommand create(RibbonCommandContext context) {
String uri = context.getUri();
if (uri.startsWith("/throwexception/")) {
String code = uri.replace("/throwexception/", "");
RestClient restClient = clientFactory
.getClient(context.getServiceId(), RestClient.class);
return new MyCommand(Integer.parseInt(code), context.getServiceId(),
restClient, context);
}
return super.create(context);
}
}
static class MyCommand extends RestClientRibbonCommandFactory.RestClientRibbonCommand {
private int errorCode;
public MyCommand(int errorCode, String commandKey, RestClient restClient,
RibbonCommandContext context) {
super(commandKey, restClient, context);
this.errorCode = errorCode;
}
@Override
protected ClientHttpResponse run() throws Exception {
if (this.errorCode == 503) {
throw new ClientException(ClientException.ErrorType.SERVER_THROTTLED);
}
return new MockClientHttpResponse((byte[]) null,
HttpStatus.valueOf(this.errorCode));
}
}
// Load balancer with fixed server list for "simple" pointing to bad host
@Configuration
static class BadHostRibbonClientConfiguration {
@Bean
public ServerList<Server> ribbonServerList() {
return new StaticServerList<>(new Server(UUID.randomUUID().toString(), 4322));
}
}
static class MyRouteLocator extends DiscoveryClientRouteLocator {
public MyRouteLocator(String servletPath, DiscoveryClient discovery, ZuulProperties properties) {
super(servletPath, discovery, properties);
}
}
}
}

View File

@@ -1,4 +1,21 @@
package org.springframework.cloud.netflix.zuul;
/*
* 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.support;
import java.util.Arrays;
import java.util.HashMap;
@@ -8,6 +25,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.http.HttpServletRequest;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -17,6 +35,7 @@ import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.cloud.netflix.ribbon.StaticServerList;
import org.springframework.cloud.netflix.zuul.RoutesEndpoint;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.discovery.DiscoveryClientRouteLocator;
@@ -41,8 +60,10 @@ import com.netflix.loadbalancer.ServerList;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assume.assumeThat;
/**
* @author Spencer Gibb
@@ -213,9 +234,31 @@ public abstract class ZuulProxyTestBase {
assertEquals("Received {key=[overridden]}", result.getBody());
}
protected static abstract class AbstractZuulProxyApplication
@Test
public void patchOnSelfViaSimpleHostRoutingFilter() {
assumeThat(supportsPatch(), is(true));
this.routes.addRoute("/self/**", "http://localhost:" + this.port + "/local");
this.endpoint.reset();
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/self/1", HttpMethod.PATCH,
new HttpEntity<>("TestPatch"), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Patched 1!", result.getBody());
}
protected abstract boolean supportsPatch();
public static abstract class AbstractZuulProxyApplication
extends DelegatingWebMvcConfiguration {
@RequestMapping(value = "/local/{id}", method = RequestMethod.PATCH)
public String patch(@PathVariable final String id,
@RequestBody final String body) {
return "Patched " + id + "!";
}
@RequestMapping("/testing123")
public String testing123() {
throw new RuntimeException("myerror");
@@ -298,64 +341,64 @@ public abstract class ZuulProxyTestBase {
return mapping;
}
}
}
// Load balancer with fixed server list for "simple" pointing to localhost
@Configuration
class SimpleRibbonClientConfiguration {
// Load balancer with fixed server list for "simple" pointing to localhost
@Configuration
public static class SimpleRibbonClientConfiguration {
@Value("${local.server.port}")
private int port;
@Value("${local.server.port}")
private int port;
@Bean
public ServerList<Server> ribbonServerList() {
return new StaticServerList<>(new Server("localhost", this.port));
}
@Bean
public ServerList<Server> ribbonServerList() {
return new StaticServerList<>(new Server("localhost", this.port));
}
}
@Configuration
public static class AnotherRibbonClientConfiguration {
@Configuration
class AnotherRibbonClientConfiguration {
@Value("${local.server.port}")
private int port;
@Value("${local.server.port}")
private int port;
@Bean
public ServerList<Server> ribbonServerList() {
return new StaticServerList<>(new Server("localhost", this.port));
}
@Bean
public ServerList<Server> ribbonServerList() {
return new StaticServerList<>(new Server("localhost", this.port));
}
}
public static class MyErrorController extends BasicErrorController {
ThreadLocal<String> uriToMatch = new ThreadLocal<>();
class MyErrorController extends BasicErrorController {
ThreadLocal<String> uriToMatch = new ThreadLocal<>();
AtomicBoolean controllerUsed = new AtomicBoolean();
AtomicBoolean controllerUsed = new AtomicBoolean();
public MyErrorController(ErrorAttributes errorAttributes) {
super(errorAttributes, new ErrorProperties());
}
public MyErrorController(ErrorAttributes errorAttributes) {
super(errorAttributes, new ErrorProperties());
}
@Override
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
String errorUri = (String) request.getAttribute("javax.servlet.error.request_uri");
@Override
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
String errorUri = (String) request.getAttribute("javax.servlet.error.request_uri");
if (errorUri != null && errorUri.equals(this.uriToMatch.get())) {
controllerUsed.set(true);
if (errorUri != null && errorUri.equals(this.uriToMatch.get())) {
controllerUsed.set(true);
}
this.uriToMatch.remove();
return super.error(request);
}
this.uriToMatch.remove();
return super.error(request);
}
public void setUriToMatch(String uri) {
this.uriToMatch.set(uri);
}
public void setUriToMatch(String uri) {
this.uriToMatch.set(uri);
}
public boolean wasControllerUsed() {
return this.controllerUsed.get();
}
public boolean wasControllerUsed() {
return this.controllerUsed.get();
}
public void clear() {
this.controllerUsed.set(false);
public void clear() {
this.controllerUsed.set(false);
}
}
}