Skip to content

Instantly share code, notes, and snippets.

@robsonkades
Created March 17, 2026 02:02
Show Gist options
  • Select an option

  • Save robsonkades/b70254c00b729d772d1608983bf415ce to your computer and use it in GitHub Desktop.

Select an option

Save robsonkades/b70254c00b729d772d1608983bf415ce to your computer and use it in GitHub Desktop.
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;
}
}
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);
}
}
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) {
}
}
}
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);
}
}
}
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)
);
}
}
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