refactor RibbonCommand to an interface and factory
lays the groundworkd to implement an httpclient ribbon command fixes gh-412
This commit is contained in:
@@ -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<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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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,29 +71,9 @@ public class RibbonRoutingFilter extends ZuulFilter {
|
||||
@Override
|
||||
public Object run() {
|
||||
RequestContext context = RequestContext.getCurrentContext();
|
||||
HttpServletRequest request = context.getRequest();
|
||||
|
||||
MultiValueMap<String, String> headers = this.helper
|
||||
.buildZuulRequestHeaders(request);
|
||||
MultiValueMap<String, String> params = this.helper
|
||||
.buildZuulRequestQueryParams(request);
|
||||
Verb 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);
|
||||
RibbonCommandContext commandContext = buildCommandContext(context);
|
||||
ClientHttpResponse response = forward(commandContext);
|
||||
setResponse(response);
|
||||
return response;
|
||||
}
|
||||
@@ -114,17 +84,37 @@ public class RibbonRoutingFilter extends ZuulFilter {
|
||||
return null;
|
||||
}
|
||||
|
||||
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,
|
||||
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);
|
||||
String verb = getVerb(request);
|
||||
InputStream requestEntity = getRequestBody(request);
|
||||
|
||||
String serviceId = (String) context.get("serviceId");
|
||||
Boolean retryable = (Boolean) context.get("retryable");
|
||||
|
||||
String uri = this.helper.buildZuulRequestURI(request);
|
||||
|
||||
// remove double slashes
|
||||
uri = uri.replace("//", "/");
|
||||
|
||||
return new RibbonCommandContext(serviceId, 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 {
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
return requestEntity;
|
||||
}
|
||||
|
||||
private Verb getVerb(HttpServletRequest request) {
|
||||
String sMethod = request.getMethod();
|
||||
return getVerb(sMethod);
|
||||
private String getVerb(HttpServletRequest request) {
|
||||
String method = request.getMethod();
|
||||
if (method == null)
|
||||
return "GET";
|
||||
return method;
|
||||
}
|
||||
|
||||
private Verb getVerb(String sMethod) {
|
||||
if (sMethod == null)
|
||||
return Verb.GET;
|
||||
try {
|
||||
return Verb.valueOf(sMethod.toUpperCase());
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
return Verb.GET;
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user