Richieste HTTP in Java

Java offre diversi strumenti per effettuare richieste HTTP, dalle API storiche incluse nel JDK fino a librerie di terze parti ampiamente adottate nell'ecosistema enterprise. In questo articolo analizziamo le principali soluzioni disponibili, illustrandone il funzionamento con esempi pratici e completi.

HttpURLConnection

HttpURLConnection è la classe storica del JDK per effettuare richieste HTTP. Fa parte del package java.net ed è disponibile fin dalle prime versioni di Java. Pur essendo verbosa rispetto alle API moderne, è ancora largamente utilizzata in ambienti dove non è possibile introdurre dipendenze esterne.

Il seguente esempio mostra come effettuare una richiesta GET e leggere la risposta:

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class HttpGetExample {

    // Esegue una richiesta GET all'URL specificato e restituisce il corpo della risposta
    public static String sendGetRequest(String targetUrl) throws Exception {
        URL url = new URL(targetUrl);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        // Imposta il metodo HTTP
        connection.setRequestMethod("GET");

        // Imposta gli header della richiesta
        connection.setRequestProperty("Accept", "application/json");
        connection.setRequestProperty("User-Agent", "Java-HttpClient/1.0");

        // Legge il codice di stato della risposta
        int statusCode = connection.getResponseCode();

        // Legge il corpo della risposta riga per riga
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(connection.getInputStream())
        );

        StringBuilder responseBody = new StringBuilder();
        String line;

        while ((line = reader.readLine()) != null) {
            responseBody.append(line);
        }

        reader.close();
        connection.disconnect();

        return responseBody.toString();
    }

    public static void main(String[] args) throws Exception {
        String response = sendGetRequest("https://jsonplaceholder.typicode.com/posts/1");
        System.out.println(response);
    }
}

Per inviare dati con una richiesta POST è necessario abilitare l'output sulla connessione tramite setDoOutput(true) e scrivere il payload sullo stream di uscita:

import java.io.DataOutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

public class HttpPostExample {

    // Invia una richiesta POST con un corpo JSON
    public static String sendPostRequest(String targetUrl, String jsonBody) throws Exception {
        URL url = new URL(targetUrl);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        // Configura la connessione per il metodo POST
        connection.setRequestMethod("POST");
        connection.setDoOutput(true);

        // Imposta gli header necessari per JSON
        connection.setRequestProperty("Content-Type", "application/json");
        connection.setRequestProperty("Accept", "application/json");

        // Scrive il payload sul flusso di output
        byte[] payload = jsonBody.getBytes(StandardCharsets.UTF_8);
        connection.setRequestProperty("Content-Length", String.valueOf(payload.length));

        try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) {
            outputStream.write(payload);
            outputStream.flush();
        }

        // Legge la risposta del server
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(connection.getInputStream())
        );

        StringBuilder responseBody = new StringBuilder();
        String line;

        while ((line = reader.readLine()) != null) {
            responseBody.append(line);
        }

        reader.close();
        connection.disconnect();

        return responseBody.toString();
    }

    public static void main(String[] args) throws Exception {
        String json = "{\"title\":\"Test\",\"body\":\"Contenuto del post\",\"userId\":1}";
        String response = sendPostRequest("https://jsonplaceholder.typicode.com/posts", json);
        System.out.println(response);
    }
}

HttpClient (Java 11+)

A partire da Java 11 è disponibile il nuovo HttpClient, introdotto nel package java.net.http. Questa API è moderna, immutabile, supporta HTTP/2 e le chiamate asincrone tramite CompletableFuture. Rappresenta lo standard consigliato per i progetti che non necessitano di compatibilità con versioni precedenti del JDK.

Vediamo prima una richiesta GET sincrona:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class ModernHttpGetExample {

    public static void main(String[] args) throws Exception {
        // Crea un'istanza del client HTTP riutilizzabile
        HttpClient client = HttpClient.newHttpClient();

        // Costruisce la richiesta GET
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
            .header("Accept", "application/json")
            .GET()
            .build();

        // Invia la richiesta e riceve la risposta come stringa
        HttpResponse<String> response = client.send(
            request,
            HttpResponse.BodyHandlers.ofString()
        );

        // Stampa il codice di stato e il corpo della risposta
        System.out.println("Codice di stato: " + response.statusCode());
        System.out.println("Corpo: " + response.body());
    }
}

La versione asincrona della stessa richiesta utilizza sendAsync, che restituisce un 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;

public class AsyncHttpGetExample {

    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
            .header("Accept", "application/json")
            .GET()
            .build();

        // Avvia la richiesta in modo asincrono
        CompletableFuture<HttpResponse<String>> futureResponse = client.sendAsync(
            request,
            HttpResponse.BodyHandlers.ofString()
        );

        // Elabora la risposta quando disponibile
        futureResponse
            .thenApply(HttpResponse::body)
            .thenAccept(body -> System.out.println("Risposta ricevuta: " + body))
            .join(); // Attende il completamento prima di terminare il programma
    }
}

Una richiesta POST con HttpClient è significativamente più leggibile rispetto alla versione con HttpURLConnection:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;

public class ModernHttpPostExample {

    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();

        String jsonBody = "{\"title\":\"Nuovo post\",\"body\":\"Testo del post\",\"userId\":1}";

        // Costruisce la richiesta POST con il corpo JSON
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://jsonplaceholder.typicode.com/posts"))
            .header("Content-Type", "application/json")
            .header("Accept", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
            .build();

        HttpResponse<String> response = client.send(
            request,
            HttpResponse.BodyHandlers.ofString()
        );

        System.out.println("Codice di stato: " + response.statusCode());
        System.out.println("Corpo: " + response.body());
    }
}

Timeout e gestione degli errori

In un contesto di produzione è fondamentale configurare timeout appropriati e gestire correttamente i codici di errore HTTP. Il nuovo HttpClient permette di impostare sia un timeout globale sul client sia uno specifico per ogni singola richiesta:

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 HttpWithTimeoutExample {

    // Crea un client con timeout di connessione globale
    private static final HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(5))
        .build();

    public static String fetchWithTimeout(String targetUrl) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(targetUrl))
            .timeout(Duration.ofSeconds(10)) // Timeout specifico per questa richiesta
            .GET()
            .build();

        HttpResponse<String> response = client.send(
            request,
            HttpResponse.BodyHandlers.ofString()
        );

        // Verifica il codice di stato prima di restituire il corpo
        int statusCode = response.statusCode();

        if (statusCode >= 200 && statusCode < 300) {
            return response.body();
        } else if (statusCode >= 400 && statusCode < 500) {
            throw new RuntimeException("Errore del client: " + statusCode);
        } else if (statusCode >= 500) {
            throw new RuntimeException("Errore del server: " + statusCode);
        }

        return response.body();
    }

    public static void main(String[] args) {
        try {
            String result = fetchWithTimeout("https://jsonplaceholder.typicode.com/posts/1");
            System.out.println(result);
        } catch (Exception e) {
            System.err.println("Richiesta fallita: " + e.getMessage());
        }
    }
}

Richieste parallele con CompletableFuture

Un caso d'uso comune nelle applicazioni moderne è l'invio di più richieste HTTP in parallelo per ridurre il tempo complessivo di attesa. Il nuovo HttpClient si integra nativamente con CompletableFuture per questo scopo:

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

public class ParallelRequestsExample {

    private static final HttpClient client = HttpClient.newHttpClient();

    // Invia richieste GET in parallelo a tutti gli URL forniti
    public static List<String> fetchAll(List<String> urls) {
        List<CompletableFuture<String>> futures = urls.stream()
            .map(url -> {
                HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .GET()
                    .build();

                // Ogni richiesta viene inviata in modo asincrono
                return client
                    .sendAsync(request, HttpResponse.BodyHandlers.ofString())
                    .thenApply(HttpResponse::body);
            })
            .collect(Collectors.toList());

        // Attende il completamento di tutte le richieste
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

        // Raccoglie e restituisce tutti i risultati
        return futures.stream()
            .map(CompletableFuture::join)
            .collect(Collectors.toList());
    }

    public static void main(String[] args) {
        List<String> urls = List.of(
            "https://jsonplaceholder.typicode.com/posts/1",
            "https://jsonplaceholder.typicode.com/posts/2",
            "https://jsonplaceholder.typicode.com/posts/3"
        );

        List<String> results = fetchAll(urls);
        results.forEach(body -> System.out.println(body + "\n---"));
    }
}

Autenticazione HTTP Basic e Bearer Token

Molte API REST richiedono autenticazione. I due schemi più diffusi sono HTTP Basic Authentication e Bearer Token. Entrambi si implementano tramite l'header Authorization:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;

public class HttpAuthExample {

    private static final HttpClient client = HttpClient.newHttpClient();

    // Richiesta con autenticazione Basic (username e password codificati in Base64)
    public static String fetchWithBasicAuth(String targetUrl, String username, String password)
        throws Exception {

        String credentials = username + ":" + password;
        String encodedCredentials = Base64.getEncoder()
            .encodeToString(credentials.getBytes());

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(targetUrl))
            .header("Authorization", "Basic " + encodedCredentials)
            .GET()
            .build();

        return client.send(request, HttpResponse.BodyHandlers.ofString()).body();
    }

    // Richiesta con autenticazione Bearer Token (tipica delle API OAuth2)
    public static String fetchWithBearerToken(String targetUrl, String token)
        throws Exception {

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(targetUrl))
            .header("Authorization", "Bearer " + token)
            .GET()
            .build();

        return client.send(request, HttpResponse.BodyHandlers.ofString()).body();
    }

    public static void main(String[] args) throws Exception {
        // Esempio con Bearer Token
        String response = fetchWithBearerToken(
            "https://api.example.com/protected-resource",
            "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
        );
        System.out.println(response);
    }
}

Invio di form data e multipart

Alcune API accettano dati in formato application/x-www-form-urlencoded oppure multipart/form-data (tipicamente per l'upload di file). Con il nuovo HttpClient il primo caso è semplice, mentre per il multipart è necessario costruire il boundary manualmente o affidarsi a una libreria:

import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.stream.Collectors;

public class FormDataExample {

    private static final HttpClient client = HttpClient.newHttpClient();

    // Codifica una mappa di parametri nel formato application/x-www-form-urlencoded
    private static String encodeFormData(Map<String, String> params) {
        return params.entrySet().stream()
            .map(entry ->
                URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)
                + "="
                + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)
            )
            .collect(Collectors.joining("&"));
    }

    public static String sendFormPost(String targetUrl, Map<String, String> formParams)
        throws Exception {

        String formBody = encodeFormData(formParams);

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(targetUrl))
            .header("Content-Type", "application/x-www-form-urlencoded")
            .POST(HttpRequest.BodyPublishers.ofString(formBody))
            .build();

        return client.send(request, HttpResponse.BodyHandlers.ofString()).body();
    }

    public static void main(String[] args) throws Exception {
        Map<String, String> fields = Map.of(
            "username", "gabriele",
            "password", "segreto123"
        );

        String response = sendFormPost("https://httpbin.org/post", fields);
        System.out.println(response);
    }
}

Utilizzo di OkHttp

OkHttp è una libreria open source sviluppata da Square, molto diffusa nell'ecosistema Java e Android. Offre un'API fluente, supporto trasparente per HTTP/2, connection pooling, retry automatico e interceptor per la personalizzazione delle richieste. Per includerla in un progetto Maven è sufficiente aggiungere la seguente dipendenza al file pom.xml:

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.12.0</version>
</dependency>

Ecco come effettuare una richiesta GET e una POST con OkHttp:

import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

public class OkHttpExample {

    // Tipo di contenuto JSON riutilizzabile
    private static final MediaType JSON_TYPE = MediaType.get("application/json");

    // Client OkHttp condiviso (thread-safe, da riutilizzare)
    private static final OkHttpClient client = new OkHttpClient();

    // Invia una richiesta GET e restituisce il corpo della risposta
    public static String get(String targetUrl) throws Exception {
        Request request = new Request.Builder()
            .url(targetUrl)
            .addHeader("Accept", "application/json")
            .build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new RuntimeException("Codice di stato inatteso: " + response.code());
            }
            return response.body().string();
        }
    }

    // Invia una richiesta POST con un corpo JSON
    public static String post(String targetUrl, String jsonBody) throws Exception {
        RequestBody body = RequestBody.create(jsonBody, JSON_TYPE);

        Request request = new Request.Builder()
            .url(targetUrl)
            .post(body)
            .addHeader("Accept", "application/json")
            .build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new RuntimeException("Codice di stato inatteso: " + response.code());
            }
            return response.body().string();
        }
    }

    public static void main(String[] args) throws Exception {
        // Esempio GET
        String getResponse = get("https://jsonplaceholder.typicode.com/posts/1");
        System.out.println("GET: " + getResponse);

        // Esempio POST
        String postResponse = post(
            "https://jsonplaceholder.typicode.com/posts",
            "{\"title\":\"Test\",\"body\":\"Testo\",\"userId\":1}"
        );
        System.out.println("POST: " + postResponse);
    }
}

Un punto di forza di OkHttp è il sistema degli interceptor, che consente di aggiungere comportamenti trasversali come logging, autenticazione automatica e retry:

import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class OkHttpInterceptorExample {

    // Interceptor per aggiungere automaticamente il token di autenticazione
    static class AuthInterceptor implements Interceptor {

        private final String token;

        AuthInterceptor(String token) {
            this.token = token;
        }

        @Override
        public Response intercept(Chain chain) throws IOException {
            // Aggiunge l'header Authorization a ogni richiesta
            Request originalRequest = chain.request();
            Request authenticatedRequest = originalRequest.newBuilder()
                .header("Authorization", "Bearer " + token)
                .build();
            return chain.proceed(authenticatedRequest);
        }
    }

    // Interceptor per il logging delle richieste e risposte
    static class LoggingInterceptor implements Interceptor {

        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            long startTime = System.currentTimeMillis();

            System.out.println("Invio richiesta: " + request.url());

            Response response = chain.proceed(request);

            long duration = System.currentTimeMillis() - startTime;
            System.out.println("Risposta ricevuta in " + duration + " ms: " + response.code());

            return response;
        }
    }

    public static OkHttpClient buildClient(String authToken) {
        return new OkHttpClient.Builder()
            .connectTimeout(5, TimeUnit.SECONDS)
            .readTimeout(10, TimeUnit.SECONDS)
            .addInterceptor(new AuthInterceptor(authToken))
            .addInterceptor(new LoggingInterceptor())
            .build();
    }
}

Utilizzo di Apache HttpClient

Apache HttpClient è una delle librerie HTTP più mature e complete per Java, parte del progetto Apache HttpComponents. È particolarmente adatta in ambienti enterprise per la sua configurabilità avanzata. La dipendenza Maven per la versione 5.x è la seguente:

<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <version>5.3.1</version>
</dependency>

Un esempio di richiesta GET e POST con Apache HttpClient 5:

import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import java.nio.charset.StandardCharsets;

public class ApacheHttpClientExample {

    public static void main(String[] args) throws Exception {
        // Il client è AutoCloseable: usarlo in un try-with-resources
        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {

            // Richiesta GET
            HttpGet getRequest = new HttpGet("https://jsonplaceholder.typicode.com/posts/1");
            getRequest.addHeader("Accept", "application/json");

            String getResponse = httpClient.execute(getRequest, (ClassicHttpResponse response) -> {
                // Converte il corpo della risposta in stringa
                return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
            });

            System.out.println("GET: " + getResponse);

            // Richiesta POST con corpo JSON
            HttpPost postRequest = new HttpPost("https://jsonplaceholder.typicode.com/posts");
            postRequest.addHeader("Content-Type", "application/json");
            postRequest.addHeader("Accept", "application/json");

            String jsonBody = "{\"title\":\"Post di prova\",\"body\":\"Corpo\",\"userId\":1}";
            postRequest.setEntity(new StringEntity(jsonBody, StandardCharsets.UTF_8));

            String postResponse = httpClient.execute(postRequest, (ClassicHttpResponse response) -> {
                return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
            });

            System.out.println("POST: " + postResponse);
        }
    }
}

Gestione dei cookie

La gestione dei cookie è necessaria quando si interagisce con applicazioni web che mantengono sessioni lato server. Il nuovo HttpClient del JDK supporta la gestione automatica dei cookie tramite un CookieHandler:

import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class CookieHandlingExample {

    public static void main(String[] args) throws Exception {
        // Configura il gestore dei cookie per accettare tutti i cookie
        CookieManager cookieManager = new CookieManager(null, CookiePolicy.ACCEPT_ALL);

        HttpClient client = HttpClient.newBuilder()
            .cookieHandler(cookieManager)
            .build();

        // Prima richiesta: il server imposta un cookie di sessione
        HttpRequest loginRequest = HttpRequest.newBuilder()
            .uri(URI.create("https://example.com/login"))
            .POST(HttpRequest.BodyPublishers.ofString("user=admin&pass=secret"))
            .header("Content-Type", "application/x-www-form-urlencoded")
            .build();

        client.send(loginRequest, HttpResponse.BodyHandlers.ofString());

        // Seconda richiesta: il cookie viene inviato automaticamente
        HttpRequest profileRequest = HttpRequest.newBuilder()
            .uri(URI.create("https://example.com/profile"))
            .GET()
            .build();

        HttpResponse<String> profileResponse = client.send(
            profileRequest,
            HttpResponse.BodyHandlers.ofString()
        );

        System.out.println("Profilo: " + profileResponse.body());
        System.out.println("Cookie memorizzati: " + cookieManager.getCookieStore().getCookies());
    }
}

Redirect automatici

Per impostazione predefinita il nuovo HttpClient non segue i redirect. Questo comportamento è configurabile tramite la policy di redirect:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class RedirectExample {

    public static void main(String[] args) throws Exception {
        // Configura il client per seguire automaticamente i redirect HTTP
        HttpClient client = HttpClient.newBuilder()
            .followRedirects(HttpClient.Redirect.ALWAYS)
            .build();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("http://httpbin.org/redirect/2")) // Restituisce 2 redirect consecutivi
            .GET()
            .build();

        HttpResponse<String> response = client.send(
            request,
            HttpResponse.BodyHandlers.ofString()
        );

        // Dopo i redirect, il codice di stato sarà 200
        System.out.println("URL finale: " + response.uri());
        System.out.println("Codice di stato: " + response.statusCode());
    }
}

Download di file

Il HttpClient del JDK include handler specializzati per salvare direttamente la risposta su disco senza caricarla interamente in memoria, operazione fondamentale per file di grandi dimensioni:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
import java.nio.file.Paths;

public class FileDownloadExample {

    public static void downloadFile(String fileUrl, String destinationPath) throws Exception {
        HttpClient client = HttpClient.newHttpClient();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(fileUrl))
            .GET()
            .build();

        Path targetPath = Paths.get(destinationPath);

        // Scrive il corpo della risposta direttamente su file
        HttpResponse<Path> response = client.send(
            request,
            HttpResponse.BodyHandlers.ofFile(targetPath)
        );

        System.out.println("File salvato in: " + response.body().toAbsolutePath());
        System.out.println("Dimensione: " + targetPath.toFile().length() + " byte");
    }

    public static void main(String[] args) throws Exception {
        downloadFile(
            "https://www.w3.org/WAI/WCAG21/wcag21.pdf",
            "/tmp/wcag21.pdf"
        );
    }
}

Confronto tra le soluzioni

La scelta dello strumento più adatto dipende dal contesto di progetto. HttpURLConnection è priva di dipendenze esterne e adatta a progetti legacy o utility minimali, ma la sua API è verbosa e poco ergonomica. Il nuovo HttpClient del JDK è la scelta consigliata per progetti su Java 11 o superiore: è moderno, supporta HTTP/2 e le chiamate asincrone senza richiedere librerie aggiuntive. OkHttp eccelle per la ricchezza di funzionalità, il sistema degli interceptor e la naturale integrazione con Android. Apache HttpClient è la scelta preferenziale in ambienti enterprise complessi dove è richiesta una configurazione avanzata di pool, proxy e SSL.

In tutti i casi è buona pratica istanziare il client una sola volta e riutilizzarlo per l'intera vita dell'applicazione, poiché la creazione di un nuovo client ad ogni richiesta è costosa in termini di risorse e annulla i benefici del connection pooling.

Conclusioni

Java offre un ecosistema maturo e variegato per la gestione delle richieste HTTP. Il nuovo HttpClient introdotto in Java 11 copre la maggior parte dei casi d'uso con un'API pulita e moderna, eliminando nella maggior parte dei scenari la necessità di librerie esterne. Per esigenze specifiche come interceptor avanzati, Android, o configurazioni enterprise particolari, OkHttp e Apache HttpClient rimangono scelte solide e ben mantenute.