Created
March 17, 2026 02:02
-
-
Save robsonkades/b70254c00b729d772d1608983bf415ce to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import com.github.benmanes.caffeine.cache.Caffeine; | |
| import com.github.benmanes.caffeine.cache.RemovalCause; | |
| import org.springframework.cache.CacheManager; | |
| import org.springframework.cache.caffeine.CaffeineCacheManager; | |
| import org.springframework.context.annotation.Bean; | |
| import org.springframework.context.annotation.Configuration; | |
| import java.time.Duration; | |
| @Configuration | |
| public class CacheConfig { | |
| @Bean | |
| public CacheManager cacheManager() { | |
| Caffeine<Object, Object> caffeine = Caffeine.newBuilder() | |
| .initialCapacity(100) | |
| .maximumSize(500) | |
| .expireAfterAccess(Duration.ofMinutes(10)) | |
| .recordStats() | |
| .evictionListener((Object key, Object value, RemovalCause cause) -> { | |
| if (value instanceof HttpClientResources resources) { | |
| try { | |
| resources.close(); | |
| } catch (Exception ex) { | |
| // ideal: logar | |
| } | |
| } | |
| }); | |
| CaffeineCacheManager cacheManager = new CaffeineCacheManager("http-client"); | |
| cacheManager.setCaffeine(caffeine); | |
| cacheManager.setAllowNullValues(false); | |
| return cacheManager; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import org.springframework.cache.annotation.Cacheable; | |
| import org.springframework.stereotype.Component; | |
| @Component | |
| public class HttpClientRegistry { | |
| private final HttpClientResourcesFactory resourcesFactory; | |
| public HttpClientRegistry(HttpClientResourcesFactory resourcesFactory) { | |
| this.resourcesFactory = resourcesFactory; | |
| } | |
| @Cacheable(value = "http-client", key = "#certificateID.value") | |
| public HttpClientResources get(CertificateID certificateID) { | |
| return resourcesFactory.create(certificateID); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| mport org.apache.hc.client5.http.impl.classic.CloseableHttpClient; | |
| import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; | |
| import org.springframework.web.client.RestClient; | |
| public final class HttpClientResources implements AutoCloseable { | |
| private final CloseableHttpClient httpClient; | |
| private final PoolingHttpClientConnectionManager connectionManager; | |
| private final RestClient restClient; | |
| public HttpClientResources( | |
| CloseableHttpClient httpClient, | |
| PoolingHttpClientConnectionManager connectionManager, | |
| RestClient restClient | |
| ) { | |
| this.httpClient = httpClient; | |
| this.connectionManager = connectionManager; | |
| this.restClient = restClient; | |
| } | |
| public CloseableHttpClient httpClient() { | |
| return httpClient; | |
| } | |
| public PoolingHttpClientConnectionManager connectionManager() { | |
| return connectionManager; | |
| } | |
| public RestClient restClient() { | |
| return restClient; | |
| } | |
| @Override | |
| public void close() { | |
| try { | |
| httpClient.close(); | |
| } catch (Exception ignored) { | |
| } | |
| try { | |
| connectionManager.close(); | |
| } catch (Exception ignored) { | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import org.apache.hc.client5.http.HttpRequestRetryStrategy; | |
| import org.apache.hc.client5.http.config.ConnectionConfig; | |
| import org.apache.hc.client5.http.config.RequestConfig; | |
| import org.apache.hc.client5.http.impl.NoopUserTokenHandler; | |
| import org.apache.hc.client5.http.impl.classic.HttpClients; | |
| import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; | |
| import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; | |
| import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; | |
| import org.apache.hc.core5.http.io.SocketConfig; | |
| import org.apache.hc.core5.reactor.ssl.SSLBufferMode; | |
| import org.apache.hc.core5.util.TimeValue; | |
| import org.apache.hc.core5.util.Timeout; | |
| import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; | |
| import org.springframework.stereotype.Component; | |
| import org.springframework.web.client.RestClient; | |
| import javax.net.ssl.KeyManager; | |
| import javax.net.ssl.SSLContext; | |
| @Component | |
| public class HttpClientResourcesFactory { | |
| private final RestClient.Builder restClientBuilder; | |
| private final BundleManager bundleManager; | |
| private final HttpClientSettings settings; | |
| private final HttpRequestRetryStrategy retryStrategy; | |
| private final RequestConfig requestConfig; | |
| private final SocketConfig socketConfig; | |
| private final ConnectionConfig connectionConfig; | |
| public HttpClientResourcesFactory(final RestClient.Builder restClientBuilder, final BundleManager bundleManager) { | |
| this.restClientBuilder = restClientBuilder; | |
| this.bundleManager = bundleManager; | |
| this.settings = HttpClientSettings.defaults(); | |
| this.retryStrategy = new TransientFailureRetryStrategy(); | |
| this.requestConfig = RequestConfig.custom() | |
| .setConnectionRequestTimeout(Timeout.of(settings.connectionRequestTimeout())) | |
| .setResponseTimeout(Timeout.of(settings.readTimeout())) | |
| .build(); | |
| this.socketConfig = SocketConfig.custom() | |
| .setTcpNoDelay(true) | |
| .setSoKeepAlive(true) | |
| .setSoTimeout(Timeout.of(settings.readTimeout())) | |
| .build(); | |
| this.connectionConfig = ConnectionConfig.custom() | |
| .setConnectTimeout(Timeout.of(settings.connectTimeout())) | |
| .setSocketTimeout(Timeout.of(settings.readTimeout())) | |
| .setValidateAfterInactivity(TimeValue.of(settings.validateAfterInactivity())) | |
| .setTimeToLive(TimeValue.of(settings.connectionTtl())) | |
| .build(); | |
| } | |
| public HttpClientResources create(final CertificateID certificateID) { | |
| try { | |
| final var bundle = bundleManager.getSslBundle(certificateID); | |
| KeyManager[] keyManagers = bundle.getManagers().getKeyManagers(); | |
| final var insecureSslContext = SSLContext.getInstance(bundle.getProtocol()); | |
| insecureSslContext.init(keyManagers, TrustServer.TRUST_MANAGERS, null); | |
| final var tlsStrategy = new DefaultClientTlsStrategy( | |
| insecureSslContext, | |
| new String[]{"TLSv1.2", "TLSv1.3"}, | |
| null, | |
| SSLBufferMode.DYNAMIC, | |
| NoopHostnameVerifier.INSTANCE | |
| ); | |
| final var connectionManager = PoolingHttpClientConnectionManagerBuilder.create() | |
| .setTlsSocketStrategy(tlsStrategy) | |
| .setDefaultSocketConfig(socketConfig) | |
| .setDefaultConnectionConfig(connectionConfig) | |
| .setMaxConnTotal(settings.maxConnTotal()) | |
| .setMaxConnPerRoute(settings.maxConnPerRoute()) | |
| .build(); | |
| final var httpClient = HttpClients.custom() | |
| .setConnectionManager(connectionManager) | |
| .setDefaultRequestConfig(requestConfig) | |
| .setRetryStrategy(retryStrategy) | |
| .evictExpiredConnections() | |
| .setUserTokenHandler(NoopUserTokenHandler.INSTANCE) | |
| .evictIdleConnections(TimeValue.of(settings.idleEvictionTimeout())) | |
| .build(); | |
| final var requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); | |
| requestFactory.setConnectionRequestTimeout(settings.connectTimeout()); | |
| requestFactory.setConnectionRequestTimeout(settings.connectionRequestTimeout()); | |
| requestFactory.setReadTimeout(settings.readTimeout()); | |
| final var restClient = restClientBuilder | |
| .requestFactory(requestFactory) | |
| .build(); | |
| return new HttpClientResources(httpClient, connectionManager, restClient); | |
| } catch (Exception ex) { | |
| throw new IllegalStateException("Failed to create HTTP client resources", ex); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import java.time.Duration; | |
| public record HttpClientSettings( | |
| int maxConnTotal, | |
| int maxConnPerRoute, | |
| Duration connectTimeout, | |
| Duration connectionRequestTimeout, | |
| Duration readTimeout, | |
| Duration connectionTtl, | |
| Duration validateAfterInactivity, | |
| Duration idleEvictionTimeout, | |
| Duration cacheExpireAfterAccess | |
| ) { | |
| public static HttpClientSettings defaults() { | |
| return new HttpClientSettings( | |
| 20, | |
| 8, | |
| Duration.ofSeconds(3), | |
| Duration.ofSeconds(2), | |
| Duration.ofSeconds(45), | |
| Duration.ofMinutes(5), | |
| Duration.ofSeconds(5), | |
| Duration.ofMinutes(1), | |
| Duration.ofMinutes(30) | |
| ); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import org.apache.hc.client5.http.HttpRequestRetryStrategy; | |
| import org.apache.hc.core5.http.HttpRequest; | |
| import org.apache.hc.core5.http.HttpResponse; | |
| import org.apache.hc.core5.http.protocol.HttpContext; | |
| import org.apache.hc.core5.util.TimeValue; | |
| import java.io.IOException; | |
| import java.net.SocketException; | |
| import java.net.SocketTimeoutException; | |
| public final class TransientFailureRetryStrategy implements HttpRequestRetryStrategy { | |
| private static final int MAX_RETRIES = 1; | |
| private static final TimeValue RETRY_INTERVAL = TimeValue.ofSeconds(1); | |
| @Override | |
| public boolean retryRequest(HttpRequest request, IOException exception, int execCount, HttpContext context) { | |
| if (execCount > MAX_RETRIES) { | |
| return false; | |
| } | |
| return isTransientFailure(exception); | |
| } | |
| @Override | |
| public boolean retryRequest(HttpResponse response, int execCount, HttpContext context) { | |
| return false; | |
| } | |
| @Override | |
| public TimeValue getRetryInterval(HttpResponse response, int execCount, HttpContext context) { | |
| return RETRY_INTERVAL; | |
| } | |
| @Override | |
| public TimeValue getRetryInterval(HttpRequest request, IOException exception, int execCount, HttpContext context) { | |
| return RETRY_INTERVAL; | |
| } | |
| private boolean isTransientFailure(IOException ex) { | |
| return ex instanceof SocketTimeoutException || ex instanceof SocketException; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment