diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonClientHttpRequestFactory.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonClientHttpRequestFactory.java index b1157283..b2fc5f88 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonClientHttpRequestFactory.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonClientHttpRequestFactory.java @@ -16,28 +16,17 @@ package org.springframework.cloud.netflix.ribbon; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.net.URI; -import java.util.List; -import java.util.Map; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.client.AbstractClientHttpRequest; -import org.springframework.http.client.AbstractClientHttpResponse; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.http.client.ClientHttpResponse; import com.netflix.client.config.IClientConfig; import com.netflix.client.http.HttpRequest; -import com.netflix.client.http.HttpResponse; import com.netflix.niws.client.http.RestClient; /** @@ -72,125 +61,4 @@ public class RibbonClientHttpRequestFactory implements ClientHttpRequestFactory return new RibbonHttpRequest(uri, verb, client, clientConfig); } - public class RibbonHttpRequest extends AbstractClientHttpRequest { - - private HttpRequest.Builder builder; - private URI uri; - private HttpRequest.Verb verb; - private RestClient client; - private IClientConfig config; - private ByteArrayOutputStream outputStream = null; - - public RibbonHttpRequest(URI uri, HttpRequest.Verb verb, RestClient client, - IClientConfig config) { - this.uri = uri; - this.verb = verb; - this.client = client; - this.config = config; - this.builder = HttpRequest.newBuilder().uri(uri).verb(verb); - } - - @Override - public HttpMethod getMethod() { - return HttpMethod.valueOf(verb.name()); - } - - @Override - public URI getURI() { - return uri; - } - - @Override - protected OutputStream getBodyInternal(HttpHeaders headers) throws IOException { - if (outputStream == null) { - outputStream = new ByteArrayOutputStream(); - } - return outputStream; - } - - @Override - @SuppressWarnings("deprecation") - protected ClientHttpResponse executeInternal(HttpHeaders headers) - throws IOException { - try { - addHeaders(headers); - if (outputStream != null) { - outputStream.close(); - builder.entity(outputStream.toByteArray()); - } - HttpRequest request = builder.build(); - HttpResponse response = client.execute(request, config); - return new RibbonHttpResponse(response); - } catch (Exception e) { - throw new IOException(e); - } - - //TODO: fix stats, now that execute is not called - // use execute here so stats are collected - /*return loadBalancer.execute(this.config.getClientName(), new LoadBalancerRequest() { - @Override - public ClientHttpResponse apply(ServiceInstance instance) throws Exception { - } - });*/ - } - - private void addHeaders(HttpHeaders headers) { - for (String name : headers.keySet()) { - // apache http RequestContent pukes if there is a body and - // the dynamic headers are already present - if (!isDynamic(name) || outputStream == null) { - List values = headers.get(name); - for (String value : values) { - builder.header(name, value); - } - } - } - } - - private boolean isDynamic(String name) { - return name.equals("Content-Length") || name.equals("Transfer-Encoding"); - } - } - - public class RibbonHttpResponse extends AbstractClientHttpResponse { - - private HttpResponse response; - private HttpHeaders httpHeaders; - - public RibbonHttpResponse(HttpResponse response) { - this.response = response; - this.httpHeaders = new HttpHeaders(); - List> headers = response.getHttpHeaders() - .getAllHeaders(); - for (Map.Entry header : headers) { - this.httpHeaders.add(header.getKey(), header.getValue()); - } - } - - @Override - public InputStream getBody() throws IOException { - return response.getInputStream(); - } - - @Override - public HttpHeaders getHeaders() { - return this.httpHeaders; - } - - @Override - public int getRawStatusCode() throws IOException { - return response.getStatus(); - } - - @Override - public String getStatusText() throws IOException { - return HttpStatus.valueOf(response.getStatus()).name(); - } - - @Override - public void close() { - response.close(); - } - - } } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonHttpRequest.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonHttpRequest.java new file mode 100644 index 00000000..f5e05edb --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonHttpRequest.java @@ -0,0 +1,115 @@ +/* + * 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; + +import com.netflix.client.config.IClientConfig; +import com.netflix.client.http.HttpRequest; +import com.netflix.client.http.HttpResponse; +import com.netflix.niws.client.http.RestClient; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.AbstractClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.util.List; + +/** + * @author Spencer Gibb + */ +public class RibbonHttpRequest extends AbstractClientHttpRequest { + + private HttpRequest.Builder builder; + private URI uri; + private HttpRequest.Verb verb; + private RestClient client; + private IClientConfig config; + private ByteArrayOutputStream outputStream = null; + + public RibbonHttpRequest(URI uri, HttpRequest.Verb verb, RestClient client, + IClientConfig config) { + this.uri = uri; + this.verb = verb; + this.client = client; + this.config = config; + this.builder = HttpRequest.newBuilder().uri(uri).verb(verb); + } + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(verb.name()); + } + + @Override + public URI getURI() { + return uri; + } + + @Override + protected OutputStream getBodyInternal(HttpHeaders headers) throws IOException { + if (outputStream == null) { + outputStream = new ByteArrayOutputStream(); + } + return outputStream; + } + + @Override + @SuppressWarnings("deprecation") + protected ClientHttpResponse executeInternal(HttpHeaders headers) + throws IOException { + try { + addHeaders(headers); + if (outputStream != null) { + outputStream.close(); + builder.entity(outputStream.toByteArray()); + } + HttpRequest request = builder.build(); + HttpResponse response = client.execute(request, config); + return new RibbonHttpResponse(response); + } catch (Exception e) { + throw new IOException(e); + } + + //TODO: fix stats, now that execute is not called + // use execute here so stats are collected + /*return loadBalancer.execute(this.config.getClientName(), new LoadBalancerRequest() { +@Override +public ClientHttpResponse apply(ServiceInstance instance) throws Exception { +} +});*/ + } + + private void addHeaders(HttpHeaders headers) { + for (String name : headers.keySet()) { + // apache http RequestContent pukes if there is a body and + // the dynamic headers are already present + if (!isDynamic(name) || outputStream == null) { + List values = headers.get(name); + for (String value : values) { + builder.header(name, value); + } + } + } + } + + private boolean isDynamic(String name) { + return name.equals("Content-Length") || name.equals("Transfer-Encoding"); + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonHttpResponse.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonHttpResponse.java new file mode 100644 index 00000000..356b27bd --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonHttpResponse.java @@ -0,0 +1,72 @@ +/* + * 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; + +import com.netflix.client.http.HttpResponse; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.AbstractClientHttpResponse; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +/** + * @author Spencer Gibb + */ +public class RibbonHttpResponse extends AbstractClientHttpResponse { + + private HttpResponse response; + private HttpHeaders httpHeaders; + + public RibbonHttpResponse(HttpResponse response) { + this.response = response; + this.httpHeaders = new HttpHeaders(); + List> headers = response.getHttpHeaders() + .getAllHeaders(); + for (Map.Entry header : headers) { + this.httpHeaders.add(header.getKey(), header.getValue()); + } + } + + @Override + public InputStream getBody() throws IOException { + return response.getInputStream(); + } + + @Override + public HttpHeaders getHeaders() { + return this.httpHeaders; + } + + @Override + public int getRawStatusCode() throws IOException { + return response.getStatus(); + } + + @Override + public String getStatusText() throws IOException { + return HttpStatus.valueOf(response.getStatus()).name(); + } + + @Override + public void close() { + response.close(); + } + +} 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 2c7a2bd4..c57d8c3c 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 @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.actuate.trace.TraceRepository; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.event.HeartbeatEvent; @@ -32,6 +33,8 @@ import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper; import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; 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.RibbonCommandFactory; import org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter; import org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter; import org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping; @@ -69,6 +72,12 @@ public class ZuulProxyConfiguration extends ZuulConfiguration { this.zuulProperties); } + @Bean + @ConditionalOnMissingBean + public RibbonCommandFactory ribbonCommandFactory() { + return new RestClientRibbonCommandFactory(this.clientFactory); + } + // pre filters @Bean public PreDecorationFilter preDecorationFilter() { @@ -78,12 +87,12 @@ public class ZuulProxyConfiguration extends ZuulConfiguration { // route filters @Bean - public RibbonRoutingFilter ribbonRoutingFilter() { + public RibbonRoutingFilter ribbonRoutingFilter(RibbonCommandFactory ribbonCommandFactory) { ProxyRequestHelper helper = new ProxyRequestHelper(); if (this.traces != null) { helper.setTraces(this.traces); } - RibbonRoutingFilter filter = new RibbonRoutingFilter(helper, this.clientFactory); + RibbonRoutingFilter filter = new RibbonRoutingFilter(helper, ribbonCommandFactory); return filter; } 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 new file mode 100644 index 00000000..5959a5fa --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommand.java @@ -0,0 +1,183 @@ +/* + * 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 + "RibbonCommand")) + .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(retryable != null) { + builder.setRetriable(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 headers; + } + + protected MultiValueMap getParams() { + return params; + } + + protected InputStream getRequestEntity() { + return requestEntity; + } + + protected RestClient getRestClient() { + return restClient; + } + + protected Boolean getRetryable() { + return retryable; + } + + protected URI getUri() { + return uri; + } + + protected Verb getVerb() { + return 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 new file mode 100644 index 00000000..da011c68 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommandFactory.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; + +import com.netflix.client.http.HttpRequest; +import com.netflix.niws.client.http.RestClient; +import lombok.SneakyThrows; +import org.springframework.cloud.netflix.ribbon.SpringClientFactory; + +/** + * @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 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/RibbonCommand.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommand.java index d8b0b717..519a0f84 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommand.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommand.java @@ -16,131 +16,11 @@ 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.util.MultiValueMap; -import org.springframework.util.StringUtils; -import org.springframework.web.util.UriComponentsBuilder; - -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; +import com.netflix.hystrix.HystrixExecutable; +import org.springframework.http.client.ClientHttpResponse; /** - * 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 + * @author Spencer Gibb */ -@SuppressWarnings("deprecation") -public class RibbonCommand extends HystrixCommand { - - private RestClient restClient; - - private Verb verb; - - private URI uri; - - private Boolean retryable; - - private MultiValueMap headers; - - private MultiValueMap params; - - private InputStream requestEntity; - - public RibbonCommand(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 RibbonCommand(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; - } - - private 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 + "RibbonCommand")) - .andCommandPropertiesDefaults(setter); - } - - @Override - protected HttpResponse run() throws Exception { - return forward(); - } - - private HttpResponse forward() throws Exception { - RequestContext context = RequestContext.getCurrentContext(); - Builder builder = HttpRequest.newBuilder().verb(this.verb).uri(this.uri) - .entity(this.requestEntity); - - if(retryable != null) { - builder.setRetriable(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); - } - } - 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(); - } - } - return response; - } - +public interface RibbonCommand extends HystrixExecutable { } 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 new file mode 100644 index 00000000..507665cf --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandContext.java @@ -0,0 +1,36 @@ +/* + * 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 lombok.Value; +import org.springframework.util.MultiValueMap; + +import java.io.InputStream; + +/** + * @author Spencer Gibb + */ +@Value +public class RibbonCommandContext { + private final String serviceId; + private final String verb; + private final String uri; + private final Boolean retryable; + private final MultiValueMap headers; + private final MultiValueMap params; + private final InputStream requestEntity; +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandFactory.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandFactory.java new file mode 100644 index 00000000..f6007a3e --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandFactory.java @@ -0,0 +1,26 @@ +/* + * 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; + +/** + * @author Spencer Gibb + */ +public interface RibbonCommandFactory { + + T create(RibbonCommandContext context); + +} 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 ebdf2baf..c7117691 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 @@ -18,26 +18,19 @@ package org.springframework.cloud.netflix.zuul.filters.route; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; import java.util.Map; -import java.util.Map.Entry; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.extern.apachecommons.CommonsLog; -import org.springframework.cloud.netflix.ribbon.SpringClientFactory; import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper; -import org.springframework.util.LinkedMultiValueMap; +import org.springframework.http.client.ClientHttpResponse; import org.springframework.util.MultiValueMap; import com.netflix.client.ClientException; -import com.netflix.client.http.HttpRequest.Verb; -import com.netflix.client.http.HttpResponse; import com.netflix.hystrix.exception.HystrixRuntimeException; -import com.netflix.niws.client.http.RestClient; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; @@ -45,20 +38,17 @@ import com.netflix.zuul.exception.ZuulException; @CommonsLog public class RibbonRoutingFilter extends ZuulFilter { - public static final String CONTENT_ENCODING = "Content-Encoding"; - - private SpringClientFactory clientFactory; - private ProxyRequestHelper helper; + private RibbonCommandFactory ribbonCommandFactory; public RibbonRoutingFilter(ProxyRequestHelper helper, - SpringClientFactory clientFactory) { + RibbonCommandFactory ribbonCommandFactory) { this.helper = helper; - this.clientFactory = clientFactory; + this.ribbonCommandFactory = ribbonCommandFactory; } - public RibbonRoutingFilter(SpringClientFactory clientFactory) { - this(new ProxyRequestHelper(), clientFactory); + public RibbonRoutingFilter(RibbonCommandFactory ribbonCommandFactory) { + this(new ProxyRequestHelper(), ribbonCommandFactory); } @Override @@ -81,50 +71,50 @@ public class RibbonRoutingFilter extends ZuulFilter { @Override public Object run() { RequestContext context = RequestContext.getCurrentContext(); + try { + RibbonCommandContext commandContext = buildCommandContext(context); + ClientHttpResponse response = forward(commandContext); + setResponse(response); + return response; + } + catch (Exception ex) { + context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + context.set("error.exception", ex); + } + return null; + } + + private RibbonCommandContext buildCommandContext(RequestContext context) { HttpServletRequest request = context.getRequest(); MultiValueMap headers = this.helper .buildZuulRequestHeaders(request); MultiValueMap params = this.helper .buildZuulRequestQueryParams(request); - Verb verb = getVerb(request); + String verb = getVerb(request); InputStream requestEntity = getRequestBody(request); String serviceId = (String) context.get("serviceId"); Boolean retryable = (Boolean) context.get("retryable"); - RestClient restClient = this.clientFactory.getClient(serviceId, RestClient.class); - String uri = this.helper.buildZuulRequestURI(request); // remove double slashes uri = uri.replace("//", "/"); - String service = (String) context.get("serviceId"); - try { - HttpResponse response = forward(restClient, service, verb, uri, retryable, headers, params, - requestEntity); - setResponse(response); - return response; - } - catch (Exception ex) { - context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - context.set("error.exception", ex); - } - return null; + return new RibbonCommandContext(serviceId, verb, uri, retryable, + headers, params, requestEntity); } - private HttpResponse forward(RestClient restClient, String service, Verb verb, String uri, Boolean retryable, - MultiValueMap headers, MultiValueMap params, - InputStream requestEntity) throws Exception { - Map info = this.helper.debug(verb.verb(), uri, headers, params, - requestEntity); - RibbonCommand command = new RibbonCommand(service, restClient, verb, uri, retryable, - headers, params, requestEntity); + private ClientHttpResponse forward(RibbonCommandContext context) throws Exception { + Map info = this.helper.debug(context.getVerb(), context.getUri(), + context.getHeaders(), context.getParams(), context.getRequestEntity()); + + RibbonCommand command = ribbonCommandFactory.create(context); try { - HttpResponse response = command.execute(); - this.helper.appendDebug(info, response.getStatus(), - revertHeaders(response.getHeaders())); + ClientHttpResponse response = command.execute(); + this.helper.appendDebug(info, response.getStatusCode().value(), + response.getHeaders()); return response; } catch (HystrixRuntimeException ex) { @@ -143,15 +133,6 @@ public class RibbonRoutingFilter extends ZuulFilter { } - private MultiValueMap revertHeaders( - Map> headers) { - MultiValueMap map = new LinkedMultiValueMap<>(); - for (Entry> entry : headers.entrySet()) { - map.put(entry.getKey(), new ArrayList<>(entry.getValue())); - } - return map; - } - private InputStream getRequestBody(HttpServletRequest request) { InputStream requestEntity = null; // ApacheHttpClient4Handler does not support body in delete requests @@ -171,26 +152,16 @@ public class RibbonRoutingFilter extends ZuulFilter { return requestEntity; } - private Verb getVerb(HttpServletRequest request) { - String sMethod = request.getMethod(); - return getVerb(sMethod); - } - - private Verb getVerb(String sMethod) { - if (sMethod == null) - return Verb.GET; - try { - return Verb.valueOf(sMethod.toUpperCase()); - } - catch (IllegalArgumentException e) { - return Verb.GET; - } + private String getVerb(HttpServletRequest request) { + String method = request.getMethod(); + if (method == null) + return "GET"; + return method; } - private void setResponse(HttpResponse resp) throws ClientException, IOException { - this.helper.setResponse(resp.getStatus(), - !resp.hasEntity() ? null : resp.getInputStream(), - revertHeaders(resp.getHeaders())); + private void setResponse(ClientHttpResponse resp) throws ClientException, IOException { + this.helper.setResponse(resp.getStatusCode().value(), + resp.getBody() == null ? null : resp.getBody(), resp.getHeaders()); } } 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/SampleZuulProxyApplicationTests.java index 5c3c3f0e..c01a4653 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/SampleZuulProxyApplicationTests.java @@ -17,6 +17,7 @@ package org.springframework.cloud.netflix.zuul; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import java.util.Arrays; import java.util.HashMap; @@ -34,10 +35,11 @@ import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.boot.test.TestRestTemplate; import org.springframework.cloud.netflix.ribbon.RibbonClient; import org.springframework.cloud.netflix.ribbon.RibbonClients; -import org.springframework.cloud.netflix.zuul.EnableZuulProxy; -import org.springframework.cloud.netflix.zuul.RoutesEndpoint; +import org.springframework.cloud.netflix.ribbon.SpringClientFactory; import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute; +import org.springframework.cloud.netflix.zuul.filters.route.RestClientRibbonCommandFactory; +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; @@ -80,6 +82,9 @@ public class SampleZuulProxyApplicationTests { @Autowired private RoutesEndpoint endpoint; + @Autowired + private RibbonCommandFactory ribbonCommandFactory; + @Test public void bindRouteUsingPhysicalRoute() { assertEquals("http://localhost:7777/local", @@ -183,6 +188,12 @@ public class SampleZuulProxyApplicationTests { assertEquals("Received {key=[overridden]}", result.getBody()); } + @Test + public void ribbonCommandFactoryOverridden() { + assertTrue("ribbonCommandFactory not a MyRibbonCommandFactory", + ribbonCommandFactory instanceof SampleZuulProxyApplication.MyRibbonCommandFactory); + } + } // Don't use @SpringBootApplication because we don't want to component scan @@ -235,6 +246,11 @@ class SampleZuulProxyApplication { return "Hello space"; } + @Bean + public RibbonCommandFactory ribbonCommandFactory(SpringClientFactory clientFactory) { + return new MyRibbonCommandFactory(clientFactory); + } + @Bean public ZuulFilter sampleFilter() { return new ZuulFilter() { @@ -269,6 +285,13 @@ class SampleZuulProxyApplication { SpringApplication.run(SampleZuulProxyApplication.class, args); } + public static class MyRibbonCommandFactory extends RestClientRibbonCommandFactory { + + public MyRibbonCommandFactory(SpringClientFactory clientFactory) { + super(clientFactory); + } + } + } // Load balancer with fixed server list for "simple" pointing to localhost