Pobieranie danych z API przez HttpClient
- WWW i API
- poziom: średni
- aktualizacja: 15 sierpnia 2026
Od Javy 11 nie potrzebujesz Apache HttpClient ani OkHttp — klasa java.net.http.HttpClient z biblioteki standardowej wystarcza do zwykłej integracji z API. Największa pułapka: metoda send() nie rzuca wyjątku, gdy serwer odpowie kodem 404 albo 500 — dostajesz zwyczajną odpowiedź i musisz sam sprawdzić statusCode(). Druga to brak limitów czasu: klient bez connectTimeout() i timeout() potrafi czekać w nieskończoność.
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.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Klient REST API oparty na java.net.http.HttpClient — skryptyJAVA.pl
* Zapisz jako: KlientApi.java
*/
public class KlientApi {
private static final String PRZEGLADARKA = "skryptyJAVA-KlientApi/1.0";
private static final Duration LIMIT_ZADANIA = Duration.ofSeconds(15);
/** Klienta buduj RAZ — trzyma pulę połączeń, więc nie twórz go przy każdym żądaniu. */
private static final HttpClient KLIENT = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(10)) // limit na nawiązanie połączenia
.followRedirects(HttpClient.Redirect.NORMAL) // podążaj za 301/302, ale nie z HTTPS na HTTP
.build();
/** Błąd zgłaszany, gdy serwer odpowie kodem spoza zakresu 2xx. Pole kod to np. 404 albo 500. */
public static class BladApi extends RuntimeException {
private static final long serialVersionUID = 1L;
public final int kod;
public BladApi(int kod, String tresc) {
super("HTTP " + kod + " — " + (tresc.length() > 200 ? tresc.substring(0, 200) + "..." : tresc));
this.kod = kod;
}
}
/**
* Wysyła żądanie GET i zwraca ciało odpowiedzi jako tekst.
*
* @param adres pełny adres URL zasobu
* @throws BladApi gdy kod odpowiedzi nie mieści się w zakresie 2xx, np. 404 albo 500
*/
public static String pobierz(String adres) throws IOException, InterruptedException {
return sprawdz(KLIENT.send(szkielet(adres).GET().build(), HttpResponse.BodyHandlers.ofString()));
}
/** Wysyła POST z gotowym dokumentem JSON i zwraca odpowiedź serwera jako tekst. */
public static String wyslijJson(String adres, String json) throws IOException, InterruptedException {
HttpRequest zadanie = szkielet(adres)
.header("Content-Type", "application/json; charset=UTF-8")
.POST(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8))
.build();
return sprawdz(KLIENT.send(zadanie, HttpResponse.BodyHandlers.ofString()));
}
/** Pobiera zasób strumieniowo i podmienia cel dopiero po poprawnej odpowiedzi 2xx. */
public static Path pobierzPlik(String adres, Path cel) throws IOException, InterruptedException {
Path wynik = cel.toAbsolutePath().normalize();
Path katalog = wynik.getParent();
if (katalog == null) { throw new IOException("Plik docelowy musi mieć katalog nadrzędny."); }
Files.createDirectories(katalog);
HttpRequest zadanie = szkielet(adres).timeout(Duration.ofMinutes(5)).GET().build();
HttpResponse<InputStream> odpowiedz = KLIENT.send(
zadanie, HttpResponse.BodyHandlers.ofInputStream());
try (InputStream body = odpowiedz.body()) {
if (odpowiedz.statusCode() < 200 || odpowiedz.statusCode() > 299) {
String tresc = new String(body.readNBytes(2048), StandardCharsets.UTF_8);
throw new BladApi(odpowiedz.statusCode(), tresc);
}
Path tymczasowy = Files.createTempFile(katalog, "pobieranie-", ".part");
try {
Files.copy(body, tymczasowy, StandardCopyOption.REPLACE_EXISTING);
Files.move(tymczasowy, wynik, StandardCopyOption.REPLACE_EXISTING);
} finally {
Files.deleteIfExists(tymczasowy);
}
}
return wynik;
}
/** Asynchroniczne GET — nie blokuje wątku, wynik odbierasz przez CompletableFuture. */
public static CompletableFuture<String> pobierzAsync(String adres) {
return KLIENT.sendAsync(szkielet(adres).GET().build(), HttpResponse.BodyHandlers.ofString())
.thenApply(KlientApi::sprawdz);
}
/** Wyciąga wartość prostego pola z płaskiego JSON-a bez biblioteki. Zwraca null, gdy pola nie ma. */
public static String pole(String json, String nazwa) {
String wzorzec = "\"" + Pattern.quote(nazwa)
+ "\"\\s*:\\s*(?:\"((?:\\\\.|[^\"\\\\])*)\"|([-\\d.eE+]+|true|false|null))";
Matcher dopasowanie = Pattern.compile(wzorzec).matcher(json);
if (!dopasowanie.find()) { return null; }
String tekst = dopasowanie.group(1);
return tekst != null ? tekst.replace("\\\"", "\"").replace("\\\\", "\\") : dopasowanie.group(2);
}
/** Wspólny szkielet żądania: nagłówki i limit odpowiedzi, którego zakres zależy od BodyHandler. */
private static HttpRequest.Builder szkielet(String adres) {
return HttpRequest.newBuilder(URI.create(adres))
.header("Accept", "application/json")
.header("User-Agent", PRZEGLADARKA)
.timeout(LIMIT_ZADANIA);
}
/** Przepuszcza odpowiedzi 2xx, resztę zamienia na czytelny wyjątek BladApi. */
private static String sprawdz(HttpResponse<String> odpowiedz) {
int kod = odpowiedz.statusCode();
if (kod < 200 || kod > 299) { throw new BladApi(kod, odpowiedz.body()); }
return odpowiedz.body();
}
/** Wykonuje krok demonstracji; przy błędzie sieci wypisuje komunikat zamiast przerywać program. */
private static void demo(String etap, Callable<String> krok) {
try {
System.out.println(etap + ": " + krok.call());
} catch (Exception e) {
if (e instanceof InterruptedException) { Thread.currentThread().interrupt(); }
System.out.println("[" + etap + "] nie powiodło się: "
+ (e.getMessage() != null ? e.getMessage() : e.getClass().getName()));
}
}
// ── PRZYKŁADY ───────────────────────────────────────────
public static void main(String[] args) {
System.out.println("pole: " + pole("{\"login\":\"duke\",\"id\":42}", "login")); // duke, bez sieci
demo("GET", () -> {
String json = pobierz("https://api.github.com/repos/openjdk/jdk");
return pole(json, "full_name") + ", gwiazdek: " + pole(json, "stargazers_count");
});
demo("POST", () -> pole(wyslijJson("https://httpbin.org/post", "{\"jezyk\":\"java\"}"), "data"));
demo("PLIK", () -> {
Path cel = Files.createTempDirectory("klient-api").resolve("zen.txt");
return pobierzPlik("https://api.github.com/zen", cel) + ", bajtow: " + Files.size(cel);
});
demo("ASYNC", () -> pobierzAsync("https://api.github.com/zen").get(20, TimeUnit.SECONDS));
}
}
Jak użyć tego skryptu
- Skopiuj kod i zapisz go w pliku
KlientApi.java— nazwa pliku odpowiada nazwie klasy publicznej — a potem uruchom go poleceniemjava KlientApi.java. Program odpyta publiczne API GitHuba oraz serwis httpbin.org i wypisze wyniki czterech przykładów; bez dostępu do internetu każdy z nich zgłosi tylko komunikat o błędzie, a program i tak zakończy się normalnie. Wymagana jest Java 11 lub nowsza — wcześniejsze wersje nie mają pakietujava.net.http. - W swoim projekcie wywołuj gotowe metody:
KlientApi.pobierz(adres)dla GET,KlientApi.wyslijJson(adres, json)dla POST iKlientApi.pobierzPlik(adres, sciezka), gdy zapisujesz odpowiedź na dysk. Wszystkie korzystają ze wspólnej stałejKLIENT—HttpClientjest bezpieczny wątkowo i trzyma pulę połączeń, więc jedna instancja obsłuży całą aplikację. - Gdy potrzebujesz uwierzytelnienia, dopisz nagłówek w metodzie
szkielet(...)— np..header("Authorization", "Bearer " + token). Trafi on wtedy do wszystkich żądań naraz. - Do wielu równoległych zapytań użyj
pobierzAsync(...)i połącz wyniki przezCompletableFuture.allOf(...). Pamiętaj, żeby na końcu odebrać wynik metodąget()z limitem czasu albojoin()— inaczej program zakończy się, zanim odpowiedzi dotrą.
Warto wiedzieć
- Metoda
pole(...)to prowizorka na regularnych wyrażeniach — poradzi sobie tylko z płaskim JSON-em i pierwszym pasującym polem. Do prawdziwego parsowania użyj biblioteki Jackson albo Gson; próba obsłużenia zagnieżdżonych obiektów i tablic wyrażeniem regularnym zawsze kończy się błędami. - Kody 4xx i 5xx nie są dla
HttpClientbłędem — dostajesz zwykłą odpowiedź, dlatego każda przechodzi tu przez kontrolę statusu.pobierzPlik(...)czyta z odpowiedzi błędu najwyżej 2 KiB, a udane dane zapisuje do unikalnego pliku tymczasowego i dopiero po pełnym pobraniu podmienia cel. Dzięki temu krótka odpowiedź nie zostawia końcówki starego pliku, a błąd HTTP nie niszczy jego poprzedniej zawartości. - Ustawiaj oba limity czasu:
connectTimeout()dotyczy tylko nawiązania połączenia, atimeout()pilnuje otrzymania odpowiedzi. PrzyBodyHandlers.ofString()obejmuje także pobranie tekstu, lecz przyofInputStream()kończy się po nagłówkach — kolejneread()mogą wisieć. Dla dużych plików dołóż strażnika zastoju, który zamknie strumień, gdy licznik bajtów zbyt długo nie rośnie. - Klucza API nigdy nie wpisuj na sztywno w kodzie ani nie doklejaj do adresu URL — adresy lądują w logach serwera i w historii przeglądarki, a klucz w kodzie pojedzie razem z nim do repozytorium. Czytaj go ze zmiennej środowiskowej i przekazuj w nagłówku.
Podobne skrypty
Prosty serwer HTTP bez frameworka
Mały serwer WWW na wbudowanej klasie HttpServer: routing, parametry zapytania, odpowiedzi UTF-8 oraz poprawne kody HTTP z obsługą HEAD i 405. Przykład sam uruchamia serwer lokalny, odpytuje go i zatrzymuje, więc nic nie zostaje w tle.
średni Zobacz kod → WWW i APIPobieranie pliku z paskiem postępu
Strumieniowe pobieranie dużego pliku przez HttpClient z procentowym postępem, limitem czasu na połączenie i bezpiecznym plikiem tymczasowym. Samodzielny przykład uruchamia lokalny serwer, więc działa bez internetu.
średni Zobacz kod → WWW i APIBudowanie adresów URL i kodowanie parametrów
Sklejanie adresów URL bez błędów: kodowanie kluczy i wartości przez URLEncoder, osobna metoda dla segmentów ścieżki z %20 zamiast plusa, rozbieranie query stringu na mapę oraz odczyt części adresu przez URI.
początkujący Zobacz kod →