Browse Source
lays the groundworkd to implement an httpclient ribbon command fixes gh-412pull/6/head
Spencer Gibb
9 years ago
11 changed files with 572 additions and 328 deletions
@ -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"); |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
@ -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); |
||||
|
||||
} |
Loading…
Reference in new issue