Java HTTP Proxy: Kompletny przewodnik po HttpClient, OkHttp i Jsoup

Praktyczny przewodnik po używaniu proxy HTTP w Java 17+. Przykłady kodu dla HttpClient, OkHttp, Jsoup i Apache HttpClient z autoryzacją, pulami połączeń i równoległym scrapowaniem.

Java HTTP Proxy: Kompletny przewodnik po HttpClient, OkHttp i Jsoup

Wprowadzenie: dlaczego Java HTTP proxy ma znaczenie

Każdy programista Java, który buduje aplikacje wymagające zewnętrznych żądań HTTP — od web scrapingu po integracje API — wcześniej czy później napotyka potrzebę użycia proxy. Java HTTP proxy to nie tylko kwestia ukrywania adresu IP. To fundament dla: geolokalizacji żądań, omijania limitów rate-limiting, testowania aplikacji z różnych lokalizacji i budowania odpornych systemów scrapowania.

Ekosystem Java oferuje kilka podejść do obsługi proxy: od natywnego HttpClient (Java 11+), przez popularny OkHttp, po biblioteki parsingowe jak Jsoup. Każda z nich ma swoją specyfikę konfiguracji — i każda wymaga innego podejścia do autoryzacji, pulowania połączeń czy obsługi TLS.

W tym przewodniku pokażę konkretne, uruchamialne przykłady kodu dla wszystkich głównych bibliotek HTTP w Java 17+. Skupię się na wzorcach produkcyjnych: retry logic, connection pooling, timeouty i równoległe scrapowanie przez pulę residential proxy.

Java 11+ HttpClient z ProxySelector

HttpClient wprowadzony w Java 11 to nowoczesny, asynchroniczny klient HTTP wbudowany w JDK. Obsługa proxy odbywa się przez ProxySelector — mechanizm, który pozwala dynamicznie wybierać proxy dla każdego żądania.

Podstawowa konfiguracja proxy

Najprostszy sposób to użycie systemowego ProxySelector lub utworzenie własnego:

import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class HttpClientProxyExample {
    
    private static final String PROXY_HOST = "gate.proxyhat.com";
    private static final int PROXY_PORT = 8080;
    
    public static void main(String[] args) throws Exception {
        // Tworzymy ProxySelector dla konkretnego proxy
        ProxySelector proxySelector = ProxySelector.of(
            new InetSocketAddress(PROXY_HOST, PROXY_PORT)
        );
        
        HttpClient client = HttpClient.newBuilder()
            .proxy(proxySelector)
            .connectTimeout(Duration.ofSeconds(30))
            .followRedirects(HttpClient.Redirect.NORMAL)
            .build();
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://httpbin.org/ip"))
            .timeout(Duration.ofSeconds(60))
            .GET()
            .build();
        
        HttpResponse<String> response = client.send(
            request, 
            HttpResponse.BodyHandlers.ofString()
        );
        
        System.out.println("Status: " + response.statusCode());
        System.out.println("Body: " + response.body());
    }
}

Ten przykład działa dla proxy bez autoryzacji. Dla proxy z autoryzacją musimy dodać Authenticator:

import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class HttpClientAuthenticatedProxy {
    
    private static final String PROXY_HOST = "gate.proxyhat.com";
    private static final int PROXY_PORT = 8080;
    private static final String USERNAME = "user-country-US"; // Geo-targeting w nazwie użytkownika
    private static final String PASSWORD = "your_password";
    
    public static void main(String[] args) throws Exception {
        // ProxySelector
        ProxySelector proxySelector = ProxySelector.of(
            new InetSocketAddress(PROXY_HOST, PROXY_PORT)
        );
        
        // Authenticator dla proxy
        Authenticator proxyAuthenticator = new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(USERNAME, PASSWORD.toCharArray());
            }
        };
        
        HttpClient client = HttpClient.newBuilder()
            .proxy(proxySelector)
            .authenticator(proxyAuthenticator)
            .connectTimeout(Duration.ofSeconds(30))
            .build();
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://httpbin.org/ip"))
            .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
            .GET()
            .build();
        
        HttpResponse<String> response = client.send(
            request, 
            HttpResponse.BodyHandlers.ofString()
        );
        
        System.out.println("Response: " + response.body());
    }
}

Dynamiczny wybór proxy z własnym ProxySelector

Dla zaawansowanych scenariuszy — np. rotacja IP czy geo-targeting — warto zaimplementować własny ProxySelector:

import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class RotatingProxySelector extends ProxySelector {
    
    private final List<Proxy> proxies;
    private final AtomicInteger counter = new AtomicInteger(0);
    private final ConcurrentHashMap<URI, List<Proxy>> cache = new ConcurrentHashMap<>();
    
    public RotatingProxySelector(List<String> proxyAddresses) {
        this.proxies = proxyAddresses.stream()
            .map(addr -> {
                String[] parts = addr.split(":");
                return new Proxy(
                    Proxy.Type.HTTP,
                    new InetSocketAddress(parts[0], Integer.parseInt(parts[1]))
                );
            })
            .toList();
    }
    
    @Override
    public List<Proxy> select(URI uri) {
        // Round-robin rotation
        int index = counter.getAndIncrement() % proxies.size();
        Proxy selected = proxies.get(index);
        System.out.println("Selected proxy: " + selected.address());
        return List.of(selected);
    }
    
    @Override
    public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
        System.err.println("Proxy connection failed: " + sa + " - " + ioe.getMessage());
        // Logika retry lub usunięcie uszkodzonego proxy z puli
    }
}

OkHttp z Proxy i Authenticator

OkHttp to najpopularniejszy klient HTTP w ekosystemie Java/Android. Jego API jest bardziej intuicyjne niż HttpClient, a obsługa proxy wymaga ustawienia Proxy i Authenticator na OkHttpClient.

Podstawowa konfiguracja OkHttp proxy

import okhttp3.Authenticator;
import okhttp3.Credentials;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Route;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

public class OkHttpProxyExample {
    
    private static final String PROXY_HOST = "gate.proxyhat.com";
    private static final int PROXY_PORT = 8080;
    private static final String USERNAME = "user-country-DE";
    private static final String PASSWORD = "your_password";
    
    public static void main(String[] args) throws IOException {
        // Definicja proxy
        Proxy proxy = new Proxy(
            Proxy.Type.HTTP,
            new InetSocketAddress(PROXY_HOST, PROXY_PORT)
        );
        
        // Authenticator dla proxy
        Authenticator proxyAuthenticator = new Authenticator() {
            @Override
            public Request authenticate(Route route, Response response) throws IOException {
                if (response.request().header("Proxy-Authorization") != null) {
                    return null; // Już próbowaliśmy autoryzacji - nie powtarzaj
                }
                String credential = Credentials.basic(USERNAME, PASSWORD);
                return response.request().newBuilder()
                    .header("Proxy-Authorization", credential)
                    .build();
            }
        };
        
        OkHttpClient client = new OkHttpClient.Builder()
            .proxy(proxy)
            .proxyAuthenticator(proxyAuthenticator)
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(60, TimeUnit.SECONDS)
            .writeTimeout(60, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .build();
        
        Request request = new Request.Builder()
            .url("https://httpbin.org/ip")
            .header("User-Agent", "Mozilla/5.0")
            .build();
        
        try (Response response = client.newCall(request).execute()) {
            System.out.println("Status: " + response.code());
            System.out.println("Body: " + response.body().string());
        }
    }
}

Connection pooling i optymalizacja w OkHttp

OkHttp automatycznie zarządza pulą połączeń, ale warto dostosować parametry do przypadku użycia:

import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import java.util.concurrent.TimeUnit;

public class OkHttpOptimizedClient {
    
    public static OkHttpClient createOptimizedClient() {
        // Connection pool: max 20 idle connections, 5 minut keep-alive
        ConnectionPool connectionPool = new ConnectionPool(20, 5, TimeUnit.MINUTES);
        
        return new OkHttpClient.Builder()
            .connectionPool(connectionPool)
            .connectTimeout(15, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            // Retry interceptor z exponential backoff
            .addInterceptor(new RetryInterceptor(3))
            // Connection pool monitoring
            .eventListener(new ConnectionPoolEventListener())
            .build();
    }
    
    // Custom retry interceptor
    static class RetryInterceptor implements Interceptor {
        private final int maxRetries;
        
        RetryInterceptor(int maxRetries) {
            this.maxRetries = maxRetries;
        }
        
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            IOException lastException = null;
            
            for (int attempt = 0; attempt <= maxRetries; attempt++) {
                try {
                    return chain.proceed(request);
                } catch (IOException e) {
                    lastException = e;
                    if (attempt == maxRetries) break;
                    
                    // Exponential backoff
                    long delayMs = (long) Math.pow(2, attempt) * 1000;
                    try {
                        Thread.sleep(delayMs);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        throw new IOException("Interrupted during retry", ie);
                    }
                }
            }
            throw new IOException("Max retries exceeded", lastException);
        }
    }
}

Jsoup z proxy dla parsowania HTML

Jsoup to biblioteka do parsowania HTML, która pozwala na pobieranie dokumentów bezpośrednio z URL. Obsługa proxy wymaga użycia Connection.proxy() lub systemowych właściwości.

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;

public class JsoupProxyExample {
    
    private static final String PROXY_HOST = "gate.proxyhat.com";
    private static final int PROXY_PORT = 8080;
    private static final String USERNAME = "user-country-US-session-abc123";
    private static final String PASSWORD = "your_password";
    
    public static void main(String[] args) {
        try {
            // Proxy HTTP
            Proxy proxy = new Proxy(
                Proxy.Type.HTTP,
                new InetSocketAddress(PROXY_HOST, PROXY_PORT)
            );
            
            // Pobieranie dokumentu przez proxy z autoryzacją
            Document doc = Jsoup.connect("https://example.com")
                .proxy(proxy)
                .header("Proxy-Authorization", java.util.Base64.getEncoder()
                    .encodeToString((USERNAME + ":" + PASSWORD).getBytes()))
                .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
                .timeout(30000)
                .followRedirects(true)
                .get();
            
            // Parsowanie tytułu
            String title = doc.title();
            System.out.println("Title: " + title);
            
            // Wyciąganie linków
            for (Element link : doc.select("a[href]")) {
                System.out.println("Link: " + link.attr("href") + " - " + link.text());
            }
            
        } catch (IOException e) {
            System.err.println("Error fetching document: " + e.getMessage());
        }
    }
}

Integracja Jsoup z OkHttp dla lepszej kontroli

Dla bardziej zaawansowanych scenariuszy warto użyć OkHttp do pobrania HTML, a Jsoup tylko do parsowania:

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class JsoupWithOkHttpExample {
    
    private final OkHttpClient httpClient;
    
    public JsoupWithOkHttpExample(OkHttpClient httpClient) {
        this.httpClient = httpClient;
    }
    
    public Document fetchAndParse(String url) throws IOException {
        Request request = new Request.Builder()
            .url(url)
            .header("Accept", "text/html,application/xhtml+xml")
            .header("Accept-Language", "en-US,en;q=0.9")
            .build();
        
        try (Response response = httpClient.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("Unexpected response code: " + response.code());
            }
            
            String html = response.body().string();
            return Jsoup.parse(html, url); // URL jako base URI dla relative links
        }
    }
}

Apache HttpClient (krótkie omówienie)

Starsze systemy często używają Apache HttpClient 4.x. Konfiguracja proxy jest bardziej rozwlekła:

import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

public class ApacheHttpClientProxy {
    
    public static void main(String[] args) throws Exception {
        // Proxy host
        HttpHost proxy = new HttpHost("gate.proxyhat.com", 8080);
        
        // Credentials provider dla autoryzacji proxy
        CredentialsProvider credsProvider = new BasicCredentialsProvider();
        credsProvider.setCredentials(
            new AuthScope("gate.proxyhat.com", 8080),
            new UsernamePasswordCredentials("user-country-US", "your_password")
        );
        
        // Request config z proxy
        RequestConfig config = RequestConfig.custom()
            .setProxy(proxy)
            .setConnectTimeout(30000)
            .setSocketTimeout(60000)
            .build();
        
        try (CloseableHttpClient client = HttpClients.custom()
            .setDefaultCredentialsProvider(credsProvider)
            .setDefaultRequestConfig(config)
            .build()) {
            
            HttpGet request = new HttpGet("https://httpbin.org/ip");
            try (CloseableHttpResponse response = client.execute(request)) {
                String body = EntityUtils.toString(response.getEntity());
                System.out.println(body);
            }
        }
    }
}

Równoległe scrapowanie z ExecutorService i pulą residential proxy

Produkcyjne scrapowanie wymaga równoległości. Oto kompletny przykład z pulą residential proxy, rate limiting i obsługą błędów:

import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ParallelScraper {
    
    private static final String PROXY_HOST = "gate.proxyhat.com";
    private static final int PROXY_PORT = 8080;
    private static final String BASE_USER = "user-country-US";
    private static final String PASSWORD = "your_password";
    
    private final HttpClient baseClient;
    private final ExecutorService executor;
    private final Semaphore rateLimiter;
    private final AtomicInteger successCount = new AtomicInteger(0);
    private final AtomicInteger failureCount = new AtomicInteger(0);
    
    public ParallelScraper(int maxConcurrency, int requestsPerSecond) {
        this.executor = Executors.newFixedThreadPool(maxConcurrency);
        this.rateLimiter = new Semaphore(requestsPerSecond);
        this.baseClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30))
            .executor(executor)
            .build();
    }
    
    public CompletableFuture<ScrapeResult> scrapeUrl(String url, String sessionId) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                rateLimiter.acquire();
                
                // Sticky session dla residential proxy
                String username = BASE_USER + "-session-" + sessionId;
                
                HttpClient client = HttpClient.newBuilder()
                    .proxy(ProxySelector.of(
                        new InetSocketAddress(PROXY_HOST, PROXY_PORT)
                    ))
                    .authenticator(new Authenticator() {
                        @Override
                        protected PasswordAuthentication getPasswordAuthentication() {
                            return new PasswordAuthentication(username, PASSWORD.toCharArray());
                        }
                    })
                    .connectTimeout(Duration.ofSeconds(30))
                    .build();
                
                HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .timeout(Duration.ofSeconds(60))
                    .header("User-Agent", randomUserAgent())
                    .GET()
                    .build();
                
                HttpResponse<String> response = client.send(
                    request,
                    HttpResponse.BodyHandlers.ofString()
                );
                
                successCount.incrementAndGet();
                return new ScrapeResult(url, response.statusCode(), response.body(), null);
                
            } catch (Exception e) {
                failureCount.incrementAndGet();
                return new ScrapeResult(url, -1, null, e.getMessage());
            } finally {
                rateLimiter.release();
            }
        }, executor);
    }
    
    public CompletableFuture<List<ScrapeResult>> scrapeUrls(List<String> urls) {
        List<CompletableFuture<ScrapeResult>> futures = urls.stream()
            .map(url -> scrapeUrl(url, generateSessionId()))
            .toList();
        
        return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
            .thenApply(v -> futures.stream()
                .map(CompletableFuture::join)
                .toList());
    }
    
    private String generateSessionId() {
        return java.util.UUID.randomUUID().toString().substring(0, 8);
    }
    
    private String randomUserAgent() {
        String[] agents = {
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"
        };
        return agents[ThreadLocalRandom.current().nextInt(agents.length)];
    }
    
    public void shutdown() {
        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
    
    public void printStats() {
        System.out.println("Success: " + successCount.get() + ", Failures: " + failureCount.get());
    }
    
    public record ScrapeResult(String url, int statusCode, String body, String error) {}
    
    public static void main(String[] args) throws Exception {
        ParallelScraper scraper = new ParallelScraper(20, 10);
        
        List<String> urls = List.of(
            "https://httpbin.org/ip",
            "https://httpbin.org/headers",
            "https://httpbin.org/user-agent",
            "https://httpbin.org/get"
        );
        
        List<ScrapeResult> results = scraper.scrapeUrls(urls).join();
        
        results.forEach(r -> {
            if (r.error() == null) {
                System.out.println(r.url() + " -> " + r.statusCode());
            } else {
                System.out.println(r.url() + " -> ERROR: " + r.error());
            }
        });
        
        scraper.printStats();
        scraper.shutdown();
    }
}

TLS/SSL i custom SSLContext

Przy pracy z proxy możesz napotkać problemy z TLS — szczególnie gdy upstream serwer używa self-signed certificates lub niestandardowych konfiguracji. W takich przypadkach potrzebujesz własnego SSLContext:

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.net.http.HttpClient;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;

public class TlsConfigurations {
    
    // UWAGA: Tylko dla developmentu! Nie używaj w produkcji.
    public static SSLContext createTrustAllContext() throws Exception {
        TrustManager[] trustAllCerts = new TrustManager[] {
            new X509TrustManager() {
                public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
                public void checkClientTrusted(X509Certificate[] certs, String authType) {}
                public void checkServerTrusted(X509Certificate[] certs, String authType) {}
            }
        };
        
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustAllCerts, new SecureRandom());
        return sslContext;
    }
    
    // Produkcyjna konfiguracja z custom truststore
    public static SSLContext createProductionContext() throws Exception {
        // Użyj domyślnego truststore z dodatkowymi certyfikatami
        SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
        sslContext.init(null, null, new SecureRandom());
        return sslContext;
    }
    
    public static HttpClient createClientWithCustomTls(SSLContext sslContext) {
        return HttpClient.newBuilder()
            .sslContext(sslContext)
            .connectTimeout(java.time.Duration.ofSeconds(30))
            .build();
    }
}

// Dla OkHttp:
import okhttp3.OkHttpClient;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;

public class OkHttpTlsConfig {
    
    public static OkHttpClient createTrustAllClient() throws Exception {
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, new TrustManager[] {
            new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] chain, String authType) {}
                public void checkServerTrusted(X509Certificate[] chain, String authType) {}
                public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
            }
        }, new SecureRandom());
        
        return new OkHttpClient.Builder()
            .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) null)
            .hostnameVerifier((hostname, session) -> true)
            .build();
    }
}

Porównanie bibliotek HTTP dla proxy

FeatureJava HttpClientOkHttpApache HttpClientJsoup
Wbudowany w JDKTak (Java 11+)NieNieNie
Async/ReactiveTak (CompletableFuture)Tak (Call.enqueue)Tak (Future callbacks)Nie
Connection poolingTak (automatyczny)Tak (ConnectionPool)Tak (PoolingHttpClient)Brak
Proxy authAuthenticatorproxyAuthenticatorCredentialsProviderHeader ręczny
HTTP/2TakTakTak (5.x)Nie
WebSocketTakTakNieNie
HTML parsingNieNieNieTak (główna funkcja)
Rozmiar zależności0 (JDK)~800KB~1.2MB~400KB

Best practices dla produkcyjnych aplikacji

1. Timeouty i retry logic

Zawsze ustawiaj timeouty na wszystkich poziomach: connect, read, write. Domyślne wartości często są zbyt długie dla scrapowania:

// HttpClient z odpowiednimi timeoutami
HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(15))
    .build();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(url))
    .timeout(Duration.ofSeconds(30))
    .build();

2. Circuit breaker dla awarii proxy

Gdy proxy przestaje odpowiadać, nie próbuj w nieskończoność. Użyj circuit breaker:

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

public class SimpleCircuitBreaker {
    private final int failureThreshold;
    private final long timeoutMs;
    private final AtomicInteger failures = new AtomicInteger(0);
    private final AtomicLong lastFailureTime = new AtomicLong(0);
    private volatile boolean open = false;
    
    public SimpleCircuitBreaker(int failureThreshold, long timeoutMs) {
        this.failureThreshold = failureThreshold;
        this.timeoutMs = timeoutMs;
    }
    
    public boolean allowRequest() {
        if (!open) return true;
        
        if (System.currentTimeMillis() - lastFailureTime.get() > timeoutMs) {
            open = false;
            failures.set(0);
            return true;
        }
        return false;
    }
    
    public void recordFailure() {
        int count = failures.incrementAndGet();
        lastFailureTime.set(System.currentTimeMillis());
        if (count >= failureThreshold) {
            open = true;
        }
    }
    
    public void recordSuccess() {
        failures.set(0);
        open = false;
    }
}

3. Logging i monitoring

Dodaj logowanie dla każdego żądania z proxy. Śledź: czas odpowiedzi, status code, IP wyjściowe:

public class ProxyMetrics {
    private final String proxyAddress;
    private final String targetUrl;
    private final long durationMs;
    private final int statusCode;
    private final String outboundIp;
    private final String errorMessage;
    
    // Constructor, getters...
    
    public void log() {
        System.out.printf(
            "[%s] %s -> %d (%dms) IP: %s%n",
            proxyAddress, targetUrl, statusCode, durationMs, outboundIp
        );
    }
}

Key Takeaways

  • Java HttpClient (Java 11+) to nowoczesny wybór z wbudowaną obsługą HTTP/2 i asynchronicznością — używaj ProxySelector i Authenticator dla proxy.
  • OkHttp oferuje lepsze API i automatyczny connection pooling — konfiguruj przez OkHttpClient.Builder().proxy() i proxyAuthenticator().
  • Jsoup proxy wymaga ręcznego ustawienia nagłówka autoryzacji lub integracji z OkHttp dla lepszej kontroli.
  • Zawsze ustawiaj timeouty na wszystkich poziomach i implementuj retry z exponential backoff.
  • Dla równoległego scrapowania używaj ExecutorService z rate limiting i sticky sessions przez residential proxy.
  • TLS issues rozwiąż przez custom SSLContext — ale nigdy nie używaj „trust all” w produkcji.

Często zadawane pytania

Czy mogę używać SOCKS5 proxy z Java HttpClient?

Tak. Zmień typ proxy na Proxy.Type.SOCKS w ProxySelector. Dla ProxyHat użyj portu 1080: new InetSocketAddress("gate.proxyhat.com", 1080). Pamiętaj, że SOCKS5 działa na niższym poziomie i może wymagać innej konfiguracji autoryzacji.

Jak obsłużyć rotację IP w Java?

Najprościej: użyj sticky sessions z residential proxy. Nazwa użytkownika zawiera identyfikator sesji: user-country-US-session-abc123. Dla każdej sesji proxy przydziela stały IP. Aby zmienić IP, wygeneruj nowy session ID. Alternatywnie, zaimplementuj własny ProxySelector z pulą proxy i rotacją round-robin.

Dlaczego moje żądania przez proxy są wolniejsze?

Proxy dodaje dodatkowy hop w trasie żądania. Residential proxy mogą być wolniejsze niż datacenter ze względu na naturę peer-to-peer. Zminimalizuj opóźnienia przez: connection pooling, utrzymywanie sticky sessions (TCP keep-alive), wybór lokalizacji geo blisko targetu, i równoległe żądania z odpowiednim concurrency.

Jak debugować problemy z proxy w Java?

Włącz JVM logging: -Djava.util.logging.config.file=logging.properties lub użyj system properties: -Djavax.net.debug=all dla TLS. Dodaj własny interceptor/logging w OkHttp lub HttpRequest.BodyPublishers w HttpClient. Sprawdź odpowiedź 407 Proxy Authentication Required — oznacza problem z credentials.

Czy mogę używać tego samego klienta HTTP dla wielu proxy?

HttpClient jest immutable — każda zmiana proxy wymaga nowego klienta. OkHttp pozwala na zmianę proxy przez OkHttpClient.newBuilder().proxy(newProxy).build() — tworzy „płytką kopię” z nowym proxy, współdzieląc connection pool. Dla wysokiej wydajności z wieloma proxy, rozważ pulę klientów lub własny ProxySelector.

Zakończenie

Java oferuje solidne narzędzia do pracy z HTTP proxy — od natywnego HttpClient, przez popularny OkHttp, po specjalistyczne biblioteki jak Jsoup. Klucz do sukcesu to właściwa konfiguracja: timeouty, retry logic, connection pooling i monitoring.

Dla poważnego scrapowania produkcyjnego, residential proxy z sticky sessions i geo-targeting to must-have. ProxyHat oferuje oba — z prostą autoryzacją w nazwie użytkownika i wysoką dostępnością.

Sprawdź ceny residential proxy lub dostępne lokalizacje na ProxyHat. Masz pytania o implementację? Zostaw komentarz lub skontaktuj się z supportem.

Gotowy, aby zacząć?

Dostęp do ponad 50 mln rezydencjalnych IP w ponad 148 krajach z filtrowaniem AI.

Zobacz cenyProxy rezydencjalne
← Powrót do Bloga