Java HttpClient

Java 11以降で標準提供されるHTTPクライアントAPI。モダンで流暢なAPIデザイン、HTTP/2サポート、非同期・同期両対応。外部依存関係不要でJDKに内蔵され、WebSocketサポートやCompletableFutureベースの非同期処理を提供。

HTTPクライアントJava標準ライブラリHTTP/2非同期セキュア

GitHub概要

openjdk/jdk

JDK main-line development https://openjdk.org/projects/jdk

スター21,352
ウォッチ355
フォーク6,084
作成日:2018年9月17日
言語:Java
ライセンス:GNU General Public License v2.0

トピックス

javajvmopenjdk

スター履歴

openjdk/jdk Star History
データ取得日時: 2025/7/18 01:39

ライブラリ

Java HttpClient

概要

Java HttpClientは、Java 11で正式導入された「java.net.http」パッケージの標準HTTPクライアントライブラリです。Java 9でインキュベーション機能として登場し、HTTP/1.1とHTTP/2の両方をサポート、同期・非同期プログラミングモデルに対応した現代的なHTTP通信ソリューション。従来のHttpURLConnectionに代わる公式後継として、リアクティブストリーム対応、ビルダーパターン採用、SSL/TLS完全サポートを特徴とし、外部依存なしでプロダクション品質のHTTP通信を実現します。

詳細

Java HttpClient 2025年版は、Java標準ライブラリとして成熟度と安定性を提供する現代的なHTTP実装です。HTTP/2プロトコルの完全サポートにより効率的な多重化通信を実現し、CompletableFutureベースの非同期API、リアクティブストリームによるボディ処理、自動リダイレクト・認証・プロキシサポートを包括的に提供。JDK標準搭載により外部依存関係が不要で、企業環境での採用が容易。Spring Boot、Quarkus等の主要フレームワークでも推奨HTTP客户端として位置づけられています。

主な特徴

  • JDK標準搭載: Java 11以降で外部依存なしで利用可能
  • HTTP/2完全対応: 多重化、サーバープッシュ、ヘッダー圧縮サポート
  • 同期・非同期両対応: CompletableFutureによる柔軟なプログラミングモデル
  • リアクティブストリーム: 効率的なリクエスト・レスポンスボディ処理
  • ビルダーパターン: 直感的で型安全なAPI設計
  • 包括的セキュリティ: SSL/TLS、認証、証明書検証の完全サポート

メリット・デメリット

メリット

  • JDK標準ライブラリで外部依存関係なし、長期サポート保証
  • HTTP/2対応による高性能な並列通信とリソース効率の向上
  • CompletableFutureによる自然な非同期プログラミング体験
  • 型安全で保守性の高いビルダーパターンAPI
  • 企業環境での採用が容易、セキュリティ監査もパス
  • Spring Boot等の主要フレームワークでの推奨クライアント

デメリット

  • Java 11以降限定でレガシー環境では利用不可
  • サードパーティライブラリと比較して高レベル機能が限定的
  • HTTPSプロキシ設定など一部の詳細設定が複雑
  • ストリーミング処理APIがやや低レベル
  • デバッグやログ出力機能が基本的
  • リトライ・サーキットブレーカー等は自前実装が必要

参考ページ

書き方の例

基本セットアップと依存関係

// Java 11以降では追加依存関係は不要
// module-info.java (モジュールシステム使用時)
module myapp {
    requires java.net.http;
}
# Java環境の確認
java -version  # Java 11以降であることを確認

# プロジェクト作成例(Mavenの場合)
mvn archetype:generate -DgroupId=com.example.httpclient \
  -DartifactId=java-httpclient-demo \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DinteractiveMode=false

# JDK 11以降を設定(pom.xml)
<!-- pom.xml -->
<properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
</properties>

基本的なHTTPリクエスト(GET・POST・PUT・DELETE)

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.CompletableFuture;

public class BasicHttpClientExample {
    
    public static void main(String[] args) throws Exception {
        // HttpClientの作成
        HttpClient client = HttpClient.newHttpClient();
        
        // 基本的なGETリクエスト
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/users"))
                .GET()
                .build();
        
        HttpResponse<String> response = client.send(request, 
                HttpResponse.BodyHandlers.ofString());
        
        System.out.println("Status Code: " + response.statusCode());
        System.out.println("Headers: " + response.headers().map());
        System.out.println("Body: " + response.body());
    }
    
    // カスタム設定でのHttpClient作成
    public static void customHttpClientExample() throws Exception {
        HttpClient client = HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_2)  // HTTP/2を優先
                .followRedirects(HttpClient.Redirect.NORMAL)
                .connectTimeout(Duration.ofSeconds(20))
                .build();
        
        // クエリパラメータ付きGETリクエスト
        String url = "https://api.example.com/users?page=1&limit=10&sort=created_at";
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .header("Accept", "application/json")
                .header("User-Agent", "MyApp/1.0 (Java HttpClient)")
                .timeout(Duration.ofMinutes(1))
                .GET()
                .build();
        
        HttpResponse<String> response = client.send(request, 
                HttpResponse.BodyHandlers.ofString());
        
        if (response.statusCode() == 200) {
            System.out.println("Success: " + response.body());
        } else {
            System.out.println("Error: " + response.statusCode());
        }
    }
    
    // POSTリクエスト(JSON送信)
    public static void postJsonExample() throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        
        String jsonBody = """
                {
                    "name": "田中太郎",
                    "email": "[email protected]",
                    "age": 30
                }
                """;
        
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/users"))
                .header("Content-Type", "application/json")
                .header("Authorization", "Bearer your-token")
                .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
                .build();
        
        HttpResponse<String> response = client.send(request, 
                HttpResponse.BodyHandlers.ofString());
        
        if (response.statusCode() == 201) {
            System.out.println("ユーザー作成完了: " + response.body());
        } else {
            System.out.println("エラー: " + response.statusCode() + " - " + response.body());
        }
    }
    
    // PUTとDELETEリクエスト
    public static void putDeleteExample() throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        
        // PUTリクエスト(データ更新)
        String updatedJsonBody = """
                {
                    "name": "田中次郎",
                    "email": "[email protected]",
                    "age": 31
                }
                """;
        
        HttpRequest putRequest = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/users/123"))
                .header("Content-Type", "application/json")
                .header("Authorization", "Bearer your-token")
                .PUT(HttpRequest.BodyPublishers.ofString(updatedJsonBody))
                .build();
        
        HttpResponse<String> putResponse = client.send(putRequest, 
                HttpResponse.BodyHandlers.ofString());
        
        System.out.println("更新ステータス: " + putResponse.statusCode());
        
        // DELETEリクエスト
        HttpRequest deleteRequest = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/users/123"))
                .header("Authorization", "Bearer your-token")
                .DELETE()
                .build();
        
        HttpResponse<String> deleteResponse = client.send(deleteRequest, 
                HttpResponse.BodyHandlers.ofString());
        
        if (deleteResponse.statusCode() == 204) {
            System.out.println("ユーザー削除完了");
        }
    }
}

非同期リクエストとCompletableFuture

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.List;
import java.util.stream.Collectors;

public class AsyncHttpClientExample {
    
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        
        // 単一の非同期リクエスト
        asyncSingleRequest(client);
        
        // 複数の並列非同期リクエスト
        asyncMultipleRequests(client);
        
        // 非同期チェイン処理
        asyncChainedRequests(client);
    }
    
    // 単一非同期リクエスト
    public static void asyncSingleRequest(HttpClient client) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/users"))
                .header("Accept", "application/json")
                .GET()
                .build();
        
        CompletableFuture<HttpResponse<String>> futureResponse = 
                client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
        
        // 非同期処理の結果を待機
        HttpResponse<String> response = futureResponse.get();
        System.out.println("非同期レスポンス: " + response.statusCode());
        
        // またはコールバックで処理
        futureResponse.thenAccept(resp -> {
            System.out.println("コールバック処理: " + resp.body());
        }).join();
    }
    
    // 複数並列非同期リクエスト
    public static void asyncMultipleRequests(HttpClient client) throws Exception {
        List<String> urls = List.of(
                "https://api.example.com/users",
                "https://api.example.com/posts",
                "https://api.example.com/comments",
                "https://api.example.com/categories"
        );
        
        // 並列リクエストの開始
        List<CompletableFuture<HttpResponse<String>>> futures = urls.stream()
                .map(url -> HttpRequest.newBuilder()
                        .uri(URI.create(url))
                        .header("Accept", "application/json")
                        .GET()
                        .build())
                .map(request -> client.sendAsync(request, HttpResponse.BodyHandlers.ofString()))
                .collect(Collectors.toList());
        
        // 全ての完了を待機
        CompletableFuture<List<HttpResponse<String>>> allFutures = 
                CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
                        .thenApply(v -> futures.stream()
                                .map(CompletableFuture::join)
                                .collect(Collectors.toList()));
        
        List<HttpResponse<String>> responses = allFutures.get();
        
        for (int i = 0; i < responses.size(); i++) {
            HttpResponse<String> response = responses.get(i);
            System.out.println("URL " + urls.get(i) + " - Status: " + response.statusCode());
        }
    }
    
    // 非同期チェイン処理
    public static void asyncChainedRequests(HttpClient client) throws Exception {
        // 最初のリクエスト
        HttpRequest initialRequest = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/auth/login"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString("""
                        {"username": "user", "password": "pass"}
                        """))
                .build();
        
        // チェイン処理:ログイン → トークン取得 → 保護されたリソースへアクセス
        CompletableFuture<String> result = client
                .sendAsync(initialRequest, HttpResponse.BodyHandlers.ofString())
                .thenCompose(loginResponse -> {
                    if (loginResponse.statusCode() == 200) {
                        // ログイン成功、トークンを抽出(実際にはJSONパースが必要)
                        String token = extractTokenFromResponse(loginResponse.body());
                        
                        // トークンを使用して保護されたリソースにアクセス
                        HttpRequest protectedRequest = HttpRequest.newBuilder()
                                .uri(URI.create("https://api.example.com/protected-resource"))
                                .header("Authorization", "Bearer " + token)
                                .GET()
                                .build();
                        
                        return client.sendAsync(protectedRequest, HttpResponse.BodyHandlers.ofString());
                    } else {
                        // ログイン失敗の場合の処理
                        return CompletableFuture.failedFuture(
                                new RuntimeException("Login failed: " + loginResponse.statusCode())
                        );
                    }
                })
                .thenApply(protectedResponse -> {
                    if (protectedResponse.statusCode() == 200) {
                        return protectedResponse.body();
                    } else {
                        throw new RuntimeException("Protected resource access failed: " + 
                                protectedResponse.statusCode());
                    }
                })
                .exceptionally(throwable -> {
                    System.err.println("チェイン処理でエラー: " + throwable.getMessage());
                    return "Error occurred";
                });
        
        String finalResult = result.get();
        System.out.println("チェイン処理結果: " + finalResult);
    }
    
    private static String extractTokenFromResponse(String body) {
        // 実際にはJSONライブラリを使用してパース
        // ここではサンプルのためハードコーディング
        return "sample-jwt-token";
    }
}

認証・セキュリティ・カスタム設定

import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
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.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.Base64;
import javax.net.ssl.SSLContext;

public class SecurityHttpClientExample {
    
    public static void main(String[] args) throws Exception {
        // Basic認証の例
        basicAuthenticationExample();
        
        // Bearer Token認証
        bearerTokenExample();
        
        // プロキシ設定
        proxyConfigurationExample();
        
        // SSL/TLS設定
        sslConfigurationExample();
        
        // カスタム認証
        customAuthenticationExample();
    }
    
    // Basic認証
    public static void basicAuthenticationExample() throws Exception {
        HttpClient client = HttpClient.newBuilder()
                .authenticator(new Authenticator() {
                    @Override
                    protected PasswordAuthentication getPasswordAuthentication() {
                        return new PasswordAuthentication("username", "password".toCharArray());
                    }
                })
                .build();
        
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/private"))
                .GET()
                .build();
        
        HttpResponse<String> response = client.send(request, 
                HttpResponse.BodyHandlers.ofString());
        
        System.out.println("Basic認証ステータス: " + response.statusCode());
        
        // または手動でBasic認証ヘッダー設定
        String auth = "username:password";
        String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());
        
        HttpRequest manualBasicRequest = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/private"))
                .header("Authorization", "Basic " + encodedAuth)
                .GET()
                .build();
        
        HttpResponse<String> manualResponse = client.send(manualBasicRequest, 
                HttpResponse.BodyHandlers.ofString());
        
        System.out.println("手動Basic認証ステータス: " + manualResponse.statusCode());
    }
    
    // Bearer Token認証
    public static void bearerTokenExample() throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        
        String jwtToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // 実際のJWTトークン
        
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/protected"))
                .header("Authorization", "Bearer " + jwtToken)
                .header("Accept", "application/json")
                .header("User-Agent", "MyApp/1.0 (Java HttpClient)")
                .GET()
                .build();
        
        HttpResponse<String> response = client.send(request, 
                HttpResponse.BodyHandlers.ofString());
        
        System.out.println("Bearer認証ステータス: " + response.statusCode());
        
        // APIキー認証の例
        HttpRequest apiKeyRequest = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/data"))
                .header("X-API-Key", "your-api-key-here")
                .header("Accept", "application/json")
                .GET()
                .build();
        
        HttpResponse<String> apiKeyResponse = client.send(apiKeyRequest, 
                HttpResponse.BodyHandlers.ofString());
        
        System.out.println("APIキー認証ステータス: " + apiKeyResponse.statusCode());
    }
    
    // プロキシ設定
    public static void proxyConfigurationExample() throws Exception {
        // HTTPプロキシの設定
        HttpClient client = HttpClient.newBuilder()
                .proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 8080)))
                .build();
        
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/data"))
                .GET()
                .build();
        
        HttpResponse<String> response = client.send(request, 
                HttpResponse.BodyHandlers.ofString());
        
        System.out.println("プロキシ経由ステータス: " + response.statusCode());
        
        // 認証付きプロキシ(Authenticatorと組み合わせ)
        HttpClient authProxyClient = HttpClient.newBuilder()
                .proxy(ProxySelector.of(new InetSocketAddress("authenticated-proxy.example.com", 8080)))
                .authenticator(new Authenticator() {
                    @Override
                    protected PasswordAuthentication getPasswordAuthentication() {
                        if (getRequestorType() == RequestorType.PROXY) {
                            return new PasswordAuthentication("proxyuser", "proxypass".toCharArray());
                        }
                        return null;
                    }
                })
                .build();
    }
    
    // SSL/TLS設定
    public static void sslConfigurationExample() throws Exception {
        // デフォルトSSLContext使用
        HttpClient defaultSslClient = HttpClient.newBuilder()
                .sslContext(SSLContext.getDefault())
                .build();
        
        // カスタムSSLContext(例:特定のTLSバージョン指定)
        try {
            SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
            sslContext.init(null, null, null);
            
            HttpClient customSslClient = HttpClient.newBuilder()
                    .sslContext(sslContext)
                    .build();
            
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create("https://secure-api.example.com/data"))
                    .header("Accept", "application/json")
                    .GET()
                    .build();
            
            HttpResponse<String> response = customSslClient.send(request, 
                    HttpResponse.BodyHandlers.ofString());
            
            System.out.println("カスタムSSL ステータス: " + response.statusCode());
            
        } catch (NoSuchAlgorithmException e) {
            System.err.println("TLSv1.3が利用できません: " + e.getMessage());
        }
    }
    
    // カスタム認証とヘッダー
    public static void customAuthenticationExample() throws Exception {
        HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(30))
                .build();
        
        // カスタムヘッダーとタイムアウト設定
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://secure-api.example.com/data"))
                .header("Accept", "application/json")
                .header("Accept-Language", "ja-JP,en-US")
                .header("X-API-Version", "v2")
                .header("X-Request-ID", java.util.UUID.randomUUID().toString())
                .header("Authorization", "Bearer your-jwt-token")
                .header("User-Agent", "MyApp/1.0 (Java " + System.getProperty("java.version") + ")")
                .timeout(Duration.ofSeconds(60))
                .GET()
                .build();
        
        try {
            HttpResponse<String> response = client.send(request, 
                    HttpResponse.BodyHandlers.ofString());
            
            System.out.println("カスタム認証ステータス: " + response.statusCode());
            System.out.println("レスポンスヘッダー: " + response.headers().map());
            
            // Cookie処理の例
            response.headers().allValues("Set-Cookie").forEach(cookie -> {
                System.out.println("受信Cookie: " + cookie);
            });
            
        } catch (Exception e) {
            System.err.println("リクエストエラー: " + e.getMessage());
        }
    }
}

エラーハンドリングとリトライ機能

import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpConnectTimeoutException;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;

public class ErrorHandlingHttpClientExample {
    
    private static final HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .followRedirects(HttpClient.Redirect.NORMAL)
            .build();
    
    public static void main(String[] args) {
        // 基本的なエラーハンドリング
        basicErrorHandling();
        
        // リトライ機能付きリクエスト
        retryableRequest();
        
        // 非同期エラーハンドリング
        asyncErrorHandling();
        
        // サーキットブレーカーパターン
        circuitBreakerExample();
    }
    
    // 基本的なエラーハンドリング
    public static void basicErrorHandling() {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/data"))
                .timeout(Duration.ofSeconds(30))
                .GET()
                .build();
        
        try {
            HttpResponse<String> response = client.send(request, 
                    HttpResponse.BodyHandlers.ofString());
            
            // ステータスコード別処理
            switch (response.statusCode()) {
                case 200:
                    System.out.println("成功: " + response.body());
                    break;
                case 401:
                    System.out.println("認証エラー: トークンを確認してください");
                    break;
                case 403:
                    System.out.println("権限エラー: アクセス権限がありません");
                    break;
                case 404:
                    System.out.println("見つかりません: リソースが存在しません");
                    break;
                case 429:
                    System.out.println("レート制限: しばらく待ってから再試行してください");
                    break;
                case 500:
                case 502:
                case 503:
                case 504:
                    System.out.println("サーバーエラー: " + response.statusCode());
                    break;
                default:
                    System.out.println("予期しないステータス: " + response.statusCode());
            }
            
        } catch (HttpConnectTimeoutException e) {
            System.err.println("接続タイムアウト: " + e.getMessage());
        } catch (HttpTimeoutException e) {
            System.err.println("リクエストタイムアウト: " + e.getMessage());
        } catch (ConnectException e) {
            System.err.println("接続エラー: サーバーに接続できません - " + e.getMessage());
        } catch (SocketTimeoutException e) {
            System.err.println("ソケットタイムアウト: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("IOエラー: " + e.getMessage());
        } catch (InterruptedException e) {
            System.err.println("処理が中断されました: " + e.getMessage());
            Thread.currentThread().interrupt(); // 割り込み状態を復元
        } catch (Exception e) {
            System.err.println("予期しないエラー: " + e.getMessage());
        }
    }
    
    // リトライ機能付きリクエスト
    public static void retryableRequest() {
        String url = "https://api.example.com/unstable-endpoint";
        int maxRetries = 3;
        Duration baseDelay = Duration.ofMillis(500);
        
        for (int attempt = 0; attempt <= maxRetries; attempt++) {
            try {
                HttpRequest request = HttpRequest.newBuilder()
                        .uri(URI.create(url))
                        .timeout(Duration.ofSeconds(10))
                        .GET()
                        .build();
                
                HttpResponse<String> response = client.send(request, 
                        HttpResponse.BodyHandlers.ofString());
                
                if (response.statusCode() == 200) {
                    System.out.println("リトライ成功 (試行 " + (attempt + 1) + "): " + response.body());
                    return; // 成功時は終了
                } else if (isRetryableStatus(response.statusCode()) && attempt < maxRetries) {
                    System.out.println("試行 " + (attempt + 1) + " 失敗 (ステータス: " + 
                            response.statusCode() + "). 再試行します...");
                } else {
                    System.err.println("最終的に失敗 (ステータス: " + response.statusCode() + ")");
                    return;
                }
                
            } catch (Exception e) {
                if (attempt < maxRetries && isRetryableException(e)) {
                    System.out.println("試行 " + (attempt + 1) + " で例外発生: " + e.getMessage() + ". 再試行します...");
                } else {
                    System.err.println("最終的に失敗: " + e.getMessage());
                    return;
                }
            }
            
            // 指数バックオフでの待機
            if (attempt < maxRetries) {
                try {
                    Duration delay = baseDelay.multipliedBy((long) Math.pow(2, attempt));
                    // ジッターを追加してサーバー負荷を分散
                    long jitter = ThreadLocalRandom.current().nextLong(0, delay.toMillis() / 4);
                    Thread.sleep(delay.toMillis() + jitter);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    System.err.println("リトライ待機中に中断されました");
                    return;
                }
            }
        }
    }
    
    private static boolean isRetryableStatus(int statusCode) {
        return statusCode == 429 || // Too Many Requests
               statusCode == 500 || // Internal Server Error
               statusCode == 502 || // Bad Gateway
               statusCode == 503 || // Service Unavailable
               statusCode == 504;   // Gateway Timeout
    }
    
    private static boolean isRetryableException(Exception e) {
        return e instanceof HttpTimeoutException ||
               e instanceof HttpConnectTimeoutException ||
               e instanceof ConnectException ||
               e instanceof SocketTimeoutException;
    }
    
    // 非同期エラーハンドリング
    public static void asyncErrorHandling() {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/async-data"))
                .timeout(Duration.ofSeconds(30))
                .GET()
                .build();
        
        CompletableFuture<String> result = client
                .sendAsync(request, HttpResponse.BodyHandlers.ofString())
                .thenApply(response -> {
                    if (response.statusCode() == 200) {
                        return response.body();
                    } else {
                        throw new RuntimeException("HTTP Error: " + response.statusCode());
                    }
                })
                .exceptionally(throwable -> {
                    if (throwable.getCause() instanceof HttpTimeoutException) {
                        System.err.println("非同期リクエストタイムアウト");
                        return "TIMEOUT_ERROR";
                    } else if (throwable.getCause() instanceof ConnectException) {
                        System.err.println("非同期接続エラー");
                        return "CONNECTION_ERROR";
                    } else {
                        System.err.println("非同期処理エラー: " + throwable.getMessage());
                        return "UNKNOWN_ERROR";
                    }
                })
                .orTimeout(Duration.ofSeconds(45)); // 全体タイムアウト
        
        try {
            String responseBody = result.get();
            System.out.println("非同期結果: " + responseBody);
        } catch (Exception e) {
            System.err.println("非同期処理例外: " + e.getMessage());
        }
    }
    
    // シンプルなサーキットブレーカーパターン
    static class SimpleCircuitBreaker {
        private int failureCount = 0;
        private long lastFailureTime = 0;
        private final int threshold;
        private final Duration timeout;
        private boolean isOpen = false;
        
        public SimpleCircuitBreaker(int threshold, Duration timeout) {
            this.threshold = threshold;
            this.timeout = timeout;
        }
        
        public boolean canExecute() {
            if (!isOpen) {
                return true;
            }
            
            // タイムアウト後にリセット試行
            if (System.currentTimeMillis() - lastFailureTime > timeout.toMillis()) {
                isOpen = false;
                failureCount = 0;
                return true;
            }
            
            return false;
        }
        
        public void recordSuccess() {
            failureCount = 0;
            isOpen = false;
        }
        
        public void recordFailure() {
            failureCount++;
            lastFailureTime = System.currentTimeMillis();
            
            if (failureCount >= threshold) {
                isOpen = true;
            }
        }
        
        public boolean isOpen() {
            return isOpen;
        }
    }
    
    public static void circuitBreakerExample() {
        SimpleCircuitBreaker circuitBreaker = new SimpleCircuitBreaker(3, Duration.ofMinutes(1));
        
        for (int i = 0; i < 10; i++) {
            if (!circuitBreaker.canExecute()) {
                System.out.println("サーキットブレーカーが開いています。リクエストをスキップ。");
                continue;
            }
            
            try {
                HttpRequest request = HttpRequest.newBuilder()
                        .uri(URI.create("https://api.example.com/unreliable"))
                        .timeout(Duration.ofSeconds(5))
                        .GET()
                        .build();
                
                HttpResponse<String> response = client.send(request, 
                        HttpResponse.BodyHandlers.ofString());
                
                if (response.statusCode() == 200) {
                    circuitBreaker.recordSuccess();
                    System.out.println("成功: " + response.body());
                } else {
                    circuitBreaker.recordFailure();
                    System.out.println("失敗 (ステータス: " + response.statusCode() + ")");
                }
                
            } catch (Exception e) {
                circuitBreaker.recordFailure();
                System.err.println("例外発生: " + e.getMessage());
            }
            
            // 次のリクエストまで少し待機
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }
}

ファイルアップロード・ダウンロードとストリーミング

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Flow;

public class FileStreamingHttpClientExample {
    
    private static final HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30))
            .build();
    
    public static void main(String[] args) throws Exception {
        // ファイルアップロード
        fileUploadExample();
        
        // ファイルダウンロード
        fileDownloadExample();
        
        // ストリーミングアップロード
        streamingUploadExample();
        
        // ストリーミングダウンロード
        streamingDownloadExample();
    }
    
    // ファイルアップロード
    public static void fileUploadExample() throws Exception {
        Path filePath = Path.of("upload-file.txt");
        
        // アップロード用ファイルの作成
        Files.writeString(filePath, "これはアップロードテスト用のファイルです。\nMultiple lines content...");
        
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://httpbin.org/post"))
                .header("Content-Type", "text/plain; charset=UTF-8")
                .header("Authorization", "Bearer your-token")
                .POST(HttpRequest.BodyPublishers.ofFile(filePath))
                .timeout(Duration.ofMinutes(5))
                .build();
        
        HttpResponse<String> response = client.send(request, 
                HttpResponse.BodyHandlers.ofString());
        
        System.out.println("ファイルアップロード ステータス: " + response.statusCode());
        System.out.println("レスポンス: " + response.body());
        
        // マルチパート形式でのファイルアップロード
        multipartFileUpload();
    }
    
    // マルチパート形式でのファイルアップロード
    public static void multipartFileUpload() throws Exception {
        Path filePath = Path.of("multipart-file.txt");
        Files.writeString(filePath, "マルチパートアップロードテストファイル");
        
        String boundary = "----WebKitFormBoundary" + System.currentTimeMillis();
        
        String multipartBody = "--" + boundary + "\r\n" +
                "Content-Disposition: form-data; name=\"file\"; filename=\"test.txt\"\r\n" +
                "Content-Type: text/plain\r\n\r\n" +
                Files.readString(filePath) + "\r\n" +
                "--" + boundary + "\r\n" +
                "Content-Disposition: form-data; name=\"description\"\r\n\r\n" +
                "テストファイルの説明\r\n" +
                "--" + boundary + "--\r\n";
        
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://httpbin.org/post"))
                .header("Content-Type", "multipart/form-data; boundary=" + boundary)
                .POST(HttpRequest.BodyPublishers.ofString(multipartBody))
                .build();
        
        HttpResponse<String> response = client.send(request, 
                HttpResponse.BodyHandlers.ofString());
        
        System.out.println("マルチパートアップロード ステータス: " + response.statusCode());
    }
    
    // ファイルダウンロード
    public static void fileDownloadExample() throws Exception {
        Path downloadPath = Path.of("downloaded-file.txt");
        
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://httpbin.org/bytes/1024"))
                .header("Accept", "*/*")
                .GET()
                .build();
        
        // ファイルに直接ダウンロード
        HttpResponse<Path> response = client.send(request, 
                HttpResponse.BodyHandlers.ofFile(downloadPath));
        
        System.out.println("ダウンロード ステータス: " + response.statusCode());
        System.out.println("ファイル保存先: " + response.body());
        System.out.println("ファイルサイズ: " + Files.size(downloadPath) + " bytes");
        
        // 非同期ダウンロード
        asyncFileDownload();
    }
    
    // 非同期ファイルダウンロード
    public static void asyncFileDownload() throws Exception {
        Path asyncDownloadPath = Path.of("async-downloaded-file.dat");
        
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://httpbin.org/bytes/2048"))
                .GET()
                .build();
        
        CompletableFuture<Path> downloadFuture = client
                .sendAsync(request, HttpResponse.BodyHandlers.ofFile(asyncDownloadPath))
                .thenApply(HttpResponse::body);
        
        // ダウンロード完了を待機
        Path downloadedFile = downloadFuture.get();
        System.out.println("非同期ダウンロード完了: " + downloadedFile);
        System.out.println("ファイルサイズ: " + Files.size(downloadedFile) + " bytes");
    }
    
    // ストリーミングアップロード
    public static void streamingUploadExample() throws Exception {
        // カスタムBodyPublisher for streaming
        HttpRequest.BodyPublisher streamingPublisher = new HttpRequest.BodyPublisher() {
            @Override
            public long contentLength() {
                return -1; // 不明なサイズ
            }
            
            @Override
            public void subscribe(Flow.Subscriber<? super java.nio.ByteBuffer> subscriber) {
                // ストリーミングデータの送信ロジック
                subscriber.onSubscribe(new Flow.Subscription() {
                    private boolean cancelled = false;
                    private int sentChunks = 0;
                    
                    @Override
                    public void request(long n) {
                        if (cancelled) return;
                        
                        try {
                            for (int i = 0; i < n && sentChunks < 5; i++, sentChunks++) {
                                String chunk = "Streaming chunk " + sentChunks + "\n";
                                subscriber.onNext(java.nio.ByteBuffer.wrap(chunk.getBytes()));
                            }
                            
                            if (sentChunks >= 5) {
                                subscriber.onComplete();
                            }
                        } catch (Exception e) {
                            subscriber.onError(e);
                        }
                    }
                    
                    @Override
                    public void cancel() {
                        cancelled = true;
                    }
                });
            }
        };
        
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://httpbin.org/post"))
                .header("Content-Type", "text/plain")
                .header("Transfer-Encoding", "chunked")
                .POST(streamingPublisher)
                .build();
        
        HttpResponse<String> response = client.send(request, 
                HttpResponse.BodyHandlers.ofString());
        
        System.out.println("ストリーミングアップロード ステータス: " + response.statusCode());
    }
    
    // ストリーミングダウンロード
    public static void streamingDownloadExample() throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://httpbin.org/stream/5"))
                .GET()
                .build();
        
        // カスタムBodyHandlerでストリーミング処理
        HttpResponse<Void> response = client.send(request, 
                HttpResponse.BodyHandlers.fromSubscriber(new Flow.Subscriber<java.util.List<java.nio.ByteBuffer>>() {
                    private Flow.Subscription subscription;
                    private int receivedChunks = 0;
                    
                    @Override
                    public void onSubscribe(Flow.Subscription subscription) {
                        this.subscription = subscription;
                        subscription.request(1); // 最初のチャンクを要求
                    }
                    
                    @Override
                    public void onNext(java.util.List<java.nio.ByteBuffer> buffers) {
                        receivedChunks++;
                        int totalBytes = buffers.stream().mapToInt(java.nio.ByteBuffer::remaining).sum();
                        
                        System.out.println("チャンク " + receivedChunks + " 受信: " + totalBytes + " bytes");
                        
                        // データ処理(この例では内容を出力)
                        buffers.forEach(buffer -> {
                            byte[] bytes = new byte[buffer.remaining()];
                            buffer.get(bytes);
                            System.out.print(new String(bytes));
                        });
                        
                        subscription.request(1); // 次のチャンクを要求
                    }
                    
                    @Override
                    public void onError(Throwable throwable) {
                        System.err.println("ストリーミングエラー: " + throwable.getMessage());
                    }
                    
                    @Override
                    public void onComplete() {
                        System.out.println("\nストリーミング完了 (総チャンク数: " + receivedChunks + ")");
                    }
                }));
        
        System.out.println("ストリーミングダウンロード ステータス: " + response.statusCode());
    }
    
    // 進捗表示付きダウンロード
    public static void downloadWithProgress() throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://httpbin.org/bytes/10240")) // 10KB
                .GET()
                .build();
        
        HttpResponse<InputStream> response = client.send(request, 
                HttpResponse.BodyHandlers.ofInputStream());
        
        if (response.statusCode() == 200) {
            Path outputPath = Path.of("progress-download.dat");
            
            try (InputStream inputStream = response.body()) {
                long totalSize = response.headers().firstValueAsLong("Content-Length").orElse(-1L);
                long downloadedBytes = 0;
                byte[] buffer = new byte[1024];
                int bytesRead;
                
                Files.deleteIfExists(outputPath);
                
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    Files.write(outputPath, buffer, 0, bytesRead, 
                            StandardOpenOption.CREATE, StandardOpenOption.APPEND);
                    downloadedBytes += bytesRead;
                    
                    if (totalSize > 0) {
                        double progress = (double) downloadedBytes / totalSize * 100;
                        System.out.printf("\rダウンロード進捗: %.1f%% (%d/%d bytes)", 
                                progress, downloadedBytes, totalSize);
                    } else {
                        System.out.printf("\rダウンロード: %d bytes", downloadedBytes);
                    }
                }
                
                System.out.println("\nダウンロード完了: " + outputPath);
            }
        }
    }
}