Java HTTPプロキシ完全ガイド:HttpClient、OkHttp、Jsoupでの実装

Java 11+ HttpClient、OkHttp、Jsoupを使ったHTTPプロキシ設定の実践的なガイド。認証、接続プール、タイムアウト、並列スクレイピング、TLS設定まで、本番環境で使えるコード例を詳しく解説します。

Java HTTPプロキシ完全ガイド:HttpClient、OkHttp、Jsoupでの実装

JavaアプリケーションでHTTPプロキシを適切に設定することは、ウェブスクレイピング、API統合、地理的制限の回避において不可欠です。このガイドでは、Java 11+の標準HttpClient、OkHttp、Jsoupを使った実践的なプロキシ設定方法を、本番環境でそのまま使えるコード例とともに解説します。

Java 11+ HttpClientとProxySelector

Java 11で導入されたjava.net.http.HttpClientは、モダンなHTTPクライアントとしてプロキシ設定をネイティブにサポートしています。ProxySelectorを使ってプロキシを設定し、Authenticatorで認証情報を提供します。

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;
import java.util.Base64;

public class JavaHttpClientProxyExample {
    
    private static final String PROXY_HOST = "gate.proxyhat.com";
    private static final int PROXY_PORT = 8080;
    private static final String USERNAME = "user-country-US";
    private static final String PASSWORD = "your_password";
    
    public static void main(String[] args) throws Exception {
        // ProxySelectorを作成
        ProxySelector proxySelector = ProxySelector.of(
            InetSocketAddress.createUnresolved(PROXY_HOST, PROXY_PORT)
        );
        
        // 認証用Authenticator
        HttpClient.Authenticator authenticator = new HttpClient.Authenticator() {
            @Override
            public String getPasswordAuthenticationString() {
                return USERNAME + ":" + PASSWORD;
            }
        };
        
        // HttpClientをビルド
        HttpClient client = HttpClient.newBuilder()
            .proxy(proxySelector)
            .authenticator(authenticator)
            .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());
    }
}

ProxySelector.of()は単一のプロキシを設定する最も簡単な方法です。本番環境では、リクエストごとに異なるプロキシを選択するカスタムProxySelectorを実装することで、IPローテーションを実現できます。

カスタムProxySelectorでIPローテーション

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

public class RotatingProxySelector extends ProxySelector {
    
    private final List<Proxy> proxies;
    private final AtomicInteger counter = new AtomicInteger(0);
    
    public RotatingProxySelector(List<String> proxyAddresses) {
        this.proxies = proxyAddresses.stream()
            .map(addr -> {
                String[] parts = addr.split(":");
                return new Proxy(
                    Proxy.Type.HTTP,
                    InetSocketAddress.createUnresolved(parts[0], Integer.parseInt(parts[1]))
                );
            })
            .toList();
    }
    
    @Override
    public List<Proxy> select(URI uri) {
        int index = counter.getAndIncrement() % proxies.size();
        return List.of(proxies.get(index));
    }
    
    @Override
    public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
        System.err.println("Proxy connection failed: " + sa + " - " + ioe.getMessage());
    }
}

OkHttpでプロキシを使用する

OkHttpはJavaエコシステムで最も人気のあるHTTPクライアントの一つです。Square社が開発したこのライブラリは、接続プール、HTTP/2、WebSocketをサポートし、プロキシ設定も柔軟に行えます。

import okhttp3.Authenticator;
import okhttp3.Credentials;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Route;
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 Exception {
        // プロキシ認証用Authenticator
        Authenticator proxyAuthenticator = new Authenticator() {
            @Override
            public Request authenticate(Route route, Response response) {
                if (response.request().header("Proxy-Authorization") != null) {
                    return null; // 認証失敗、リトライしない
                }
                String credential = Credentials.basic(USERNAME, PASSWORD);
                return response.request().newBuilder()
                    .header("Proxy-Authorization", credential)
                    .build();
            }
        };
        
        // OkHttpClientをビルド
        OkHttpClient client = new OkHttpClient.Builder()
            .proxy(new Proxy(
                Proxy.Type.HTTP,
                InetSocketAddress.createUnresolved(PROXY_HOST, PROXY_PORT)
            ))
            .proxyAuthenticator(proxyAuthenticator)
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(60, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .build();
        
        Request request = new Request.Builder()
            .url("https://httpbin.org/headers")
            .header("User-Agent", "ProxyHat-Java-Client/1.0")
            .build();
        
        try (Response response = client.newCall(request).execute()) {
            System.out.println("Status: " + response.code());
            System.out.println("Body: " + response.body().string());
        }
    }
}

OkHttpのretryOnConnectionFailure(true)は、プロキシ接続が失敗した場合に自動的にリトライします。ただし、同じプロキシでリトライするため、本格的なIPローテーションには別途ロジックが必要です。

Jsoupでプロキシ経由のHTML解析

JsoupはHTML解析に特化したライブラリで、プロキシ経由でHTMLを取得し、そのまま解析できます。SEOデータの収集やウェブスクレイピングに最適です。

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.net.Proxy;
import java.net.InetSocketAddress;

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-search1";
    private static final String PASSWORD = "your_password";
    
    public static void main(String[] args) throws Exception {
        // HTTPプロキシを作成
        Proxy proxy = new Proxy(
            Proxy.Type.HTTP,
            InetSocketAddress.createUnresolved(PROXY_HOST, PROXY_PORT)
        );
        
        // Jsoupでプロキシ経由のHTMLを取得・解析
        Document doc = Jsoup.connect("https://example.com/search?q=java+http+proxy")
            .proxy(proxy)
            .header("Proxy-Authorization", 
                "Basic " + java.util.Base64.getEncoder()
                    .encodeToString((USERNAME + ":" + PASSWORD).getBytes()))
            .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
            .timeout(30000)
            .followRedirects(true)
            .get();
        
        // 検索結果のタイトルを抽出
        Elements results = doc.select("h3");
        for (Element result : results) {
            System.out.println("Title: " + result.text());
        }
        
        // リンクを抽出
        Elements links = doc.select("a[href]");
        System.out.println("Found " + links.size() + " links");
    }
}

Jsoupのproxy()メソッドはjava.net.Proxyを受け入れます。認証情報はProxy-Authorizationヘッダーとして手動で設定する必要があります。

接続プール、タイムアウト、リトライポリシー

本番環境では、接続プールの管理、適切なタイムアウト設定、リトライポリシーが重要です。これらを適切に設定しないと、リソースリークや無限待機が発生します。

パラメータ 推奨値 説明
connectTimeout 10-30秒 TCP接続確立のタイムアウト
readTimeout 30-120秒 レスポンス読み込みのタイムアウト
writeTimeout 15-30秒 リクエスト送信のタイムアウト
maxIdleConnections 5-20 アイドル接続の最大数
keepAliveDuration 5分 接続を保持する時間
retryCount 3回 失敗時のリトライ回数
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import java.util.concurrent.TimeUnit;

public class ProductionHttpClient {
    
    private final OkHttpClient client;
    private final okhttp3.Authenticator proxyAuth;
    
    public ProductionHttpClient(String proxyHost, int proxyPort, 
                                 String username, String password) {
        this.proxyAuth = (route, response) -> {
            if (response.request().header("Proxy-Authorization") != null) {
                return null;
            }
            return response.request().newBuilder()
                .header("Proxy-Authorization", 
                    okhttp3.Credentials.basic(username, password))
                .build();
        };
        
        ConnectionPool connectionPool = new ConnectionPool(
            20,     // maxIdleConnections
            5,      // keepAliveDuration
            TimeUnit.MINUTES
        );
        
        this.client = new OkHttpClient.Builder()
            .proxy(new Proxy(
                Proxy.Type.HTTP,
                InetSocketAddress.createUnresolved(proxyHost, proxyPort)
            ))
            .proxyAuthenticator(proxyAuth)
            .connectionPool(connectionPool)
            .connectTimeout(15, TimeUnit.SECONDS)
            .readTimeout(60, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .build();
    }
    
    public OkHttpClient getClient() {
        return client;
    }
    
    public void shutdown() {
        client.dispatcher().executorService().shutdown();
        client.connectionPool().evictAll();
    }
}

Apache HttpClientでのプロキシ設定

レガシーシステムではApache HttpClientが依然として広く使用されています。バージョン4.xと5.xで設定方法が異なります。

import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.classic.methods.HttpGet;
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.proxy.HttpProxyPlanner;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.util.Timeout;

public class ApacheHttpClientProxyExample {
    
    public static void main(String[] args) throws Exception {
        HttpHost proxy = new HttpHost("http", "gate.proxyhat.com", 8080);
        
        PoolingHttpClientConnectionManager connManager = 
            PoolingHttpClientConnectionManagerBuilder.create()
                .setMaxConnTotal(100)
                .setMaxConnPerRoute(20)
                .build();
        
        RequestConfig requestConfig = RequestConfig.custom()
            .setConnectTimeout(Timeout.ofSeconds(30))
            .setResponseTimeout(Timeout.ofSeconds(60))
            .build();
        
        try (CloseableHttpClient client = HttpClients.custom()
            .setConnectionManager(connManager)
            .setProxy(proxy)
            .setDefaultRequestConfig(requestConfig)
            .build()) {
            
            HttpGet request = new HttpGet("https://httpbin.org/get");
            // プロキシ認証ヘッダーを設定
            String auth = java.util.Base64.getEncoder()
                .encodeToString(("user-country-JP:your_password").getBytes());
            request.setHeader("Proxy-Authorization", "Basic " + auth);
            
            try (var response = client.execute(request)) {
                System.out.println("Status: " + response.getCode());
                System.out.println("Body: " + EntityUtils.toString(response.getEntity()));
            }
        }
    }
}

並列スクレイピングとExecutorService

大規模なスクレイピングでは、複数のプロキシを使って並列にリクエストを送信することで、スループットを劇的に向上させられます。プロキシプールとExecutorServiceを組み合わせたパターンを紹介します。

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 PASSWORD = "your_password";
    
    // 国コードのリスト(IPローテーション用)
    private static final List<String> COUNTRIES = List.of(
        "US", "DE", "JP", "GB", "FR", "CA", "AU"
    );
    
    public static void main(String[] args) throws Exception {
        int threadCount = 10;
        int requestCount = 50;
        
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        AtomicInteger successCount = new AtomicInteger(0);
        AtomicInteger failureCount = new AtomicInteger(0);
        
        // 各スレッド用のHttpClient(スレッドセーフ)
        HttpClient clientTemplate = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30))
            .build();
        
        List<String> urls = generateUrls(requestCount);
        
        List<CompletableFuture<Void>> futures = urls.stream()
            .map(url -> CompletableFuture.runAsync(() -> {
                // 国コードをローテーション
                String country = COUNTRIES.get(
                    ThreadLocalRandom.current().nextInt(COUNTRIES.size())
                );
                String username = "user-country-" + country;
                
                try {
                    scrapeUrl(clientTemplate, url, username);
                    successCount.incrementAndGet();
                } catch (Exception e) {
                    System.err.println("Failed: " + url + " - " + e.getMessage());
                    failureCount.incrementAndGet();
                }
            }, executor))
            .toList();
        
        // 全タスクの完了を待機
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
        
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        
        System.out.println("Success: " + successCount.get());
        System.out.println("Failure: " + failureCount.get());
    }
    
    private static void scrapeUrl(HttpClient template, String url, String username) 
            throws Exception {
        // セッションごとに新しいクライアントを作成(異なるプロキシ認証)
        HttpClient client = HttpClient.newBuilder()
            .proxy(java.net.ProxySelector.of(
                InetSocketAddress.createUnresolved(PROXY_HOST, PROXY_PORT)
            ))
            .authenticator(new HttpClient.Authenticator() {
                @Override
                public String getPasswordAuthenticationString() {
                    return username + ":" + PASSWORD;
                }
            })
            .connectTimeout(Duration.ofSeconds(30))
            .build();
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .timeout(Duration.ofSeconds(60))
            .GET()
            .build();
        
        HttpResponse<String> response = client.send(
            request, HttpResponse.BodyHandlers.ofString()
        );
        
        if (response.statusCode() == 200) {
            System.out.println("OK: " + url + " via " + username);
        }
    }
    
    private static List<String> generateUrls(int count) {
        return java.util.stream.IntStream.range(0, count)
            .mapToObj(i -> "https://httpbin.org/delay/1?page=" + i)
            .toList();
    }
}

このパターンでは、user-country-XX形式のユーザー名を使って、リクエストごとに異なる国のIPアドレスを割り当てています。ProxyHatのSERPトラッキングなどで、地理的に分散したデータ収集に役立ちます。

TLS/SSL設定とカスタムSSLContext

プロキシを使用する際、TLS接続はクライアントとターゲットサーバーの間で確立されます(プロキシはTCPトンネルとして機能)。ただし、特殊な環境ではカスタムSSLContextが必要になる場合があります。

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

public class CustomTlsHttpClient {
    
    // 警告: 本番環境では適切な証明書検証を使用してください
    // これはテスト環境や自己署名証明書用の例です
    
    public static HttpClient createTrustAllClient() throws Exception {
        TrustManager[] trustAllCerts = new TrustManager[]{
            new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType) {}
                
                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType) {}
                
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            }
        };
        
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
        
        return HttpClient.newBuilder()
            .sslContext(sslContext)
            .connectTimeout(Duration.ofSeconds(30))
            .build();
    }
    
    // OkHttp用のカスタムSSLContext
    public static okhttp3.OkHttpClient createOkHttpWithCustomSsl(
            okhttp3.OkHttpClient baseClient) throws Exception {
        
        SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
        sslContext.init(null, null, null);
        
        return baseClient.newBuilder()
            .sslSocketFactory(
                sslContext.getSocketFactory(),
                (X509TrustManager) javax.net.ssl.TrustManagerFactory
                    .getInstance(javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm())
                    .getTrustManagers()[0]
            )
            .build();
    }
}

セキュリティ警告: 上記の「全て信頼」設定はテスト環境専用です。本番環境では、適切な証明書検証を行うか、信頼できるCAの証明書のみを受け入れるTrustManagerを実装してください。

エラーハンドリングとリトライ戦略

プロキシ経由のリクエストでは、ネットワークエラー、プロキシエラー、ターゲットサーバーのエラーなど、様々な障害が発生します。指数バックオフを使ったリトライ戦略を実装しましょう。

import java.io.IOException;
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.concurrent.TimeUnit;

public class RetryableHttpClient {
    
    private final HttpClient client;
    private final int maxRetries;
    private final long initialDelayMs;
    private final double backoffMultiplier;
    
    public RetryableHttpClient(int maxRetries, long initialDelayMs, 
                                 double backoffMultiplier) {
        this.client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30))
            .build();
        this.maxRetries = maxRetries;
        this.initialDelayMs = initialDelayMs;
        this.backoffMultiplier = backoffMultiplier;
    }
    
    public String get(String url) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .timeout(Duration.ofSeconds(60))
            .GET()
            .build();
        
        Exception lastException = null;
        long delay = initialDelayMs;
        
        for (int attempt = 0; attempt <= maxRetries; attempt++) {
            try {
                HttpResponse<String> response = client.send(
                    request, HttpResponse.BodyHandlers.ofString()
                );
                
                // 成功
                if (response.statusCode() >= 200 && response.statusCode() < 300) {
                    return response.body();
                }
                
                // 429 Too Many Requests または 5xx エラー
                if (response.statusCode() == 429 || response.statusCode() >= 500) {
                    throw new IOException("HTTP " + response.statusCode());
                }
                
                // 4xx エラーはリトライしない
                throw new RuntimeException("HTTP Error: " + response.statusCode());
                
            } catch (IOException e) {
                lastException = e;
                if (attempt < maxRetries) {
                    System.out.println("Retry " + (attempt + 1) + " after " + delay + "ms");
                    TimeUnit.MILLISECONDS.sleep(delay);
                    delay = (long) (delay * backoffMultiplier);
                }
            }
        }
        
        throw new RuntimeException("Max retries exceeded", lastException);
    }
}

Key Takeaways

  • Java 11+ HttpClientProxySelectorAuthenticatorでプロキシを設定。最も標準的でモダンなアプローチ。
  • OkHttpは接続プールとリトライが組み込み済み。カスタムAuthenticatorでプロキシ認証を処理。
  • JsoupはHTML解析に最適。proxy()メソッドとProxy-Authorizationヘッダーで設定。
  • 接続プールを適切に設定しないと、高負荷時にリソース枯渇が発生。
  • タイムアウトは必ず設定。デフォルトの無限待機は本番環境で致命的。
  • 並列スクレイピングではExecutorServiceとプロキシローテーションを組み合わせて、IP分散とスループット向上を実現。
  • TLS設定は必要に応じてカスタマイズ。自己署名証明書環境ではSSLContextを調整。

詳細なプロキシ設定や料金については、ProxyHatの料金ページをご確認ください。世界中のロケーションから選択できる住宅用プロキシとデータセンタープロキシを提供しています。

始める準備はできましたか?

AIフィルタリングで148か国以上、5,000万以上のレジデンシャルIPにアクセス。

料金を見るレジデンシャルプロキシ
← ブログに戻る