Browse Source

refactor RibbonCommand to an interface and factory

lays the groundworkd to implement an httpclient ribbon command

fixes gh-412
pull/6/head
Spencer Gibb 9 years ago
parent
commit
b494e1d2cc
  1. 132
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonClientHttpRequestFactory.java
  2. 115
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonHttpRequest.java
  3. 72
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonHttpResponse.java
  4. 13
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java
  5. 183
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommand.java
  6. 61
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommandFactory.java
  7. 128
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommand.java
  8. 36
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandContext.java
  9. 26
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandFactory.java
  10. 107
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java
  11. 27
      spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyApplicationTests.java

132
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonClientHttpRequestFactory.java

@ -16,28 +16,17 @@ @@ -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 @@ -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<ClientHttpResponse>() {
@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<String> 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<Map.Entry<String, String>> headers = response.getHttpHeaders()
.getAllHeaders();
for (Map.Entry<String, String> 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();
}
}
}

115
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonHttpRequest.java

@ -0,0 +1,115 @@ @@ -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<ClientHttpResponse>() {
@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<String> 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");
}
}

72
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/ribbon/RibbonHttpResponse.java

@ -0,0 +1,72 @@ @@ -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<Map.Entry<String, String>> headers = response.getHttpHeaders()
.getAllHeaders();
for (Map.Entry<String, String> 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();
}
}

13
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; @@ -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; @@ -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 { @@ -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 { @@ -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;
}

183
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommand.java

@ -0,0 +1,183 @@ @@ -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<ClientHttpResponse> implements RibbonCommand {
private RestClient restClient;
private Verb verb;
private URI uri;
private Boolean retryable;
private MultiValueMap<String, String> headers;
private MultiValueMap<String, String> params;
private InputStream requestEntity;
public RestClientRibbonCommand(RestClient restClient, Verb verb, String uri,
Boolean retryable,
MultiValueMap<String, String> headers,
MultiValueMap<String, String> params, InputStream requestEntity)
throws URISyntaxException {
this("default", restClient, verb, uri, retryable , headers, params, requestEntity);
}
public RestClientRibbonCommand(String commandKey, RestClient restClient, Verb verb, String uri,
Boolean retryable,
MultiValueMap<String, String> headers,
MultiValueMap<String, String> params, InputStream requestEntity)
throws URISyntaxException {
super(getSetter(commandKey));
this.restClient = restClient;
this.verb = verb;
this.uri = new URI(uri);
this.retryable = retryable;
this.headers = headers;
this.params = params;
this.requestEntity = requestEntity;
}
protected static HystrixCommand.Setter getSetter(String commandKey) {
// we want to default to semaphore-isolation since this wraps
// 2 others commands that are already thread isolated
String name = ZuulConstants.ZUUL_EUREKA + commandKey + ".semaphore.maxSemaphores";
DynamicIntProperty value = DynamicPropertyFactory.getInstance().getIntProperty(
name, 100);
HystrixCommandProperties.Setter setter = HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)
.withExecutionIsolationSemaphoreMaxConcurrentRequests(value.get());
return Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RibbonCommand"))
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey + "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<String> values = this.headers.get(name);
for (String value : values) {
builder.header(name, value);
}
}
for (String name : this.params.keySet()) {
List<String> values = this.params.get(name);
for (String value : values) {
builder.queryParams(name, value);
}
}
customizeRequest(builder);
HttpRequest httpClientRequest = builder.build();
HttpResponse response = this.restClient
.executeWithLoadBalancer(httpClientRequest);
context.set("ribbonResponse", response);
// Explicitly close the HttpResponse if the Hystrix command timed out to
// release the underlying HTTP connection held by the response.
//
if( this.isResponseTimedOut() ) {
if( response!= null ) {
response.close();
}
}
RibbonHttpResponse ribbonHttpResponse = new RibbonHttpResponse(response);
return ribbonHttpResponse;
}
protected void customizeRequest(Builder requestBuilder) {
}
protected MultiValueMap<String, String> getHeaders() {
return headers;
}
protected MultiValueMap<String, String> 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;
}
}

61
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RestClientRibbonCommandFactory.java

@ -0,0 +1,61 @@ @@ -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<RestClientRibbonCommand> {
private final SpringClientFactory clientFactory;
public RestClientRibbonCommandFactory(SpringClientFactory clientFactory) {
this.clientFactory = clientFactory;
}
@Override
@SuppressWarnings("deprecation")
@SneakyThrows
public RestClientRibbonCommand create(RibbonCommandContext context) {
RestClient restClient = this.clientFactory.getClient(context.getServiceId(),
RestClient.class);
return new RestClientRibbonCommand(
context.getServiceId(), restClient, getVerb(context.getVerb()),
context.getUri(), context.getRetryable(), context.getHeaders(),
context.getParams(), context.getRequestEntity());
}
protected SpringClientFactory getClientFactory() {
return 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;
}
}
}

128
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommand.java

@ -16,131 +16,11 @@ @@ -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<HttpResponse> {
private RestClient restClient;
private Verb verb;
private URI uri;
private Boolean retryable;
private MultiValueMap<String, String> headers;
private MultiValueMap<String, String> params;
private InputStream requestEntity;
public RibbonCommand(RestClient restClient, Verb verb, String uri,
Boolean retryable,
MultiValueMap<String, String> headers,
MultiValueMap<String, String> params, InputStream requestEntity)
throws URISyntaxException {
this("default", restClient, verb, uri, retryable , headers, params, requestEntity);
}
public RibbonCommand(String commandKey, RestClient restClient, Verb verb, String uri,
Boolean retryable,
MultiValueMap<String, String> headers,
MultiValueMap<String, String> params, InputStream requestEntity)
throws URISyntaxException {
super(getSetter(commandKey));
this.restClient = restClient;
this.verb = verb;
this.uri = new URI(uri);
this.retryable = retryable;
this.headers = headers;
this.params = params;
this.requestEntity = requestEntity;
}
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<String> values = this.headers.get(name);
for (String value : values) {
builder.header(name, value);
}
}
for (String name : this.params.keySet()) {
List<String> values = this.params.get(name);
for (String value : values) {
builder.queryParams(name, value);
}
}
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<ClientHttpResponse> {
}

36
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandContext.java

@ -0,0 +1,36 @@ @@ -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<String, String> headers;
private final MultiValueMap<String, String> params;
private final InputStream requestEntity;
}

26
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonCommandFactory.java

@ -0,0 +1,26 @@ @@ -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 extends RibbonCommand> {
T create(RibbonCommandContext context);
}

107
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; @@ -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; @@ -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 { @@ -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<String, String> headers = this.helper
.buildZuulRequestHeaders(request);
MultiValueMap<String, String> 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<String, String> headers, MultiValueMap<String, String> params,
InputStream requestEntity) throws Exception {
Map<String, Object> 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<String, Object> 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 { @@ -143,15 +133,6 @@ public class RibbonRoutingFilter extends ZuulFilter {
}
private MultiValueMap<String, String> revertHeaders(
Map<String, Collection<String>> headers) {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
for (Entry<String, Collection<String>> 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 { @@ -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());
}
}

27
spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyApplicationTests.java

@ -17,6 +17,7 @@ @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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

Loading…
Cancel
Save