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
|
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
|
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:

Repozytorium
Kod dostępny jest na GitHubie: covidstat.
Zmiany źródeł opisanie w tym wpisie zostały zawarte w komicie c204c79