Java é um cavalo de batalha para coleta de dados em escala: clientes HTTP maduros, threads reais e ferramentas da JVM que tornam crawlers de longa duração observáveis. Conectar um proxy residencial a qualquer um dos dois clientes mais usados pelas equipes, OkHttp e Apache HttpClient, é simples. O atrito está nos detalhes que cada biblioteca trata de forma diferente: como a autenticação do proxy é fornecida, um padrão de pool de conexões que discretamente limita você, e a regra sobre consumir respostas que decide se o pooling funciona ou não.
Este é o capítulo Java da mesma série de residential proxies com Python, com Playwright e com Go: o código funcional para os dois clientes, além das armadilhas específicas do Java.
Tudo abaixo usa o gateway residencial da Shifter: um único endpoint, p.shifter.io:443, com toda a segmentação codificada no nome de usuário. Troque o host e as credenciais para usar outro provedor; a estrutura é a mesma.
O modelo de gateway em um parágrafo
O nome de usuário do proxy carrega sua autenticação e sua segmentação. Você não troca de endpoint para mudar de país ou sessão, você muda a string do nome de usuário:
customer-USERNAME-country-us-sid-abc123-ttl-600country-us segmenta para os EUA, sid fixa uma sessão persistente, ttl mantém esse IP por N segundos. Omita sid/ttl e cada nova conexão rotaciona. A senha é constante. Um detalhe que vale destacar de antemão: em ambos os clientes Java, as credenciais do proxy não são colocadas na URL do proxy, elas passam por um mecanismo de autenticação dedicado. Esse é o erro mais comum que as pessoas cometem.
OkHttp
O OkHttp recebe um objeto Proxy para o endereço e um proxyAuthenticator separado para as credenciais. Não tente codificar user:pass@ em uma URL; o OkHttp não vai lê-la.
import okhttp3.*;import java.net.InetSocketAddress;import java.net.Proxy;import java.io.IOException;
public class ProxyExample { public static void main(String[] args) throws IOException { String user = System.getenv("SHIFTER_USER") + "-country-us"; String pass = System.getenv("SHIFTER_PASS");
OkHttpClient client = new OkHttpClient.Builder() .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("p.shifter.io", 443))) .proxyAuthenticator((route, response) -> { // Chamado quando o proxy retorna 407. Anexa Proxy-Authorization. String credential = Credentials.basic(user, pass); return response.request().newBuilder() .header("Proxy-Authorization", credential) .build(); }) .build();
Request request = new Request.Builder().url("https://api.ipify.org").build(); try (Response response = client.newCall(request).execute()) { // try-with-resources fecha o body System.out.println(response.body().string()); // um IP residencial dos EUA } }}Duas coisas para internalizar. A string user inclui as flags de segmentação (-country-us), porque é ali que a geolocalização reside. E o try-with-resources em volta do Response não é um estilo opcional, ele fecha o corpo da resposta, o que é o que devolve a conexão ao pool.
Reutilize o client. O OkHttpClient foi projetado para ser criado uma vez e compartilhado; ele mantém o pool de conexões e o dispatcher de threads, e é thread-safe. Criar um por requisição descarta o pooling e vaza recursos. Para variar a identidade por requisição, derive uma variante com newBuilder(), que compartilha o pool e o dispatcher subjacentes:
// Um client base, compartilhado. Variantes por identidade reutilizam seu pool + dispatcher.OkHttpClient forGeo(OkHttpClient base, String country, String sid) { String user = System.getenv("SHIFTER_USER") + "-country-" + country + (sid != null ? "-sid-" + sid + "-ttl-600" : ""); String pass = System.getenv("SHIFTER_PASS"); return base.newBuilder() .proxyAuthenticator((route, resp) -> resp.request().newBuilder() .header("Proxy-Authorization", Credentials.basic(user, pass)) .build()) .build();}Dê a cada unidade lógica de trabalho seu próprio sid e rotacione entre unidades, não no meio de um fluxo (sticky vs rotating cobre essa distinção).
Apache HttpClient (5.x)
O Apache HttpClient fornece o proxy através da configuração da requisição ou de um planejador de rota, e as credenciais através de um CredentialsProvider restrito ao host do proxy.
import org.apache.hc.client5.http.classic.methods.HttpGet;import org.apache.hc.client5.http.impl.classic.*;import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;import org.apache.hc.client5.http.auth.*;import org.apache.hc.client5.http.config.RequestConfig;import org.apache.hc.core5.http.HttpHost;import org.apache.hc.core5.util.Timeout;
public class ApacheProxyExample { public static void main(String[] args) throws Exception { HttpHost proxy = new HttpHost("http", "p.shifter.io", 443); String user = System.getenv("SHIFTER_USER") + "-country-us"; char[] pass = System.getenv("SHIFTER_PASS").toCharArray();
BasicCredentialsProvider creds = new BasicCredentialsProvider(); creds.setCredentials(new AuthScope(proxy), new UsernamePasswordCredentials(user, pass));
// Pool: aumente por rota a partir do padrão de 5 (veja a armadilha abaixo). PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); cm.setMaxTotal(200); cm.setDefaultMaxPerRoute(50);
RequestConfig config = RequestConfig.custom() .setProxy(proxy) .setConnectTimeout(Timeout.ofSeconds(10)) // fase de conexão .setResponseTimeout(Timeout.ofSeconds(30)) // fase de resposta .build();
try (CloseableHttpClient client = HttpClients.custom() .setConnectionManager(cm) .setDefaultCredentialsProvider(creds) .setDefaultRequestConfig(config) .build()) {
HttpGet get = new HttpGet("https://api.ipify.org"); // try-with-resources na resposta consome + libera a conexão. try (var response = client.execute(get)) { System.out.println(new String(response.getEntity().getContent().readAllBytes())); } } }}Note os timeouts separados de conexão e resposta, essa divisão entre conexão e resposta é exatamente o que torna os timeouts diagnosticáveis quando algo trava.
Armadilha 1: o máximo por rota padrão do Apache é 5
Este é o equivalente em Java da armadilha do MaxIdleConnsPerHost do Go, e ela morde forte. O PoolingHttpClientConnectionManager tem como padrão 2 conexões por rota e 20 no total em builds mais antigos, e um limite baixo por rota no 5.x. Execute 50 threads contra um host e a maioria delas fica bloqueada esperando uma conexão liberar, o que parece exatamente um proxy lento.
Configure o pool para pelo menos sua concorrência por host:
cm.setMaxTotal(200);cm.setDefaultMaxPerRoute(50); // >= sua concorrência por hostSe o throughput do seu crawler estaciona não importa quantas threads você adicione, esse padrão é a primeira coisa a verificar.
Armadilha 2: consuma a entidade, ou a conexão nunca retorna
Ambos os clientes fazem pooling de conexões, e ambos só devolvem uma conexão ao pool quando a resposta é totalmente consumida e fechada. No Apache HttpClient, uma entidade não consumida mantém a conexão reservada; faça isso o suficiente e seu pool fica sem conexões mesmo sem nada tecnicamente vazando.
Use try-with-resources na resposta (como acima), ou consuma explicitamente:
import org.apache.hc.core5.http.io.entity.EntityUtils;// ...EntityUtils.consume(response.getEntity()); // drena + libera a conexãoA armadilha são as saídas antecipadas: abandonar um status diferente de 200 sem consumir a entidade prende a conexão. Em um scraper que enfrenta muitos bloqueios, isso é a maior parte do seu tráfego, e o pool morre silenciosamente.
Armadilha 3: reutilize o client, sempre
Tanto OkHttpClient quanto CloseableHttpClient são pesados, thread-safe, e feitos para serem compartilhados em toda a aplicação. Eles possuem o pool de conexões, e um novo a cada requisição paga um handshake completo de TCP + TLS através do proxy toda vez, a sobrecarga que o guia de latência existe para eliminar. Construa um na inicialização, injete-o, e derive variantes com escopo de requisição apenas quando precisar variar a identidade.
Rotação de geo e sessões
Como a segmentação reside no nome de usuário, uma identidade diferente é uma string de credencial diferente.
OkHttp: derive um client com newBuilder() por identidade (mostrado acima); ele reutiliza o pool compartilhado, então permanece eficiente.
Apache HttpClient: anexe um HttpClientContext por requisição carregando um CredentialsProvider para aquela identidade, de modo que um client sirva muitas identidades sem ser reconstruído:
HttpClientContext ctx = HttpClientContext.create();BasicCredentialsProvider perCall = new BasicCredentialsProvider();perCall.setCredentials(new AuthScope(proxy), new UsernamePasswordCredentials(userFor("de", "job-42"), pass));ctx.setCredentialsProvider(perCall);client.execute(get, ctx, resp -> { /* handle */ return null; });Rotacione entre unidades lógicas de trabalho em vez de dentro de uma só, e mapeie o trabalho para identidades da forma que o post sobre load balancing descreve.
Concorrência, por host
Limite a concorrência por host de destino, não com um único limite global, para que um alvo frágil não seja sobrecarregado e um permissivo não fique subutilizado. Um Semaphore por host é a expressão mais simples:
Map<String, Semaphore> limits = Map.of( "tough-site.example", new Semaphore(4), "open-site.example", new Semaphore(32));
void fetch(String host, Runnable work) throws InterruptedException { Semaphore sem = limits.get(host); sem.acquire(); try { work.run(); } finally { sem.release(); }}Além da tolerância de um alvo, mais threads compram bloqueios, não throughput (como evitar ser bloqueado). Dimensione o limite por rota do pool e seu semáforo por host em conjunto.
Verifique se você realmente está usando o proxy
Antes de fazer benchmark ou depurar qualquer outra coisa, confirme o IP de saída:
Request req = new Request.Builder().url("http://ip-api.com/json").build();try (Response r = client.newCall(req).execute()) { System.out.println(r.body().string()); // espera-se um IP residencial no país segmentado}Ver seu próprio IP significa que o client não está usando o proxy. Um travamento significa que a saída local está bloqueada. Ambos são cobertos no guia de diagnóstico de timeout.
Perguntas frequentes
Por que http://user:pass@host não funciona para o proxy em Java?
Nem o OkHttp nem o Apache HttpClient leem credenciais de proxy embutidas em uma URL. O OkHttp usa um proxyAuthenticator que define Proxy-Authorization; o Apache usa um CredentialsProvider restrito ao host do proxy. Como o gateway codifica a segmentação no nome de usuário, esse nome de usuário vai para o authenticator/credentials, não para uma URL.
Meu scraper Java trava sob carga mesmo com um único client. Por quê?
O mais provável é que o limite por rota do pool do Apache (baixo por padrão) esteja limitando você, ou que você não esteja consumindo as entidades de resposta, fazendo com que as conexões nunca retornem ao pool. Aumente o setDefaultMaxPerRoute para sua concorrência e consuma toda resposta com try-with-resources.
Como rotaciono IPs por requisição em Java?
Varie o nome de usuário do proxy. No OkHttp, derive um client por identidade com newBuilder() (ele compartilha o pool). No Apache HttpClient, passe um HttpClientContext por requisição com um CredentialsProvider para aquela identidade. Ambos permitem que um único client compartilhado sirva muitas identidades.
Devo configurar os timeouts de conexão e resposta separadamente? Sim. Um timeout de conexão separado do timeout de resposta/socket permite distinguir uma conexão lenta (do seu lado ou sem IP correspondente) de uma resposta lenta (do alvo), o que é a distinção chave ao diagnosticar timeouts. Um único timeout genérico esconde qual fase falhou.
OkHttp ou Apache HttpClient para scraping? Ambos funcionam bem. O OkHttp é mais leve e tem uma API mais limpa; o Apache HttpClient é mais configurável e consolidado há mais tempo. Escolha com base na ergonomia e nas dependências existentes; a configuração do proxy e as armadilhas acima se aplicam a ambos.
Conclusão
Java com proxies residenciais funciona bem uma vez que você respeita as regras de cada client: forneça as credenciais do proxy através do mecanismo adequado (o proxyAuthenticator do OkHttp, o CredentialsProvider do Apache), nunca embutidas em uma URL; compartilhe um único client de longa duração e derive dele variantes de identidade; aumente o limite do pool por rota do Apache para corresponder à sua concorrência; sempre consuma e feche as respostas para que as conexões retornem ao pool; e separe os timeouts de conexão e resposta. Varie o nome de usuário do proxy para mudar a geo ou a sessão, e limite a concorrência por host.
Acerte esses pontos e ambos os clientes terão o desempenho que a JVM deveria oferecer. Aponte-os para o gateway residencial, e lembre-se de que a qualidade do pool decide com que frequência você está tentando novamente (ou não) (reputação de IP). A página de preços tem os planos por GB para testar contra seus próprios alvos.