From 7ac81f8fd5a2428d995a430903952125d9edc1db Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Wed, 22 Jun 2016 15:47:13 -0600 Subject: [PATCH] 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 --- .../apache/RibbonApacheHttpRequest.java | 61 ++-- .../apache/RibbonLoadBalancingHttpClient.java | 62 +--- .../okhttp/OkHttpLoadBalancingClient.java | 90 ++++++ .../ribbon/okhttp/OkHttpRibbonRequest.java | 128 ++++++++ .../ribbon/okhttp/OkHttpRibbonResponse.java | 151 +++++++++ .../support/AbstractLoadBalancingClient.java | 97 ++++++ .../ribbon/support/ContextAwareRequest.java | 48 +++ .../netflix/zuul/ZuulProxyConfiguration.java | 14 +- .../zuul/filters/ProxyRequestHelper.java | 31 +- .../route/RestClientRibbonCommand.java | 183 ----------- .../route/RestClientRibbonCommandFactory.java | 63 ---- .../filters/route/RibbonCommandContext.java | 24 +- .../filters/route/RibbonRoutingFilter.java | 2 +- .../route/apache/HttpClientRibbonCommand.java | 129 -------- .../HttpClientRibbonCommandFactory.java | 29 +- .../okhttp/OkHttpRibbonCommandFactory.java | 61 ++++ .../RestClientRibbonCommandFactory.java | 102 +++++++ .../route/support/AbstractRibbonCommand.java | 100 ++++++ .../cloud/netflix/AdhocTestSuite.java | 3 +- .../apache/RibbonApacheHttpRequestTests.java | 15 +- .../OkHttpLoadBalancingClientTests.java | 123 ++++++++ .../okhttp/OkHttpRibbonRequestTests.java | 106 +++++++ .../okhttp/OkHttpRibbonResponseTests.java | 75 +++++ .../ContextPathZuulProxyApplicationTests.java | 9 +- .../RetryableZuulProxyApplicationTests.java | 6 +- .../ServletPathZuulProxyApplicationTests.java | 33 +- ...pClientRibbonCommandIntegrationTests.java} | 77 +++-- .../OkHttpRibbonCommandIntegrationTests.java | 120 ++++++++ ...tClientRibbonCommandIntegrationTests.java} | 287 +++++++++--------- .../route/support}/ZuulProxyTestBase.java | 129 +++++--- 30 files changed, 1582 insertions(+), 776 deletions(-) create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpLoadBalancingClient.java create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequest.java create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonResponse.java create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/AbstractLoadBalancingClient.java create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequest.java delete mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommand.java delete mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommandFactory.java delete mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommand.java create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/okhttp/OkHttpRibbonCommandFactory.java create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/restclient/RestClientRibbonCommandFactory.java create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/support/AbstractRibbonCommand.java create mode 100644 spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpLoadBalancingClientTests.java create mode 100644 spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequestTests.java create mode 100644 spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonResponseTests.java rename spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/{SampleZuulProxyWithHttpClientTests.java => filters/route/apache/HttpClientRibbonCommandIntegrationTests.java} (67%) create mode 100644 spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/okhttp/OkHttpRibbonCommandIntegrationTests.java rename spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/{SampleZuulProxyApplicationTests.java => filters/route/restclient/RestClientRibbonCommandIntegrationTests.java} (65%) rename spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/{ => filters/route/support}/ZuulProxyTestBase.java (74%) diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequest.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequest.java index 598fb9b8..f4157883 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequest.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequest.java @@ -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 headers; - - private final MultiValueMap params; - - private final InputStream requestEntity; - - public RibbonApacheHttpRequest(final String method, final URI uri, - final Boolean retryable, final MultiValueMap headers, - final MultiValueMap 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 headers, - final MultiValueMap 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 values = this.headers.get(name); + for (final String name : this.context.getHeaders().keySet()) { + final List values = this.context.getHeaders().get(name); for (final String value : values) { builder.addHeader(name, value); } } - for (final String name : this.params.keySet()) { - final List values = this.params.get(name); + for (final String name : this.context.getParams().keySet()) { + final List 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)); } } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonLoadBalancingHttpClient.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonLoadBalancingHttpClient.java index 8563e83a..09539007 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonLoadBalancingHttpClient.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/apache/RibbonLoadBalancingHttpClient.java @@ -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 { + AbstractLoadBalancingClient { 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; - } } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpLoadBalancingClient.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpLoadBalancingClient.java new file mode 100644 index 00000000..d96b9b7d --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpLoadBalancingClient.java @@ -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 { + 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(); + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequest.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequest.java new file mode 100644 index 00000000..fb83140b --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequest.java @@ -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 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 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(); + } + } + } + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonResponse.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonResponse.java new file mode 100644 index 00000000..f5826539 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonResponse.java @@ -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> getHeaders() { + final Map> headers = new HashMap<>(); + for (Map.Entry> 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 values = new ArrayList<>(); + values.add(value); + headers.put(name, values); + } + } + } + + return headers; + } + + @Override + public HttpHeaders getHttpHeaders() { + final CaseInsensitiveMultiMap headers = new CaseInsensitiveMultiMap(); + for (Map.Entry> 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 getEntity(Class type) throws Exception { + return null; + } + + @Override + public T getEntity(Type type) throws Exception { + return null; + } + + @Override + public T getEntity(TypeToken type) throws Exception { + return null; + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/AbstractLoadBalancingClient.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/AbstractLoadBalancingClient.java new file mode 100644 index 00000000..8805f216 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/AbstractLoadBalancingClient.java @@ -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 extends + AbstractLoadBalancerAwareClient { + + 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; + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequest.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequest.java new file mode 100644 index 00000000..af62d9bf --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/support/ContextAwareRequest.java @@ -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; + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java index 2c4a1ba8..1d472695 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java @@ -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, diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelper.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelper.java index b10fe14c..7e5e10d9 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelper.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelper.java @@ -249,14 +249,7 @@ public class ProxyRequestHelper { Map input = new LinkedHashMap<>(); trace.put("request", input); info.put("headers", trace); - for (Entry> entry : headers.entrySet()) { - Collection 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 trace = (Map) info.get("headers"); - Map output = new LinkedHashMap(); + Map output = new LinkedHashMap<>(); trace.put("response", output); - for (Entry> key : headers.entrySet()) { - Collection 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 headers, Map output) { + for (Entry> key : headers.entrySet()) { + Collection 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 info, InputStream inputStream) throws IOException { if (RequestContext.getCurrentContext().isChunkedRequestBody()) { diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommand.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommand.java deleted file mode 100644 index 8cb95861..00000000 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommand.java +++ /dev/null @@ -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 implements RibbonCommand { - - private RestClient restClient; - - private Verb verb; - - private URI uri; - - private Boolean retryable; - - private MultiValueMap headers; - - private MultiValueMap params; - - private InputStream requestEntity; - - public RestClientRibbonCommand(RestClient restClient, Verb verb, String uri, - Boolean retryable, - MultiValueMap headers, - MultiValueMap 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 headers, - MultiValueMap 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 values = this.headers.get(name); - for (String value : values) { - builder.header(name, value); - } - } - for (String name : this.params.keySet()) { - List 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 getHeaders() { - return this.headers; - } - - protected MultiValueMap 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; - } -} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommandFactory.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommandFactory.java deleted file mode 100644 index 30cd9dbf..00000000 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommandFactory.java +++ /dev/null @@ -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 { - - 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; - } - } -} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandContext.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandContext.java index 507665cf..9d704d8a 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandContext.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandContext.java @@ -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 headers; private final MultiValueMap 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; + } } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java index 02305106..95f7ec5c 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java @@ -119,7 +119,7 @@ public class RibbonRoutingFilter extends ZuulFilter { } protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception { - Map info = this.helper.debug(context.getVerb(), context.getUri(), + Map info = this.helper.debug(context.getMethod(), context.getUri(), context.getHeaders(), context.getParams(), context.getRequestEntity()); RibbonCommand command = this.ribbonCommandFactory.create(context); diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommand.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommand.java deleted file mode 100644 index 1182cb33..00000000 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommand.java +++ /dev/null @@ -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 implements - RibbonCommand { - - private final RibbonLoadBalancingHttpClient client; - private final String method; - private final String uri; - private final MultiValueMap headers; - private final MultiValueMap params; - private final InputStream requestEntity; - private final Boolean retryable; - - public HttpClientRibbonCommand(final RibbonLoadBalancingHttpClient client, - final String method, final String uri, - final MultiValueMap headers, - final MultiValueMap 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 headers, - final MultiValueMap 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); - } - -} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommandFactory.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommandFactory.java index dcef3e82..4fa6e409 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommandFactory.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommandFactory.java @@ -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 { + RibbonCommandFactory { 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 { + + 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); + } + } } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/okhttp/OkHttpRibbonCommandFactory.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/okhttp/OkHttpRibbonCommandFactory.java new file mode 100644 index 00000000..ea898972 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/okhttp/OkHttpRibbonCommandFactory.java @@ -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 { + + 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 { + + 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); + } + + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/restclient/RestClientRibbonCommandFactory.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/restclient/RestClientRibbonCommandFactory.java new file mode 100644 index 00000000..a727b736 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/restclient/RestClientRibbonCommandFactory.java @@ -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 { + + 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 { + + 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 values = this.context.getHeaders().get(name); + for (String value : values) { + builder.header(name, value); + } + } + for (String name : this.context.getParams().keySet()) { + List 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; + } + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/support/AbstractRibbonCommand.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/support/AbstractRibbonCommand.java new file mode 100644 index 00000000..29935600 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/support/AbstractRibbonCommand.java @@ -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, RQ extends ClientRequest, RS extends HttpResponse> extends HystrixCommand 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; +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/AdhocTestSuite.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/AdhocTestSuite.java index 3f029e1e..2200743e 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/AdhocTestSuite.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/AdhocTestSuite.java @@ -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 { diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequestTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequestTests.java index 564d8b4c..4b1e74d7 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequestTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/apache/RibbonApacheHttpRequestTests.java @@ -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 headers = new LinkedMultiValueMap<>(); headers.add("my-header", "my-value"); LinkedMultiValueMap 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(), requestEntity, - length); + RibbonCommandContext context = new RibbonCommandContext("example", method, uri.toString(), false, headers, new LinkedMultiValueMap(), requestEntity); + context.setContentLength(length); + RibbonApacheHttpRequest httpRequest = new RibbonApacheHttpRequest(context); HttpUriRequest request = httpRequest.toRequest(RequestConfig.custom().build()); diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpLoadBalancingClientTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpLoadBalancingClientTests.java new file mode 100644 index 00000000..d19584b1 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpLoadBalancingClientTests.java @@ -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; + } + } + +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequestTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequestTests.java new file mode 100644 index 00000000..94144d08 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonRequestTests.java @@ -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 headers = new LinkedMultiValueMap<>(); + headers.add("my-header", "my-value"); + LinkedMultiValueMap 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 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(), 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))); + } + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonResponseTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonResponseTests.java new file mode 100644 index 00000000..3855a432 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/okhttp/OkHttpRibbonResponseTests.java @@ -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()); + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ContextPathZuulProxyApplicationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ContextPathZuulProxyApplicationTests.java index 0df6b818..9a9868b1 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ContextPathZuulProxyApplicationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ContextPathZuulProxyApplicationTests.java @@ -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); - } - } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RetryableZuulProxyApplicationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RetryableZuulProxyApplicationTests.java index c6c7e375..66bccf29 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RetryableZuulProxyApplicationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RetryableZuulProxyApplicationTests.java @@ -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)); } -} \ No newline at end of file +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ServletPathZuulProxyApplicationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ServletPathZuulProxyApplicationTests.java index 021e1885..8da0e2de 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ServletPathZuulProxyApplicationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ServletPathZuulProxyApplicationTests.java @@ -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); - } - } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyWithHttpClientTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommandIntegrationTests.java similarity index 67% rename from spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyWithHttpClientTests.java rename to spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommandIntegrationTests.java index bc76eb55..0947b0e8 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyWithHttpClientTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/apache/HttpClientRibbonCommandIntegrationTests.java @@ -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 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); + } } } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/okhttp/OkHttpRibbonCommandIntegrationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/okhttp/OkHttpRibbonCommandIntegrationTests.java new file mode 100644 index 00000000..725f7327 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/okhttp/OkHttpRibbonCommandIntegrationTests.java @@ -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 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 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 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); + } + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyApplicationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/restclient/RestClientRibbonCommandIntegrationTests.java similarity index 65% rename from spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyApplicationTests.java rename to spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/restclient/RestClientRibbonCommandIntegrationTests.java index af8f2a5a..a135c103 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyApplicationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/restclient/RestClientRibbonCommandIntegrationTests.java @@ -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 ? "" : header; - } - - @RequestMapping("/add-header") - public ResponseEntity addHeader(HttpServletRequest request) { - HttpHeaders headers = new HttpHeaders(); - headers.set("X-Header", "FOO"); - ResponseEntity result = new ResponseEntity( - 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 headers, - MultiValueMap 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 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 ? "" : header; } - } + @RequestMapping("/add-header") + public ResponseEntity addHeader(HttpServletRequest request) { + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Header", "FOO"); + ResponseEntity result = new ResponseEntity( + 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 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); + } } } } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ZuulProxyTestBase.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/support/ZuulProxyTestBase.java similarity index 74% rename from spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ZuulProxyTestBase.java rename to spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/support/ZuulProxyTestBase.java index 85afec6b..ffdb3771 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ZuulProxyTestBase.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/support/ZuulProxyTestBase.java @@ -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 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 ribbonServerList() { + return new StaticServerList<>(new Server("localhost", this.port)); + } - @Bean - public ServerList 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 ribbonServerList() { + return new StaticServerList<>(new Server("localhost", this.port)); + } - @Bean - public ServerList ribbonServerList() { - return new StaticServerList<>(new Server("localhost", this.port)); } -} + public static class MyErrorController extends BasicErrorController { + ThreadLocal uriToMatch = new ThreadLocal<>(); -class MyErrorController extends BasicErrorController { - ThreadLocal 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> error(HttpServletRequest request) { + String errorUri = (String) request.getAttribute("javax.servlet.error.request_uri"); - @Override - public ResponseEntity> 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); + } } }