Klient HTTP i obsługa API – część I

13.03.2020

Artykuł

Jako programista Javy prędzej czy później będziesz musiał zacząć komunikować się ze światem zewnętrznym. Spokojnie, spokojnie. Chodzi mi tylko o Twoją aplikację.

mem obrazujący związek koronawirusa na programistów - nie muszą dzięki niemu przebywać z ludźmi

W dobie wszechobecnego internetu, kiedy Twoja komórka bez sieci jest praktycznie bezużyteczna, kiedy “taksówkarz” gubi się bez Google Maps, kiedy hulajnogi zatrzymują się po utracie sygnału, kiedy wkrótce Twoja pralka będzie rozmawiać z Twoją lodówką, Twoja aplikacja również prędzej czy później będzie musiała wysyłać zapytania do innych.
Ten artykuł jest pierwszym z serii, który ma na celu pokazać Ci, w jaki sposób korzystać z API przy pomocy klienta HTTP z Javy 11. Zaczniemy od bardzo prostego zapytania, które zwróci nam prosty tekst, który wyświetlimy w konsoli. Starałem się opisać wszystkie kroki bardzo szczegółowo, kolejne części będą stopniowo trudniejsze z racji zarówno bardziej skomplikowanych zapytań, jak i mniejszej ilości wyjaśnień. Niezrozumiałe fragmenty zawsze jednak z chęcią wyjaśnię w komentarzu.

API – czym właściwie jest?

Nie będę się tutaj silił na jak najbardziej precyzyjną definicję API, bo to wymagałoby oddzielnego artykułu, który byłby dość suchy i niezbyt ciekawy. Interesujące, czym jest API, kiedy mówi o nim programista backendowy w środowisku webowym.

Tak w uproszczeniu i telegraficznym skrócie – jest to część serwera, pod który możemy wysłać nasze zapytanie (request) i dostaniemy odpowiedź (response) w postaci czystych danych, bez zbędnej otoczki. Czyli bez szaty graficznej, bez HTML itd. Z reguły to nie my – ludzie – jesteśmy odbiorcą tych danych, ale nasza aplikacja, która te dane odpowiednio przetwarza i wyświetla w formie nam przystępnej. Co więcej, celem naszego zapytania może być nie tylko otrzymanie danych, ale również ich wysłanie.

Wiele firm wystawia takie API. Google do tłumaczeń, gdzie możesz wysłać tekst w jednym języku i otrzymać w drugim. Aplikacje pogodynek korzystają z API, aby otrzymać czyste dane dotyczące pogody i wyświetlić je w postaci słoneczek i chmurek. Samochody Tesli wystawiają API, dzięki któremu możliwe jest sterowanie częścią funkcji samochodu – choć dokumentacja jest nieoficjalna. Wiele z nich jest płatne i wymaga autoryzacji, która może się na początek wydawać skomplikowana. Całe szczęście jest też wiele darmowych, które zostały zebrane i skategoryzowane na tej liście.

API nie musi wcale być wystawiane “na zewnątrz”. Dzisiejsze aplikacje internetowe są tak duże, że często dzielone są na tak zwane mikroserwisy, które porozumiewają się między sobą za pomocą swoich API w obrębie serwera. Jeżeli planujesz zacząć komercyjną pracę w większym projekcie, to nie obejdziesz się bez umiejętności obsługi tego typu zapytań.

Http Client

Podstawową klasą, z której będziemy korzystać w tym artykule i kolejnych w tej serii jest HttpClient. Dołączyła do Javy w inkubatorze w wersji 9, a jako standardowa część JDK w wersji 11. Jest ona następcą HttpURLConnection, która z racji skomplikowanego użycia przeważnie zastępowana była zewnętrznymi bibliotekami. Po przeczytaniu artykułu, polecam wyszukać sposób użycia poprzedniczki, by przekonać się jak bardzo twórcy Javy uprościli programistom życie.

Plain text – Lorem ipsum generator

W tym artykule skorzystamy z API generatora Lorem ipsum. Czym jest Lorem ipsum? W skrócie – to nic nie znaczący fragment tekstu, stosowany jako “wypełniacz” na wzorach stron internetowych, plakatów i innego rodzaju grafik. Dla nas istotne jest to, że API zwraca nam prosty tekst – “plain text”.

Lorem ipsum – dokumentacja

Od czego zaczynamy? Oczywiście – od przeczytania dokumentacji! Ta znajduje się na samym dole strony https://loripsum.net i mówi:

How to use the API
Just do a GET request on loripsum.net/api, to get some placeholder text. You can add extra parameters to specify the output you're going to get. Say, you need 10 short paragraphs with headings, use loripsum.net/api/10/short/headers.

(integer) - The number of paragraphs to generate.
short, medium, long, verylong - The average length of a paragraph.
decorate - Add bold, italic and marked text.
link - Add links.
ul - Add unordered lists.
ol - Add numbered lists.
dl - Add description lists.
bq - Add blockquotes.
code - Add code samples.
headers - Add headers.
allcaps - Use ALL CAPS.
prude - Prude version.
plaintext - Return plain text, no HTML.

Czego się z niej dowiedzieliśmy? Po pierwsze – adres strony, którą musimy odwiedzić, by dostać tylko interesujący nas tekst, bez całej otoczki strony internetowej – “loripsum.net/api”. Możemy go odwiedzić również w przeglądarce internetowej. Co się pod nim znajduje? Tekst, ale ze znacznikami HTML oznaczającymi nowy paragraf – “<p>”. Nie jest to nam do niczego potrzebne. Na całe szczęście zapytanie możemy sparametryzować.

Odwiedźmy więc przykładowy adres z parametrami podany w dokumentacji – “loripsum.net/api/10/short/headers”. Co powinniśmy dostać? 10 krótkich akapitów i nagłówki. Zgadza się – jeżeli weźmiemy pod uwagę, że nagłówki nie wliczają się do liczby akapitów. Ale takie zapytanie też nas nie interesuje. My chcemy prosty, krótki tekst bez znaczników, który łatwo wyświetlimy w konsoli. Jeden akapit nam starczy. Jaki adres wpiszemy? Po spojrzeniu do dokumentacji, możemy stworzyć coś takiego: loripsum.net/api/1/short/plaintext.

Lorem ipsum – implementacja

Najtrudniejsza część za nami, teraz czas to “tylko” zaimplementować 😉!

Boilerplate code

Na początek stwórzmy “wydmuszkę” prostej klasy Main. W polu “LOREM_URL” będziemy przechowywać interesujący nas adres API, w standardowej metodzie #main będziemy wyświetlać Stringa, którego otrzymamy z metody #getLoremIpsum. Ta ostatnia metoda będzie zawierała całą naszą implementację w miejscu komentarza, który znajduje się w poniższym kodzie.
Pisanie takiego proceduralnego kodu, gdzie wszystko odbywa się linijka po linijce, nie jest uznawane za dobrą praktykę w językach obiektowych (jakim jest Java).

Przez zwolenników czystego kodu wyśmiewane jest jako “spaghetti code”. I słusznie! W następnych artykułach z tej serii skupimy się na lepszej enkapsulacji logiki w klasach. Jeżeli na rekrutacji dostaniesz dostatecznie dużo czasu na wykonanie zadania, również nie polecałbym Ci pisać kodu w ten sposób. Tym razem jednak, z racji tego, że kod który dziś napiszemy jest dość krótki, a także by przejrzyście pokazać kolejne kroki, pozwolimy sobie na to.

Drugim grzechem, który popełnimy, jest dodanie do sygnatury metody “throws Exception”. Jedna z metod, które wykorzystamy, rzuca wyjątki i normalnie powinniśmy je odpowiednio obsłużyć. Nie zrobimy tego, gdyż nasz “spaghetti code” byłby przez to trochę mniej czytelny, czego chciałbym uniknąć. Drugim powodem jest to, by zrobić to lege artis, musiałbym się trochę rozpisać o wyjątkach, ich rodzajach oraz obsłudze, a nie jest to tematem tego artykułu. Warto jednak przyswoić sobie tę wiedzę z innych źródeł i na rekrutacji, w miarę możliwości, wykazać się nią.

public class Main {
    private static final String LOREM_URL =
                        "https://loripsum.net/api/1/short/plaintext";

    public static void main(String[] args) throws Exception {
        System.out.println(getLoremIpsum());
    }

    private static String getLoremIpsum() throws Exception {
        // todo: implement me!
        return null;
    }
}

HttpClient
Najwyższy czas stworzyć klienta HTTP! Odruchowo chciałoby się użyć konstruktora i zainicjalizować naszego HttpClient przez użycie słowa kluczowego “new”:

HttpClient client = new HttpClient();

Jest to jednak niemożliwe! Klasa ta jest klasą abstrakcyjną, więc nie możemy stworzyć jej instancji. Jednak po wpisaniu kropki po jej nazwie IDE podpowiada nam, że są dostępne dwie metody:

HttpClient.newHttpClient()
HttpClient.newBuilder()

Pierwsza z nich to statyczna metoda fabrykująca zwracająca nam klasę implementującą HttpClient posiadającego domyślną konfigurację. W skrócie – dostajemy to, co chcemy – prostego klienta HTTP. Druga metoda zwraca nam klasę budującą – buildera, który umożliwi nam dodatkową konfigurację naszego klienta, jeżeli będzie to potrzebne. Na razie domyślny klient Http nam starczy, więc używamy tej pierwszej.

HttpRequest
Przygotujmy teraz zapytanie, które wyśle nasz klient. Zapytanie to po angielsku request, więc miałoby sens, gdyby potrzebna nam klasa nazywała się HttpRequest. I tak faktycznie jest! Ach, jak przyjemnie korzystać z bibliotek, które mają dokładnie takie nazwy jakich się spodziewasz. Zabieramy się za stworzenie instancji i ponownie nie możemy zrobić tego przez konstruktor. Jakie podpowiedzi daje nam IDE?

HttpRequest.newBuilder()
HttpRequest.newBuilder(URI uri)

Nie ma .newHttpRequest? Cóż, brak domyślnego zapytania ma sens, w końcu za każdym razem jak je tworzymy, będziemy chcieli pewnie uderzać do innego adresu. Skorzystamy, więc z metody, która umożliwi nam skonfigurowanie requestu, zanim go utworzymy, dzięki wykorzystaniu wzorca projektowego budowniczy – builder. Tym razem użyjemy pierwszej, bezparametrowej metody.

HttpRequest request = HttpRequest.newBuilder();

Po napisaniu powyższego kodu IDE krzyczy, że typ się nie zgadza. Dlaczego? Metoda newBuilder zwraca nam w końcu buildera, nie HttpRequest. Musimy więc zmienić naszą deklarację na następującą:

HttpRequest.Builder builder = HttpRequest.newBuilder();

Czas teraz skonfigurować nasze zapytanie. Pierwsze co zrobimy, to dodanie adresu naszego zapytania:

builder.uri("https://loripsum.net/api/1/plaintext");

I znów IDE krzyczy, że coś jest nie tak. Tym razem z typem parametru. Kompilator chciałby URI, a my daliśmy Stringa. Jak stworzyć URI? Sprawdźmy, co nam podpowie IDE po kropce:

URI.create(String str)

Po raz kolejny autorzy popisali się nazewnictwem! Chcesz stworzyć URI? Masz metodę URI create. Jesteśmy też w stanie stworzyć ten obiekt poprzez konstruktor, ale on rzuca wyjątek typu checked, który musielibyśmy obsłużyć. A jak wspominałem wcześniej, nie będziemy tym razem zajmować się obsługą wyjątków. Dodam tylko, że po spojrzeniu w źródła możemy zobaczyć, że metoda create pod spodem korzysta z konstruktora i tylko opakowuje wyjątek w taki, którego nie musimy obsłużyć. Także wciąż istnieje ryzyko, że taki wyjątek poleci.

W ramach zasady czystego kodu – “no magic numbers” – umieściliśmy przezornie nasz adres w statycznym i finalnym polu o nazwie LOREM_URL, możemy więc go tutaj użyć.

URI loremUri = URI.create(LOREM_URL);

Pozostaje nam przekazanie tego obiektu naszemu budowniczemu:

builder.uri(loremUri);

Zanim zakończymy budowę naszego zapytania, chciałbym żebyśmy dodali jeszcze jedną konfigurację. W dokumentacji mamy wzmiankę o tym, że nasz request ma wykorzystywać metodę GET. Jest to co prawda domyślna metoda zapytania, ale chciałbym żebyśmy przyzwyczaili się do samej nazwy i nie zdziwiło nas, gdy innym razem zostaniemy poproszeni o użycie innej – jak POST, czy DELETE. Jak to zrobimy? Ponownie autorzy stanęli na wysokości zadania – metodą GET.

builder.GET();

Na tym kończymy naszą konfigurację, ale gdzie jest nasze zapytanie? Jeszcze nie powstało. W końcu nie powiedzieliśmy naszemu budowniczemu, żeby nam je zbudował. Jak to zrobimy? Pewnie się już domyślacie – budowniczy zbuduj.

HttpRequest request = builder.build();

Tym razem w końcu zwrócone zostanie nasze zapytanie. Będzie ono niemutowalne (ang. immutable), co oznacza, że nie będziemy w stanie go edytować. By uderzyć do innego adresu, albo z inną metodą – będziemy musieli stworzyć nowe. Jak teraz wygląda nasz kod?

URI uri = URI.create(LOREM_URL);
HttpRequest.Builder builder = HttpRequest.newBuilder();
builder.uri(uri);
builder.GET();
HttpRequest request = builder.build();

Dwie metody, których używamy nie mają żadnej deklaracji. Co one zwracają? Również naszego buildera. Oznacza to, że moglibyśmy to zapisać w sposób następujący:

HttpRequest.Builder builder;
builder = HttpRequest.newBuilder();
builder = builder.uri(uri);
builder = builder.GET();

Ale zaraz, skoro w jednej linii zwracamy buildera, a w następnej się do niego znów odwołujemy… To czy nie moglibyśmy zrobić tego w jednej linii? Bez średników?

HttpRequest.Builder builder = HttpRequest.newBuilder().uri(uri).GET();

Jasne! Co więcej, w ogóle nie musimy deklarować naszego budowniczego! W końcu jest on nam potrzebny jedynie tymczasowo. To czego będziemy potrzebować, to request, które zwróci nam ostatnia metoda – build. W ramach czytelności rozbijemy tylko metody na oddzielne linie:

HttpRequest request = HttpRequest.newBuilder()
                                 .uri(loremUri)
                                 .GET()
                                 .build();

Deklarujemy tylko obiekt HttpRequest, który zwraca nam ostatnia metoda w naszym kombo – build. Pozostałe metody wciąż zwracają buildera, ale jako że od razu wywołujemy na nim kolejne, nie ma potrzeby jego deklarowania. Ten rodzaj nazywany jest fluent builder i z uwagi na przejrzystość jest bardzo często wykorzystywany w nowoczesnych bibliotekach.

HttpResponse
Mamy klienta, mamy również zapytanie. Pozostało je już tylko wysłać i sprawdzić odpowiedź! Jakie metody sugeruje nam IDE dla naszego klienta? Send wydaje się sensowna:

client.send(HttpRequest request,
            HttpResponse.BodyHandler<T> responseBodyHandler)

Potrzebne są nam dwa parametry, request już mamy, ale czym jest BodyHandler?
Odpowiedź HTTP (response) składa się z trzech części: statusu (status line), nagłówków (headers) i ciała (body). BodyHandler to obiekt, który pomoże nam obsłużyć ciało odpowiedzi. My oczekujemy prostego tekstu, więc potrzebujemy takiego, który obsłuży nam Stringi. Jak go stworzyć? Poprzez statyczną metodę fabrykującą:

HttpResponse.BodyHandlers.ofString();

By zwiększyć czytelność kodu i móc skorzystać tylko z nazwy metody ofString posłużymy się importem statycznym, który dodamy ponad deklaracją klasy:

import static java.net.http.HttpResponse.BodyHandlers.ofString;

Dzięki czemu wysłanie naszego zapytania będzie wyglądało w sposób następujący:

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

Jeżeli IDE podkreśla Ci tę metodę, spowodowane jest to tym, że rzuca ona wyjątki, które należy obsłużyć. Jak wspomniałem na początku, w tym artykule nie będę poruszał tego tematu, dlatego do sygnatury metody dodałem:

throws Exception

Należy jednak pamiętać, że jest to obejście problemu na potrzeby uproszczenia kodu w artykule, nie jego rozwiązanie, które należałoby stosować w kodzie produkcyjnym.
Wróćmy teraz do naszej odpowiedzi. Jak wspominałem wcześniej, składa się ona z kilku części. Ta, na której nam obecnie zależy, to ciało. Wydobycie jej jest bardzo proste:

response.body();

Rezultat
I to już wszystko! Po drobnych zmianach nasz kod ostatecznie będzie wyglądał w sposób następujący:

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

import static java.net.http.HttpResponse.BodyHandlers.ofString;

public class LoremMain {
    private static final URI LOREM_URI =
               URI.create("https://loripsum.net/api/1/short/plaintext");

    public static void main(String[] args) throws Exception {
        System.out.println(getLoremIpsum());
    }

    private static String getLoremIpsum() throws Exception {
        HttpClient client = HttpClient.newHttpClient();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(LOREM_URI)
                .GET()
                .build();

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

Pozostało tylko uruchomić metodę main i zobaczyć odpowiedź, która powinna się wyświetlić w terminalu. Ja po pierwszym uruchomieniu otrzymałem:

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duo Reges: constructio interrete. Quae cum dixisset, finem ille. Eaedem res maneant alio modo. Si longus, levis; Gerendus est mos, modo recte sentiat.

Twoja odpowiedź będzie się lekko różniła, jako że API za każdym razem generuję inną. Jedynie pierwsze zdanie będzie takie samo.

Podsumowanie

Mam nadzieję, że zgodzisz się ze mną, że wysyłanie zapytań przy pomocy klienta HTTP jest łatwe i przyjemne. Udało się nam to za pomocą tylko kilku linijek kodu! Oczywiście to dopiero początek. Zapytanie które otrzymaliśmy to był jedynie prosty tekst. W następnych artykułach z tej serii będziemy stopniowo rozszerzać wiedzę. Zaczniemy od odbierania obiektów typu JSON, które będziemy mapować na stworzone przez nas klasy. Zajmiemy się też obsługą błędów i autoryzacją.

Autor:
Hubert Pieśniak

Hubert Pieśniak - autor wpisu

Software Engineer w EPAM Systems, kodujący w Javie. Od dziecka pasjonat technologii, któremu programowanie jako hobby towarzyszyło od najmłodszych lat. Do branży IT trafił jednak bardzo okrężną drogą. Studiował Ekonomię na SGH, UEP i Sogang University w Seulu. Do niedawna prowadził największy klub bilardowy na Pomorzu, wcześniej sklepy jubilerskie do których importował towar z Włoch, Turcji i Tajlandii, tuż po tym jak reprezentował firmę sprzedającą mufy do kabli wysokonapięciowych. Człowiek wielu pasji i zainteresowań.