Java est un cheval de trait pour la collecte de données à l’échelle : des clients HTTP mûrs, de vrais threads, et un outillage JVM qui rend observables les crawlers de longue durée. Brancher un proxy résidentiel dans l’un des deux clients que la plupart des équipes utilisent, OkHttp et Apache HttpClient, est simple. La friction est dans les détails que chaque bibliothèque gère différemment : comment l’authentification du proxy est fournie, un défaut du pool de connexions qui vous bride silencieusement, et la règle sur la consommation des réponses qui décide si le pooling fonctionne tout court.
Ceci est l’entrée Java de la même série que proxys résidentiels avec Python, avec Playwright, et avec Go : le code qui marche pour les deux clients, plus les pièges spécifiques à Java.
Tout ci-dessous utilise le gateway résidentiel de Shifter : un point de terminaison, p.shifter.io:443, avec tout le ciblage encodé dans le nom d’utilisateur. Changez l’hôte et les identifiants pour un autre fournisseur ; la forme est la même.
Le modèle du gateway en un paragraphe
Le nom d’utilisateur du proxy porte votre authentification et votre ciblage. Vous ne changez pas de point de terminaison pour changer de pays ou de session, vous changez la chaîne du nom d’utilisateur :
customer-USERNAME-country-us-sid-abc123-ttl-600country-us cible les États-Unis, sid fixe une session sticky, ttl maintient cette IP pendant N secondes. Omettez sid/ttl et chaque nouvelle connexion tourne. Le mot de passe est constant. Une subtilité à signaler d’emblée : dans les deux clients Java, les identifiants du proxy ne se mettent pas dans l’URL du proxy, ils passent par un mécanisme d’authentification dédié. C’est ce que les gens se trompent le plus souvent.
OkHttp
OkHttp prend un objet Proxy pour l’adresse et un proxyAuthenticator séparé pour les identifiants. N’essayez pas d’encoder user:pass@ dans une URL ; OkHttp ne le lira pas.
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) -> { // Appelé quand le proxy renvoie 407. Attacher 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 ferme le body System.out.println(response.body().string()); // une IP résidentielle américaine } }}Deux choses à intérioriser. La chaîne user inclut les flags de ciblage (-country-us), parce que c’est là que vit la géo. Et le try-with-resources autour de la Response n’est pas un style optionnel, il ferme le body de la réponse, ce qui rend la connexion au pool.
Réutilisez le client. OkHttpClient est conçu pour être créé une fois et partagé ; il détient le pool de connexions et le dispatcher de threads, et il est thread-safe. En créer un par requête jette le pooling et fuit des ressources. Pour varier l’identité par requête, dérivez une variante avec newBuilder(), qui partage le pool et le dispatcher sous-jacents :
// Un client de base, partagé. Les variantes par identité réutilisent son 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();}Donnez à chaque unité logique de travail son propre sid et tournez entre unités, pas en milieu de flux (sticky vs rotatif couvre la distinction).
Apache HttpClient (5.x)
Apache HttpClient fournit le proxy via la request config ou un route planner, et les identifiants via un CredentialsProvider limité à l’hôte du 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 : montez le par-route depuis le défaut de 5 (voir le piège ci-dessous). PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); cm.setMaxTotal(200); cm.setDefaultMaxPerRoute(50);
RequestConfig config = RequestConfig.custom() .setProxy(proxy) .setConnectTimeout(Timeout.ofSeconds(10)) // phase de connexion .setResponseTimeout(Timeout.ofSeconds(30)) // phase de réponse .build();
try (CloseableHttpClient client = HttpClients.custom() .setConnectionManager(cm) .setDefaultCredentialsProvider(creds) .setDefaultRequestConfig(config) .build()) {
HttpGet get = new HttpGet("https://api.ipify.org"); // try-with-resources sur la réponse consomme + libère la connexion. try (var response = client.execute(get)) { System.out.println(new String(response.getEntity().getContent().readAllBytes())); } } }}Notez les timeouts de connexion et de réponse séparés, cette séparation connexion-versus-réponse est exactement ce qui rend les timeouts diagnosticables quand quelque chose bloque.
Piège 1 : le max-par-route par défaut d’Apache est 5
C’est l’équivalent Java du piège MaxIdleConnsPerHost de Go, et il mord fort. PoolingHttpClientConnectionManager a par défaut 2 connexions par route et 20 au total dans les anciens builds, et un cap par-route bas en 5.x. Lancez 50 threads contre un hôte et la plupart bloquent en attendant qu’une connexion se libère, ce qui ressemble exactement à un proxy lent.
Réglez le pool au moins à votre concurrence par hôte :
cm.setMaxTotal(200);cm.setDefaultMaxPerRoute(50); // >= votre concurrence par hôteSi le débit de votre crawler plafonne quel que soit le nombre de threads que vous ajoutez, ce défaut est la première chose à vérifier.
Piège 2 : consommez l’entity, ou la connexion ne revient jamais
Les deux clients poolent les connexions, et les deux ne rendent une connexion au pool que lorsque la réponse est entièrement consommée et fermée. Dans Apache HttpClient, une entity non consommée garde la connexion sortie ; faites-en assez et votre pool s’affame même si rien ne fuit techniquement.
Utilisez try-with-resources sur la réponse (comme ci-dessus), ou consommez explicitement :
import org.apache.hc.core5.http.io.entity.EntityUtils;// ...EntityUtils.consume(response.getEntity()); // draine + libère la connexionLe piège, ce sont les sorties anticipées : abandonner sur un statut non-200 sans consommer l’entity échoue la connexion. Sur un scraper qui rencontre beaucoup de blocages, c’est l’essentiel de votre trafic, et le pool meurt silencieusement.
Piège 3 : réutilisez le client, toujours
OkHttpClient et CloseableHttpClient sont tous deux lourds, thread-safe, et destinés à être partagés dans toute l’application. Ils possèdent le pool de connexions, et un frais par requête paie un handshake TCP + TLS complet à travers le proxy à chaque fois, le surcoût que le guide de latence existe pour éliminer. Construisez-en un au démarrage, injectez-le, et ne dérivez des variantes à portée de requête que lorsque vous devez varier l’identité.
Faire tourner géo et sessions
Parce que le ciblage vit dans le nom d’utilisateur, une identité différente est une chaîne d’identifiants différente.
OkHttp : dérivez un client avec newBuilder() par identité (montré ci-dessus) ; il réutilise le pool partagé, donc ça reste efficace.
Apache HttpClient : attachez un HttpClientContext par requête portant un CredentialsProvider pour cette identité, pour qu’un client serve plusieurs identités sans reconstruire :
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 -> { /* traiter */ return null; });Tournez entre unités logiques de travail plutôt qu’au sein d’une, et mappez le travail aux identités comme le décrit le billet sur la répartition de charge.
Concurrence, par hôte
Bornez la concurrence par hôte cible, pas avec un cap global, pour qu’une cible fragile ne soit pas martelée et qu’une permissive ne soit pas affamée. Un Semaphore par hôte est l’expression la plus simple :
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(); }}Au-delà de la tolérance d’une cible, plus de threads achètent des blocages, pas du débit (comment éviter de se faire bloquer). Dimensionnez la limite par-route du pool et votre sémaphore par hôte ensemble.
Vérifiez que vous êtes bien sur le proxy
Avant de benchmarker ou de déboguer quoi que ce soit d’autre, confirmez l’IP de sortie :
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()); // attendez une IP résidentielle dans le pays ciblé}Votre propre IP signifie que le client n’utilise pas le proxy. Un blocage signifie que la sortie locale est bloquée. Les deux sont couverts dans le guide de diagnostic des timeouts.
FAQ
Pourquoi http://user:pass@host ne marche-t-il pas pour le proxy en Java ?
Ni OkHttp ni Apache HttpClient ne lit les identifiants de proxy en ligne depuis une URL. OkHttp utilise un proxyAuthenticator qui pose Proxy-Authorization ; Apache utilise un CredentialsProvider limité à l’hôte du proxy. Comme le gateway encode le ciblage dans le nom d’utilisateur, ce nom d’utilisateur va dans l’authenticator/les identifiants, pas dans une URL.
Mon scraper Java cale sous charge même avec un seul client. Pourquoi ?
Le plus probable : la limite par-route du pool Apache (basse par défaut) vous bride, ou vous ne consommez pas les entities de réponse donc les connexions ne reviennent jamais au pool. Montez setDefaultMaxPerRoute à votre concurrence et consommez chaque réponse avec try-with-resources.
Comment faire tourner les IP par requête en Java ?
Variez le nom d’utilisateur du proxy. En OkHttp, dérivez un client par identité avec newBuilder() (il partage le pool). En Apache HttpClient, passez un HttpClientContext par requête avec un CredentialsProvider pour cette identité. Les deux laissent un client partagé servir plusieurs identités.
Devrais-je poser les timeouts de connexion et de réponse séparément ? Oui. Un timeout de connexion séparé et un timeout de réponse/socket vous laissent distinguer une connexion lente (votre côté ou aucune IP correspondante) d’une réponse lente (la cible), la distinction clé pour diagnostiquer les timeouts. Un seul timeout grossier cache quelle phase a échoué.
OkHttp ou Apache HttpClient pour le scraping ? Les deux marchent bien. OkHttp est plus léger et a une API plus propre ; Apache HttpClient est plus configurable et de longue date. Choisissez selon l’ergonomie et les dépendances existantes ; la configuration du proxy et les pièges ci-dessus s’appliquent aux deux.
En résumé
Java plus proxys résidentiels est solide dès lors que vous respectez les règles de chaque client : fournissez les identifiants du proxy via le bon mécanisme (le proxyAuthenticator d’OkHttp, le CredentialsProvider d’Apache), jamais en ligne dans une URL ; partagez un seul client durable et dérivez-en des variantes d’identité ; montez la limite du pool par-route d’Apache au niveau de votre concurrence ; consommez et fermez toujours les réponses pour que les connexions reviennent au pool ; et séparez les timeouts de connexion et de réponse. Variez le nom d’utilisateur du proxy pour changer géo ou session, et bornez la concurrence par hôte.
Réussissez cela et les deux clients performent comme la JVM devrait. Pointez-les vers le gateway résidentiel, et souvenez-vous que la qualité du pool décide à quelle fréquence vous réessayez tout court (réputation d’IP). La page tarifs propose les forfaits au Go pour le tester contre vos propres cibles.