All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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);
                }
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy