Spis treści

Prosta aplikacja w javafx (3) - sięgamy po dane przy użyciu klasy HttpClient

Dziś kolejna część miniserii, w której tworzymy prostą aplikację w javafx do przeglądania statystyk covidowych. Spróbujemy parsować już nie lokalne pliki, lecz odpowiedzi HttpRequest na żądania wysyłane do serwisu udostępniającego odpowiednie API.

Przygotowanie

W ramach przygotowania kodu do dodania docelowej funkcjonalności robię mały refaktoring: wyodrębniam kod parsujący JSON-a ze strumienia InputStream do klasy Deserializer (patrz komit 5eff1ea). Z tej klasy korzysta bezpośrednio implementacja FileService interdejsu CovidService, a w tym wpisie okaże się, że również nowa implementacja będzie z niego korzystać.

Wydzielenie klasy Deserializer

To prosta klasa, która zamyka w sobie zależność do biblioteki Jackson. w konstruktorze konfiguruję mój ObjectMapper i implementuję jedną, kluczową metodę: parseInputStream.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

class Deserializer {
    private final ObjectMapper mapper = new ObjectMapper();
    public Deserializer() {
        mapper.configure(
                DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        SimpleModule module = new SimpleModule();
        module.addDeserializer(Case.class, new CaseDeserializer());
        mapper.registerModule(module);
    }

    public <T> Stream<T> parseInputStream(Class<T> tClass, InputStream is) throws java.io.IOException {
        List<T> values = mapper.readerForListOf(tClass).readValue(is);
        is.close();
        return values.stream();
    }
}

Deserializer będzie po prostu polem final w FileService, a także w nowej implementacji CovidService. FileService odrobinę się uprościł:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class FileService implements CovidService {
    private static final String COUNTRIES_FILE = "countries.json";
    private final Deserializer deserializer = new Deserializer();

    @Override
    public Stream<Country> getCountries() {
        return deserialize(COUNTRIES_FILE, Country.class);
    }

    @Override
    public Stream<Case> getCases(Country data) {
        return deserialize(data.slug() + ".json", Case.class);
    }

    private <T> Stream<T> deserialize(String fname, Class<T> tClass) {
        Stream<T> result;
        try(InputStream is = Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream(fname))){
            result = deserializer.parseInputStream(tClass, is);
        } catch (Exception e) {
            result = Stream.empty();
        }

        return result;
    }
}

…a po refaktoryzacji wersja aplikacji, w której czytamy dane z pliku, wciąż działa. Jesteśmy więc na dobrej drodze.

Trzecia implementacja interfejsu CovidService

Nowy CovidService - nazwijmy go NetworkService - będzie używał bezpośrednio klasy HttpClient (przeczytasz o niej we wpisie Java 11 - http client i uruchamianie jednoplikowych programów): wyśle synchroniczne żądania GET, a otrzymany InputStream sprarsuje korzystając ze wspomnianego wyżej “deserializera”.

Wymagamy nowego pakietu

Do module-info.java dodajemy wiersz:

1
requires java.net.http;

Kod NetworkService

Oto cały NetworkService:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class NetworkService implements CovidService {
    public static final String URI_GET_COUNTRIES = "https://api.covid19api.com/countries";
    public static final String URI_GET_COUNTRY_DATA = "https://api.covid19api.com/total/country/";

    private final HttpClient httpClient = HttpClient.newHttpClient();
    private final HttpRequest requestCountries = buildGetRequest(URI_GET_COUNTRIES);
    private Deserializer deser = new Deserializer();

    @Override
    public Stream<Case> getCases(Country data) {
        try {
            HttpRequest requestSingle = buildGetRequest(URI_GET_COUNTRY_DATA + data.slug());
            InputStream body = httpClient.send(requestSingle, HttpResponse.BodyHandlers.ofInputStream()).body();
            return deser.parseInputStream(Case.class, body);
        } catch (IOException|InterruptedException e) {
            return Stream.empty();
        }
    }

    @Override
    public Stream<Country> getCountries() {
        try {
            return deser.parseInputStream(Country.class, httpClient.send(requestCountries, HttpResponse.BodyHandlers.ofInputStream()).body());
        } catch (IOException|InterruptedException e) {
            return Stream.empty();
        }
    }

    private HttpRequest buildGetRequest(String uri) {
        return HttpRequest.newBuilder().GET().uri(URI.create(uri)).build();
    }
}

Tworzenie żądań

Potrzebujemy wykonać proste żądania GET, więc budowanie obiektu HttpRequest jest bardzo proste. API HttpReuest wykorzystuje wzorzec Builder, dzięki któremu można łatwo skonfigurować żądanie:

W przypadku naszego prostego przypadku użycia:

  • określam typ żądania (GET)
  • tworzę URI na podstawie przekazanego w parametrze napisu:
1
2
3
4
5
6
7
8
private HttpRequest buildGetRequest(String uri) {
    return HttpRequest
      .newBuilder()
        .GET()
        .uri(URI.create(uri))
      .build();
}

Parsowanie odpowiedzi

Obydwie metody w interfejsie CovidService będą korzystały z funkcji _getStreamFromNet(String uri, Class<T> tClass)_, która wyśle żądanie pod odpowiedni adres oraz zserializuje odpowiedź. Będzie to metoda generyczna sparametryzowana typem danych, jaki będzie zwracany w strumieniu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private <T> Stream<T> getStreamFromNet(String uri, Class<T> tClass) {
        try {
            HttpRequest request = buildGetRequest(uri);
            InputStream body = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()).body();
            body.close();
            return deser.parseInputStream(tClass, body);
        } catch (IOException|InterruptedException e) {
            return Stream.empty();
        }
    }

Implementacja metod interfejsu

Implementacja metod CovidService jest teraz trywialna:

  • wywołuję w każdej z nich getStreamFromNet
  • przekazuję uri (w getCases zależy od sluga kraju, w getCountries jest stały) i token klasy (Case albo Country)
1
2
3
4
5
6
7
8
9
@Override
public Stream<Case> getCases(Country data) {
    return getStreamFromNet(URI_GET_COUNTRY_DATA + data.slug(), Case.class);
}

@Override
public Stream<Country> getCountries() {
    return getStreamFromNet(URI_GET_COUNTRIES, Country.class);
}

Screenshot - przegląd krajów

Oto pełna lista krajów i wykres:

/covid-app-window-all-countries.png

Repozytorium

Kod dostępny jest na GitHubie: covidstat.

Zmiany źródeł opisanie w tym wpisie zostały zawarte w komicie c204c79