Commit 18dff852 by Christian Lohmann Committed by Spencer Gibb

an Apache http client zuul ribbon command

This is an implementation of a RibbonCommand and a factory which makes use of a loadbalancing http client based on apache's http-components http-client. With this command/factory it is possible to route PATCH http requests via zuul/ribbon as well. fixes gh-412
parent 81b430db
/*
* 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.apache;
import java.io.InputStream;
import java.net.URI;
import java.util.List;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.entity.BasicHttpEntity;
import org.springframework.util.MultiValueMap;
import com.netflix.client.ClientRequest;
import lombok.Getter;
/**
* @author Christian Lohmann
*/
@Getter
public class RibbonApacheHttpRequest extends ClientRequest implements Cloneable {
private final String method;
private final MultiValueMap<String, String> headers;
private final MultiValueMap<String, String> params;
private final InputStream requestEntity;
public RibbonApacheHttpRequest(final String method, final URI uri,
final Boolean retryable, final MultiValueMap<String, String> headers,
final MultiValueMap<String, String> params, final InputStream requestEntity) {
this.method = method;
this.uri = uri;
this.isRetriable = retryable;
this.headers = headers;
this.params = params;
this.requestEntity = requestEntity;
}
public HttpUriRequest toRequest(final RequestConfig requestConfig) {
final RequestBuilder builder = RequestBuilder.create(this.method);
builder.setUri(this.uri);
for (final String name : this.headers.keySet()) {
final List<String> values = this.headers.get(name);
for (final String value : values) {
builder.addHeader(name, value);
}
}
for (final String name : this.params.keySet()) {
final List<String> values = this.params.get(name);
for (final String value : values) {
builder.addParameter(name, value);
}
}
if (this.requestEntity != null) {
final BasicHttpEntity entity;
entity = new BasicHttpEntity();
entity.setContent(this.requestEntity);
builder.setEntity(entity);
}
builder.setConfig(requestConfig);
return builder.build();
}
public RibbonApacheHttpRequest withNewUri(final URI uri) {
return new RibbonApacheHttpRequest(this.method, uri, this.isRetriable,
this.headers, this.params, this.requestEntity);
}
}
/*
* 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.apache;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import com.google.common.reflect.TypeToken;
import com.netflix.client.ClientException;
import com.netflix.client.http.CaseInsensitiveMultiMap;
import com.netflix.client.http.HttpHeaders;
/**
* @author Christian Lohmann
*/
public class RibbonApacheHttpResponse implements com.netflix.client.http.HttpResponse {
private HttpResponse httpResponse;
private URI uri;
public RibbonApacheHttpResponse(final HttpResponse httpResponse, final URI uri) {
this.httpResponse = httpResponse;
this.uri = uri;
}
@Override
public Object getPayload() throws ClientException {
try {
return this.httpResponse.getEntity().getContent();
}
catch (final IOException e) {
throw new ClientException(e.getMessage(), e);
}
}
@Override
public boolean hasPayload() {
return this.httpResponse.getEntity() != null;
}
@Override
public boolean isSuccess() {
return this.httpResponse.getStatusLine().getStatusCode() == 200;
}
@Override
public URI getRequestedURI() {
return this.uri;
}
public int getStatus() {
return httpResponse.getStatusLine().getStatusCode();
}
public String getStatusLine() {
return httpResponse.getStatusLine().toString();
}
@Override
public Map<String, Collection<String>> getHeaders() {
final Map<String, Collection<String>> headers = new HashMap<>();
for (final Header header : this.httpResponse.getAllHeaders()) {
if (headers.containsKey(header.getName())) {
headers.get(header.getName()).add(header.getValue());
}
else {
final List<String> values = new ArrayList<>();
values.add(header.getValue());
headers.put(header.getName(), values);
}
}
return headers;
}
@Override
public HttpHeaders getHttpHeaders() {
final CaseInsensitiveMultiMap headers = new CaseInsensitiveMultiMap();
for (final Header header : httpResponse.getAllHeaders()) {
headers.addHeader(header.getName(), header.getValue());
}
return headers;
}
@Override
public void close() {
if (this.httpResponse != null && this.httpResponse.getEntity() != null) {
try {
this.httpResponse.getEntity().getContent().close();
}
catch (final IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}
@Override
public InputStream getInputStream() {
try {
return this.httpResponse.getEntity().getContent();
}
catch (final IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
@Override
public boolean hasEntity() {
return hasPayload();
}
/**
* Not used
*/
@Override
public <T> T getEntity(final Class<T> type) throws Exception {
return null;
}
/**
* Not used
*/
@Override
public <T> T getEntity(final Type type) throws Exception {
return null;
}
/**
* Not used
*/
@Override
public <T> T getEntity(final TypeToken<T> type) throws Exception {
return null;
}
}
/*
* 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.apache;
import java.net.URI;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.web.util.UriComponentsBuilder;
import com.netflix.client.AbstractLoadBalancerAwareClient;
import com.netflix.client.RequestSpecificRetryHandler;
import com.netflix.client.RetryHandler;
import com.netflix.client.config.CommonClientConfigKey;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer;
import lombok.Setter;
/**
* @author Christian Lohmann
*/
public class RibbonLoadBalancingHttpClient
extends AbstractLoadBalancerAwareClient<RibbonApacheHttpRequest, RibbonApacheHttpResponse> {
private final HttpClient delegate = HttpClientBuilder.create().build();
@Setter
private int connectTimeout;
@Setter
private int readTimeout;
@Setter
private boolean secure;
@Setter
private IClientConfig clientConfig;
public RibbonLoadBalancingHttpClient() {
super(null);
this.setRetryHandler(RetryHandler.DEFAULT);
}
public RibbonLoadBalancingHttpClient(final ILoadBalancer lb) {
super(lb);
this.setRetryHandler(RetryHandler.DEFAULT);
}
@Override
public RequestSpecificRetryHandler getRequestSpecificRetryHandler(
final RibbonApacheHttpRequest request, final IClientConfig requestConfig) {
if (this.clientConfig.get(CommonClientConfigKey.OkToRetryOnAllOperations, false)) {
return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(),
requestConfig);
}
if (!request.getMethod().equals("GET")) {
return new RequestSpecificRetryHandler(true, false, this.getRetryHandler(),
requestConfig);
}
else {
return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(),
requestConfig);
}
}
@Override
public RibbonApacheHttpResponse execute(RibbonApacheHttpRequest request,
final IClientConfig configOverride) throws Exception {
final RequestConfig.Builder builder = RequestConfig.custom();
if (configOverride != null) {
builder.setConnectTimeout(configOverride.get(
CommonClientConfigKey.ConnectTimeout, this.connectTimeout));
builder.setConnectionRequestTimeout(configOverride.get(
CommonClientConfigKey.ReadTimeout, this.readTimeout));
}
else {
builder.setConnectTimeout(this.connectTimeout);
builder.setConnectionRequestTimeout(this.readTimeout);
}
final RequestConfig requestConfig = builder.build();
if (isSecure(configOverride)) {
final URI secureUri = UriComponentsBuilder.fromUri(request.getUri())
.scheme("https").build().toUri();
request = request.withNewUri(secureUri);
}
final HttpUriRequest httpUriRequest = request.toRequest(requestConfig);
final HttpResponse httpResponse = this.delegate.execute(httpUriRequest);
return new RibbonApacheHttpResponse(httpResponse, httpUriRequest.getURI());
}
private boolean isSecure(final IClientConfig config) {
return (config != null) ? config.get(CommonClientConfigKey.IsSecure) : secure;
}
}
/*
* Copyright 2013-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.netflix.zuul.filters.route.apache;
import java.io.InputStream;
import java.net.URI;
import org.springframework.cloud.netflix.ribbon.RibbonHttpResponse;
import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpRequest;
import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpResponse;
import org.springframework.cloud.netflix.ribbon.apache.RibbonLoadBalancingHttpClient;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommand;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.MultiValueMap;
import com.netflix.config.DynamicIntProperty;
import com.netflix.config.DynamicPropertyFactory;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.zuul.constants.ZuulConstants;
import com.netflix.zuul.context.RequestContext;
/**
* @author Christian Lohmann
*/
public class HttpClientRibbonCommand extends HystrixCommand<ClientHttpResponse> implements
RibbonCommand {
private final RibbonLoadBalancingHttpClient client;
private final String method;
private final String uri;
private final MultiValueMap<String, String> headers;
private final MultiValueMap<String, String> params;
private final InputStream requestEntity;
private final Boolean retryable;
public HttpClientRibbonCommand(final RibbonLoadBalancingHttpClient client,
final String method, final String uri,
final MultiValueMap<String, String> headers,
final MultiValueMap<String, String> params, final InputStream requestEntity,
final Boolean retryable) {
this("default", client, method, uri, headers, params, requestEntity, retryable);
}
public HttpClientRibbonCommand(final String commandKey,
final RibbonLoadBalancingHttpClient client, final String method,
final String uri, final MultiValueMap<String, String> headers,
final MultiValueMap<String, String> params, final InputStream requestEntity,
final Boolean retryable) {
super(getSetter(commandKey));
this.client = client;
this.method = method;
this.uri = uri;
this.headers = headers;
this.params = params;
this.requestEntity = requestEntity;
this.retryable = retryable;
}
protected static Setter getSetter(final String commandKey) {
// we want to default to semaphore-isolation since this wraps
// 2 others commands that are already thread isolated
final String name = ZuulConstants.ZUUL_EUREKA + commandKey
+ ".semaphore.maxSemaphores";
final DynamicIntProperty value = DynamicPropertyFactory.getInstance()
.getIntProperty(name, 100);
final HystrixCommandProperties.Setter setter = HystrixCommandProperties
.Setter()
.withExecutionIsolationStrategy(
HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
.withExecutionIsolationSemaphoreMaxConcurrentRequests(value.get());
return Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RibbonCommand"))
.andCommandKey(
HystrixCommandKey.Factory.asKey(commandKey + "RibbonCommand"))
.andCommandPropertiesDefaults(setter);
}
@Override
protected ClientHttpResponse run() throws Exception {
return forward();
}
protected ClientHttpResponse forward() throws Exception {
final RequestContext context = RequestContext.getCurrentContext();
URI uriInstance = new URI(this.uri);
RibbonApacheHttpRequest request = new RibbonApacheHttpRequest(this.method,
uriInstance, this.retryable, this.headers, this.params,
this.requestEntity);
final RibbonApacheHttpResponse response = this.client
.executeWithLoadBalancer(request);
context.set("ribbonResponse", response);
// Explicitly close the HttpResponse if the Hystrix command timed out to
// release the underlying HTTP connection held by the response.
//
if (this.isResponseTimedOut()) {
if (response != null) {
response.close();
}
}
return new RibbonHttpResponse(response);
}
}
/*
* Copyright 2013-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.netflix.zuul.filters.route.apache;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.netflix.ribbon.apache.RibbonLoadBalancingHttpClient;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory;
import lombok.RequiredArgsConstructor;
/**
* @author Christian Lohmann
*/
@RequiredArgsConstructor
public class HttpClientRibbonCommandFactory implements
RibbonCommandFactory<HttpClientRibbonCommand> {
private final SpringClientFactory clientFactory;
@Override
public HttpClientRibbonCommand create(final RibbonCommandContext context) {
final String serviceId = context.getServiceId();
final RibbonLoadBalancingHttpClient client = clientFactory.getClient(serviceId,
RibbonLoadBalancingHttpClient.class);
client.setLoadBalancer(this.clientFactory.getLoadBalancer(serviceId));
client.setClientConfig(this.clientFactory.getClientConfig(serviceId));
final HttpClientRibbonCommand httpClientRibbonCommand = new HttpClientRibbonCommand(
serviceId, client, context.getVerb(), context.getUri(),
context.getHeaders(), context.getParams(), context.getRequestEntity(),
context.getRetryable());
return httpClientRibbonCommand;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment