Created
October 6, 2025 01:16
-
-
Save robsonkades/0dcbe88f295a272c2993e7dbc8a8c1c0 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 org.springframework.web.client.RestClient; | |
| public record WebServiceClient(RestClient restClient, CloseableResources closeableResources) { | |
| } | |
| import org.springframework.http.ResponseEntity; | |
| import java.util.concurrent.CompletableFuture; | |
| public interface WebService { | |
| CompletableFuture<ResponseEntity<String>> post(String url, String body); | |
| CompletableFuture<ResponseEntity<String>> get(String uri); | |
| } | |
| import io.github.robsonkades.sefaz.ssl.RegisterBundle; | |
| import org.apache.hc.client5.http.config.RequestConfig; | |
| import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; | |
| import org.apache.hc.client5.http.impl.classic.HttpClients; | |
| import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; | |
| 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.pool.PoolConcurrencyPolicy; | |
| import org.apache.hc.core5.pool.PoolReusePolicy; | |
| import org.apache.hc.core5.reactor.ssl.SSLBufferMode; | |
| import org.apache.hc.core5.util.Timeout; | |
| import org.springframework.boot.ssl.SslBundle; | |
| import org.springframework.boot.ssl.SslBundles; | |
| import org.springframework.cache.annotation.Cacheable; | |
| import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; | |
| import org.springframework.stereotype.Component; | |
| import org.springframework.web.client.RestClient; | |
| import java.time.Duration; | |
| import java.util.function.Consumer; | |
| /** | |
| * Fábrica responsável por criar e configurar instâncias de {@link RestClient} | |
| * e seus recursos subjacentes (HttpClient e ConnectionManager) com suporte a TLS, | |
| * pooling de conexões, timeouts e integração com {@link SslBundles} do Spring Boot. | |
| * | |
| * <p>Principais características:</p> | |
| * <ul> | |
| * <li>Configuração explícita de timeouts de socket, conexão e leitura.</li> | |
| * <li>Gerenciamento de conexões com {@link PoolingHttpClientConnectionManager}. | |
| * Define limites de conexões totais e por rota, além de política de | |
| * expurgo de conexões ociosas.</li> | |
| * <li>Suporte a TLS 1.2 e 1.3 a partir de um {@link SslBundle} nomeado.</li> | |
| * <li>Criação de {@link WebServiceClient} contendo o {@link RestClient} | |
| * e os recursos "fecháveis" para encerramento adequado.</li> | |
| * <li>Resultado de criação cacheado por nome do bundle SSL via {@link Cacheable}.</li> | |
| * </ul> | |
| * | |
| * <p>Thread-safety: Esta classe é um {@link Component} Spring com dependências imutáveis e | |
| * não mantém estado mutável; pode ser utilizada de forma segura entre múltiplas threads.</p> | |
| */ | |
| @Component | |
| public class RestClientFactory { | |
| /** | |
| * Timeout de socket (inatividade na camada de leitura/escrita) utilizado pelo {@link SocketConfig}. | |
| * Valor padrão: 60 segundos. | |
| */ | |
| public static final Timeout SO_TIMEOUT = Timeout.ofSeconds(60); | |
| /** | |
| * Timeout máximo aguardando uma conexão do pool para o {@link RequestConfig}. | |
| * Valor padrão: 10 segundos. | |
| */ | |
| public static final Timeout REQUEST_CONFIG_CONNECTION_REQUEST_TIMEOUT = Timeout.ofSeconds(10); | |
| /** | |
| * Timeout de resposta do request no {@link RequestConfig} (tempo total aguardando a resposta). | |
| * Valor padrão: 60 segundos. | |
| */ | |
| public static final Timeout REQUEST_CONFIG_RESPONSE_TIMEOUT = Timeout.ofSeconds(60); | |
| /** | |
| * Timeout de conexão (handshake TCP) aplicado na request factory do Spring. | |
| * Valor padrão: 5 segundos. | |
| */ | |
| private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5); | |
| /** | |
| * Timeout para aguardar uma conexão disponível no pool aplicado na request factory do Spring. | |
| * Valor padrão: 10 segundos. | |
| */ | |
| private static final Duration CONNECTION_REQUEST_TIMEOUT = Duration.ofSeconds(10); | |
| /** | |
| * Timeout de leitura (entre bytes) aplicado na request factory do Spring. | |
| * Valor padrão: 60 segundos. | |
| */ | |
| private static final Duration READ_TIMEOUT = Duration.ofSeconds(60); | |
| /** | |
| * Número máximo de conexões totais no pool do HttpClient. | |
| * Valor padrão: 100. | |
| */ | |
| public static final int MAX_CONN_TOTAL = 20; | |
| /** | |
| * Número máximo de conexões por rota (host) no pool do HttpClient. | |
| * Valor padrão: 20. | |
| */ | |
| public static final int MAX_CONN_PER_ROUTE = 5; | |
| /** | |
| * Configuração de socket padrão utilizada pelo pool de conexões. | |
| * Ativa TCP no-delay, mantém keep-alive e reuso de endereço, além do {@link #SO_TIMEOUT}. | |
| */ | |
| public static final SocketConfig SOCKET_CONFIG = SocketConfig.custom() | |
| .setTcpNoDelay(true) | |
| .setSoTimeout(SO_TIMEOUT) | |
| .setSoReuseAddress(true) | |
| .setSoKeepAlive(true) | |
| .build(); | |
| /** | |
| * Configuração de request padrão aplicada ao {@link CloseableHttpClient}. | |
| * Inclui timeouts de espera por conexão do pool e de resposta. | |
| */ | |
| public static final RequestConfig REQUEST_CONFIG = RequestConfig.custom() | |
| .setConnectionRequestTimeout(REQUEST_CONFIG_CONNECTION_REQUEST_TIMEOUT) | |
| .setResponseTimeout(REQUEST_CONFIG_RESPONSE_TIMEOUT) | |
| .build(); | |
| /** Builder do Spring para criação de {@link RestClient}. */ | |
| private final RestClient.Builder restClientBuilder; | |
| /** Registro de bundles SSL configurados na aplicação. */ | |
| private final RegisterBundle registerBundle; | |
| /** | |
| * Cria a fábrica a partir do builder de {@link RestClient} e do registro de {@link SslBundles}. | |
| * | |
| * @param restClientBuilder builder base para configuração do RestClient | |
| * @param registerBundle registro de bundles SSL (certificados/chaves) nomeados | |
| */ | |
| public RestClientFactory(RestClient.Builder restClientBuilder, RegisterBundle registerBundle, SslBundles sslBundles) { | |
| this.restClientBuilder = restClientBuilder; | |
| this.registerBundle = registerBundle; | |
| } | |
| /** | |
| * Cria um {@link WebServiceClient} configurado para um bundle SSL específico. | |
| * O resultado é armazenado em cache com a chave igual ao nome do bundle, evitando recriação | |
| * de recursos dispendiosos como o pool de conexões e o HttpClient. | |
| * | |
| * @param bundleName nome do {@link SslBundle} a ser utilizado (deve existir em {@link SslBundles}) | |
| * @return instância de {@link WebServiceClient} com {@link RestClient} e os recursos fecháveis | |
| */ | |
| @Cacheable(value = "sefaz", key = "#bundleName") | |
| public WebServiceClient create(String bundleName) { | |
| CloseableResources closeableResources = closeableResources(bundleName); | |
| RestClient restClient = restClientBuilder | |
| .apply(fromHttpClient(closeableResources.httpClient())) | |
| .build(); | |
| return new WebServiceClient(restClient, closeableResources); | |
| } | |
| /** | |
| * Adapta um {@link CloseableHttpClient} para o {@link RestClient.Builder} do Spring, | |
| * configurando a {@link HttpComponentsClientHttpRequestFactory} com os timeouts | |
| * {@link #CONNECT_TIMEOUT}, {@link #CONNECTION_REQUEST_TIMEOUT} e {@link #READ_TIMEOUT}. | |
| * | |
| * @param httpClient cliente HTTP já configurado | |
| * @return função que aplica a request factory ao builder do RestClient | |
| */ | |
| public Consumer<RestClient.Builder> fromHttpClient(CloseableHttpClient httpClient) { | |
| return (builder) -> { | |
| HttpComponentsClientHttpRequestFactory rf = new HttpComponentsClientHttpRequestFactory(httpClient); | |
| rf.setConnectTimeout(CONNECT_TIMEOUT); | |
| rf.setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT); | |
| rf.setReadTimeout(READ_TIMEOUT); | |
| builder.requestFactory(rf); | |
| }; | |
| } | |
| /** | |
| * Cria os recursos fecháveis necessários para execução de chamadas HTTP com TLS. | |
| * Isso inclui: | |
| * <ul> | |
| * <li>Construção de uma estratégia TLS utilizando o {@link SslBundle} informado | |
| * (protocolos suportados: TLSv1.2 e TLSv1.3, verificação de host desabilitada via {@link NoopHostnameVerifier}).</li> | |
| * <li>Criação de um {@link PoolingHttpClientConnectionManager} com {@link #SOCKET_CONFIG}, | |
| * limites de conexões ({@link #MAX_CONN_TOTAL}, {@link #MAX_CONN_PER_ROUTE}) e estratégia TLS.</li> | |
| * <li>Criação de um {@link CloseableHttpClient} que usa o connection manager, | |
| * aplica {@link #REQUEST_CONFIG}.</li> | |
| * </ul> | |
| * | |
| * @param bundleName nome do {@link SslBundle} carregado a partir de {@link SslBundles} | |
| * @return contêiner com o {@link CloseableHttpClient} e o {@link PoolingHttpClientConnectionManager} | |
| * para permitir fechamento explícito quando não forem mais necessários | |
| * @throws IllegalArgumentException se o bundle não existir em {@link SslBundles} | |
| */ | |
| public CloseableResources closeableResources(String bundleName) { | |
| SslBundle bundle = registerBundle.getSslBundle(bundleName); | |
| DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy( | |
| bundle.createSslContext(), | |
| new String[]{"TLSv1.2", "TLSv1.3"}, | |
| null, | |
| SSLBufferMode.DYNAMIC, | |
| NoopHostnameVerifier.INSTANCE); | |
| PoolingHttpClientConnectionManager connectionManager = | |
| PoolingHttpClientConnectionManagerBuilder.create() | |
| .setTlsSocketStrategy(tlsStrategy) | |
| .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT) | |
| .setConnPoolPolicy(PoolReusePolicy.FIFO) | |
| .setDefaultSocketConfig(SOCKET_CONFIG) | |
| .setMaxConnTotal(MAX_CONN_TOTAL) | |
| .setMaxConnPerRoute(MAX_CONN_PER_ROUTE) | |
| .build(); | |
| CloseableHttpClient httpClient = HttpClients.custom() | |
| .setConnectionManager(connectionManager) | |
| .setDefaultRequestConfig(REQUEST_CONFIG) | |
| .setConnectionManagerShared(true) | |
| .build(); | |
| return new CloseableResources(httpClient, connectionManager); | |
| } | |
| } | |
| import org.springframework.stereotype.Component; | |
| @Component | |
| public class ConsultaProtocolo extends AbstractWebService { | |
| protected ConsultaProtocolo(RestClientFactory restClientBuilder) { | |
| super(restClientBuilder); | |
| } | |
| @Override | |
| protected String resolverBundleName() { | |
| return "test"; | |
| } | |
| } | |
| import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; | |
| import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; | |
| import org.slf4j.Logger; | |
| import org.slf4j.LoggerFactory; | |
| import java.io.Closeable; | |
| import java.io.IOException; | |
| public record CloseableResources(CloseableHttpClient httpClient, PoolingHttpClientConnectionManager connectionManager) implements Closeable { | |
| private static final Logger log = LoggerFactory.getLogger(CloseableResources.class); | |
| @Override | |
| public void close() throws IOException { | |
| if (httpClient != null) { | |
| log.debug("Closing http client"); | |
| httpClient.close(); | |
| } | |
| if (connectionManager != null) { | |
| log.debug("Closing connection manager"); | |
| connectionManager.close(); | |
| } | |
| } | |
| } | |
| import org.springframework.http.MediaType; | |
| import org.springframework.http.ResponseEntity; | |
| import org.springframework.web.client.RestClient; | |
| import java.util.concurrent.CompletableFuture; | |
| import java.util.concurrent.ExecutorService; | |
| import java.util.concurrent.Executors; | |
| import java.util.regex.Matcher; | |
| import java.util.regex.Pattern; | |
| public abstract class AbstractWebService implements WebService { | |
| private static final ExecutorService VIRTUAL_THREAD_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor(); | |
| private static final Pattern SOAP_BODY_PATTERN = Pattern.compile( | |
| "<(?:\\w+:)?Body[^>]*>(.*?)</(?:\\w+:)?Body>", | |
| Pattern.DOTALL | Pattern.CASE_INSENSITIVE | |
| ); | |
| private final RestClientFactory restClientBuilder; | |
| protected AbstractWebService(RestClientFactory restClientBuilder) { | |
| this.restClientBuilder = restClientBuilder; | |
| } | |
| public static String wrapWithSoapEnvelope(String xmlBody) { | |
| return """ | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> | |
| <soap:Body> | |
| %s | |
| </soap:Body> | |
| </soap:Envelope>""".formatted(xmlBody.trim()); | |
| } | |
| public static String unwrapSoapEnvelope(String soapResponse) { | |
| if (soapResponse == null || soapResponse.isBlank()) return ""; | |
| Matcher matcher = SOAP_BODY_PATTERN.matcher(soapResponse); | |
| if (matcher.find()) { | |
| return matcher.group(1).trim(); | |
| } | |
| return soapResponse.trim(); | |
| } | |
| @Override | |
| public CompletableFuture<ResponseEntity<String>> post(String uri, String body) { | |
| return CompletableFuture.supplyAsync(() -> { | |
| WebServiceClient webServiceClient = restClientBuilder.create(resolverBundleName()); | |
| return webServiceClient.restClient() | |
| .post() | |
| .uri(uri) | |
| .body(body) | |
| .contentType(MediaType.parseMediaType("application/soap+xml")) | |
| .accept(MediaType.parseMediaType("application/soap+xml")) | |
| .retrieve() | |
| .toEntity(String.class); | |
| }, VIRTUAL_THREAD_EXECUTOR); | |
| } | |
| @Override | |
| public CompletableFuture<ResponseEntity<String>> get(String uri) { | |
| RestClient restClient = restClientBuilder.create(resolverBundleName()).restClient(); | |
| return CompletableFuture.supplyAsync(() -> restClient | |
| .get() | |
| .uri(uri) | |
| .retrieve() | |
| .toEntity(String.class), VIRTUAL_THREAD_EXECUTOR); | |
| } | |
| public void test() { | |
| restClientBuilder.create(resolverBundleName()).restClient(); | |
| } | |
| protected abstract String resolverBundleName(); | |
| } | |
| import com.github.benmanes.caffeine.cache.Caffeine; | |
| import io.github.robsonkades.sefaz.newwb.WebServiceClient; | |
| import org.slf4j.Logger; | |
| import org.slf4j.LoggerFactory; | |
| import org.springframework.cache.CacheManager; | |
| import org.springframework.cache.annotation.EnableCaching; | |
| import org.springframework.cache.caffeine.CaffeineCacheManager; | |
| import org.springframework.context.annotation.Bean; | |
| import org.springframework.context.annotation.Configuration; | |
| import java.io.IOException; | |
| import java.time.Duration; | |
| @Configuration | |
| @EnableCaching | |
| public class CacheConfig { | |
| private final Logger logger = LoggerFactory.getLogger(this.getClass()); | |
| @Bean | |
| public CacheManager cacheManager() { | |
| CaffeineCacheManager cacheManager = new CaffeineCacheManager(); | |
| cacheManager.registerCustomCache("sefaz", | |
| Caffeine.newBuilder() | |
| .recordStats() | |
| .maximumSize(1000) | |
| .softValues() | |
| .expireAfterWrite(Duration.ofMinutes(5)) | |
| .evictionListener((key, value, cause) -> { | |
| logger.info("Eviction of cache {} has been cleared", key); | |
| }) | |
| .removalListener((key, value, cause) -> { | |
| String safeValue = (value != null ? value.getClass().getName() : "null"); | |
| logger.info("Removed key: {}, valueType: {}, cause: {}", key, safeValue, cause); | |
| if (value instanceof WebServiceClient webServiceClient) { | |
| try { | |
| webServiceClient.closeableResources().close(); | |
| } catch (IOException ignored) { | |
| } | |
| } | |
| }).build()); | |
| // --- Cache 2: Para outra funcionalidade (ex: dados de usuários) --- | |
| // Política: Tamanho máximo de 500, expira 10 minutos após a escrita (bom para dados que mudam com frequência). | |
| String usuariosSpec = "maximumSize=500,expireAfterWrite=600s"; | |
| Caffeine<Object, Object> keystore = Caffeine.from(usuariosSpec); | |
| cacheManager.registerCustomCache("keystore", keystore.build()); | |
| // --- Cache 3: Um cache com configuração padrão do Caffeine --- | |
| // Se você não especificar uma spec, ele usará as configurações default. | |
| // É útil ter um cache genérico. | |
| cacheManager.registerCustomCache("truststore", Caffeine.newBuilder().build()); | |
| // Opcional: Se você quiser que o CacheManager conheça os nomes dos caches | |
| // de antemão, você pode usar setCacheNames. Isso pode ser útil para algumas | |
| // implementações, mas registerCustomCache já é suficiente. | |
| // cacheManager.setCacheNames(List.of("sefazClients", "dadosDeUsuarios", "cacheGenerico")); | |
| return cacheManager; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment