com.turbospaces.http.CloseableHttpClientFactoryBean Maven / Gradle / Ivy
The newest version!
package com.turbospaces.http;
import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpRequestWrapper;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.execchain.ClientExecChain;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.config.AbstractFactoryBean;
import com.netflix.archaius.api.Property;
import com.turbospaces.cfg.ApplicationProperties;
import io.micrometer.core.instrument.MeterRegistry;
public class CloseableHttpClientFactoryBean extends AbstractFactoryBean implements BeanNameAware {
protected final Logger log = LoggerFactory.getLogger(getClass());
protected final ApplicationProperties props;
protected final MeterRegistry meterRegistry;
protected String beanName;
public CloseableHttpClientFactoryBean(ApplicationProperties props, MeterRegistry meterRegistry) {
this.props = Objects.requireNonNull(props);
this.meterRegistry = Objects.requireNonNull(meterRegistry);
setSingleton(true);
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
@Override
public Class> getObjectType() {
return CloseableHttpClient.class;
}
@SuppressWarnings("deprecation")
public HttpClientBuilder getBuilder() {
HttpClientBuilder builder = new HttpClientBuilder() {
@Override
protected ClientExecChain decorateProtocolExec(ClientExecChain protocolExec) {
return (route, request, clientContext, execAware) -> {
try {
Optional mockProxy = proxyMockRequest(props, request.getRequestLine().getUri());
if (mockProxy.isPresent()) {
HttpUriRequest proxyReq = RequestBuilder.copy(request.getOriginal()).setUri(mockProxy.get()).build();
URI proxyUri = new URI(props.MOCK_HTTP_PROXY.get());
HttpRoute proxyRoute = new HttpRoute(new HttpHost(proxyUri.getHost(), proxyUri.getPort(), proxyUri.getScheme()));
return protocolExec.execute(proxyRoute, HttpRequestWrapper.wrap(proxyReq), clientContext, execAware);
}
Optional proxy = proxyRequest(props, request.getRequestLine().getUri());
if (proxy.isPresent()) {
HttpUriRequest proxyReq = RequestBuilder.copy(request.getOriginal()).setUri(proxy.get()).build();
URI proxyUri = new URI(props.HTTP_PROXY.get());
HttpRoute proxyRoute = new HttpRoute(new HttpHost(proxyUri.getHost(), proxyUri.getPort(), proxyUri.getScheme()));
return protocolExec.execute(proxyRoute, HttpRequestWrapper.wrap(proxyReq), clientContext, execAware);
}
} catch (Exception err) {
log.error("Failed to proxy request", err);
}
return protocolExec.execute(route, HttpRequestWrapper.wrap(request), clientContext, execAware);
};
}
};
// For capability with SOAPConnector
builder.addInterceptorFirst(new RemoveSoapHeadersInterceptor());
// ~ metrics
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setDefaultMaxPerRoute(props.HTTP_POOL_MAX_PER_ROUTE.get());
connectionManager.setMaxTotal(props.HTTP_POOL_MAX_SIZE.get());
connectionManager.setDefaultSocketConfig(socketConfig(props).build());
// ~ https://stackoverflow.com/questions/70175836/apache-httpclient-throws-java-net-socketexception-connection-reset-if-i-use-it
int validateAfterInactivity = (int) props.TCP_CONNECTION_VALIDATE_AFTER_INACTIVITY_TIMEOUT.get().toMillis();
if (validateAfterInactivity > 0) {
connectionManager.setValidateAfterInactivity(validateAfterInactivity);
}
long evictIdleConnection = props.TCP_CONNECTION_EVICT_IDLE_TIMEOUT.get().toMillis();
if (evictIdleConnection > 0) {
builder.evictExpiredConnections();
builder.evictIdleConnections(evictIdleConnection, TimeUnit.MILLISECONDS);
}
if (props.TCP_CONNECTION_CLOSE_IDLE_IMMEDIATELY.get()) {
connectionManager.closeIdleConnections(0, TimeUnit.MICROSECONDS);
}
builder.setConnectionManager(connectionManager);
builder.setRequestExecutor(CustomMicrometerHttpRequestExecutor.builder(meterRegistry).uriMapper(httpRequest -> {
List mask = props.HTTP_METRICS_OUTBOUND_PATH_MASK.get();
Header uriPattern = httpRequest.getLastHeader("URI_PATTERN");
if (uriPattern != null && uriPattern.getValue() != null) {
return uriPattern.getValue();
}
String uri = httpRequest.getRequestLine().getUri();
if (CollectionUtils.isNotEmpty(mask)) {
Optional mapping = mask.stream().filter(p -> p.asPredicate().test(uri)).map(Pattern::pattern).findAny();
if (mapping.isPresent()) {
return mapping.get();
}
}
return URI.create(uri).getPath();
}).build());
//
new io.micrometer.core.instrument.binder.httpcomponents.PoolingHttpClientConnectionManagerMetricsBinder(connectionManager, "httpclient-" + beanName).bindTo(meterRegistry);
builder.setKeepAliveStrategy(keepAliveStrategy(props));
builder.setDefaultRequestConfig(requestConfig(props).build());
if (props.HTTP_RETRY_REQUEST_ENABLED.get()) {
builder.setRetryHandler(new CustomHttpRequestRetryHandler(
props.HTTP_RETRY_REQUEST_COUNT.get(),
props.HTTP_RETRY_ALREADY_SENT_REQUEST_ENABLED.get()));
}
return builder;
}
@Override
protected CloseableHttpClient createInstance() throws Exception {
HttpClientBuilder builder = getBuilder();
return builder.build();
}
@Override
protected void destroyInstance(CloseableHttpClient instance) throws Exception {
if (instance != null) {
instance.close();
}
}
public static DefaultConnectionKeepAliveStrategy keepAliveStrategy(ApplicationProperties props) {
return new DefaultConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
long keepAliveDuration = super.getKeepAliveDuration(response, context);
if (keepAliveDuration <= 0) {
keepAliveDuration = props.TCP_KEEP_ALIVE_TIMEOUT.get().toMillis();
}
return keepAliveDuration;
}
};
}
public static RequestConfig.Builder requestConfig(ApplicationProperties props) {
return requestConfig(props.TCP_CONNECTION_TIMEOUT, props.TCP_SOCKET_TIMEOUT);
}
public static RequestConfig.Builder requestConfig(long connectTimeout, long soTimeout) {
int connectionTimeout = (int) TimeUnit.SECONDS.toMillis(connectTimeout);
int socketTimeout = (int) TimeUnit.SECONDS.toMillis(soTimeout);
RequestConfig.Builder requestCfg = RequestConfig.custom();
requestCfg.setConnectTimeout(connectionTimeout);
requestCfg.setSocketTimeout(socketTimeout);
requestCfg.setCookieSpec(CookieSpecs.STANDARD);
return requestCfg;
}
public static RequestConfig.Builder requestConfig(Supplier connectTimeout, Supplier soTimeout) {
return requestConfig(connectTimeout.get(), soTimeout.get());
}
public static RequestConfig.Builder requestConfig(Property connectTimeout, Property soTimeout) {
int connectTimeoutSecs = (int) connectTimeout.get().toSeconds();
int soTimeoutSecs = (int) soTimeout.get().toSeconds();
return requestConfig(connectTimeoutSecs, soTimeoutSecs);
}
public static SocketConfig.Builder socketConfig(ApplicationProperties props) {
int socketTimeout = (int) props.TCP_SOCKET_TIMEOUT.get().toMillis();
SocketConfig.Builder socketCfg = SocketConfig.custom();
socketCfg.setSoKeepAlive(props.TCP_KEEP_ALIVE.get());
socketCfg.setSoReuseAddress(props.TCP_REUSE_ADDRESS.get());
socketCfg.setTcpNoDelay(props.TCP_NO_DELAY.get());
socketCfg.setBacklogSize(props.TCP_SOCKET_BACKLOG.get());
socketCfg.setSoTimeout(socketTimeout);
return socketCfg;
}
public static Optional proxyRequest(ApplicationProperties props, String plainUri) {
if (StringUtils.isEmpty(props.HTTP_PROXY.get())) {
return Optional.empty();
}
//
// ~ check by pattern
//
List patterns = props.HTTP_REQUEST_TO_PROXY_PATTERNS.get();
if (CollectionUtils.isEmpty(patterns)) {
return Optional.empty();
}
if (Objects.nonNull(patterns)) {
boolean apply = patterns.stream().anyMatch(p -> p.asPredicate().test(plainUri));
if (BooleanUtils.isFalse(apply)) {
return Optional.empty();
}
}
URI uri = URI.create(plainUri);
URI proxyUri = URI.create(props.HTTP_PROXY.get());
String orig = new URIBuilder().setScheme(uri.getScheme()).setHost(uri.getHost()).setPort(uri.getPort()).toString();
return Optional.of(URI.create(new URIBuilder(uri)
.setScheme(proxyUri.getScheme())
.setHost(proxyUri.getHost())
.setPort(proxyUri.getPort())
.addParameter(HttpProto.PARAM_PROXY_REF, orig)
.toString()));
}
public static Optional proxyMockRequest(ApplicationProperties props, String plainUri) {
if (StringUtils.isEmpty(props.MOCK_HTTP_PROXY.get())) {
return Optional.empty();
}
//
// ~ check by pattern
//
List patterns = props.MOCK_HTTP_REQUEST_TO_PROXY_PATTERNS.get();
if (CollectionUtils.isEmpty(patterns)) {
return Optional.empty();
}
if (Objects.nonNull(patterns)) {
boolean apply = patterns.stream().anyMatch(p -> p.asPredicate().test(plainUri));
if (BooleanUtils.isFalse(apply)) {
return Optional.empty();
}
}
URI uri = URI.create(plainUri);
URI proxyUri = URI.create(props.MOCK_HTTP_PROXY.get());
return Optional.of(URI.create(new URIBuilder(uri)
.setScheme(proxyUri.getScheme())
.setHost(proxyUri.getHost())
.setPort(proxyUri.getPort())
.toString()));
}
/**
* Removes {@code Content-Length} and
* {@code Transfer-Encoding} headers from the request if present 'SOAPAction' header.
* Necessary, because some SAAJ and other SOAP implementations set
* these headers themselves, and HttpClient throws an exception if they have been set.
*/
public static class RemoveSoapHeadersInterceptor implements HttpRequestInterceptor {
@Override
public void process(HttpRequest request, HttpContext context) {
// Second condition is not required, but added for double check, these headers will be removed and added by HttpClient later.
if (request instanceof HttpEntityEnclosingRequest && request.getFirstHeader("SOAPAction") != null) {
if (request.containsHeader(HTTP.TRANSFER_ENCODING)) {
request.removeHeaders(HTTP.TRANSFER_ENCODING);
}
if (request.containsHeader(HTTP.CONTENT_LEN)) {
request.removeHeaders(HTTP.CONTENT_LEN);
}
}
}
}
}