Spis treści

Java 11 - HTTP Client i uruchamianie jednoplikowych programów

Ilustracja - pliki w chmurze

HTTP Client

W końcu wszedł do biblioteki standardowej (oficjalnie, a nie jako incubator) porządny klient HTTP. Wcześniej Java posiadała API HttpURLConnection, która jednak brzydko się zestarzała.

Dlaczego wyrzucamy HttpURLConnection do kosza

  • HttpURLConnection był projektowany z myślą o protokołach, które dziś są już praktycznie nie używane (gopher, ftp etc)
  • API powstało przed HTTP/1.1 i jest zbyt abstrakcyjne
  • trudno się go używa, a implementacja przejawia wiele nieudokumentowanych zachowań
  • działa wyłącznie w trybie blokującym (na każde żądanie/odpowiedź wykorzystywany jest jeden wątek)
  • istniejąca implementacja jest trudna do utrzymania

Jaki będzie nowy HTTP Client?

Cele, jakie przyświecały twórcom nowego kliena HTTP, to przede wszystkim:

  • możliwość szybkiego i prostego wykonania najbardziej powszechnych zadań
  • musi wspierać standardowe mechanizmy identyfikacyjne (na początek Basic auth)
  • łatwe do użycia WebSockets
  • wydajność tak dobra lub lepsza niż użycie Jetty bądź Netty
  • zużycie pamięci porównywalne bądź niższe niż to w Apache HttpClient, Netty lub Jetty
  • możliwość użycia asynchonicznego API pozwalającego na powiadomienie aplikacji o “przybyciu” kolejnego nagłówka czy porcji danych

Co oferują nowe klasy

Na stronie openjdk znajdują się przykłady użycia klasy HTTPClient w kilku użytecznych sytuacjach:

Wywołanie (synchroniczne) żądania GET

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void get(String uri) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.build();

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

System.out.println(response.body());
}

Wysłanie żądania wymaga zawsze podania, jaki BodyHandler ma obsłużyć odpowiedź serwera. Jest on wywoływany po przybyciu nagłówków i przed nadejściem treści, a jego zadaniem jest stworzenie BodySubscriber, subscribera (w nomenklaturze reactive-stream), który w sposób nieblokujący przekształci bajty odpowiedzi w typ Javy (np. String albo File).

HTTPResponse.BodyHandlers oferuje różne typy handlerów: ofString, ofFile, ofInputStream czy ofByteArrayConsumer.

Ściągnięcie pliku

Poniższy kod ściąga i zapisuje odpowiedź z serwera do pliku:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public void get(String uri) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.build();

HttpResponse<Path> response =
client.send(request, BodyHandlers.ofFile(
Paths.get("body.txt")));

System.out.println("Response in file:" + response.body());
}

Asynchroniczne żądanie GET

Takie żądanie wraca natychmiast, zwracając CompletableFuture, co pozwala wskazać kod do wykonania asynchronicznego gdy już HttpResponse będzie dostępny.

1
2
3
4
5
6
7
8
9
public CompletableFuture<String> get(String uri) {
  HttpClient client = HttpClient.newHttpClient();
  HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(uri))
    .build();

  return client.sendAsync(request, BodyHandlers.ofString())
    .thenApply(HttpResponse::body);
}

Wysłanie żądania POST

HTTPRequest.BodyPublisher jest odpowiedzialny za przekształcenie napisu (danych) w bajty wysyłanie w żądaniu POST. BodyPublisher jest publisherem w rozumieniu reactive-streams i “publikuje” dane umieszczone w body żądania w asynchronicznym strumieniu danych (być może w małych fragmentach, w zależności od rozmiaru danych i możliwoścu back-pressure).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void post(String uri, String data) throws Exception {
  HttpClient client = HttpClient.newBuilder().build();
  HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(uri))
    .POST(BodyPublishers.ofString(data))
    .build();

  HttpResponse<?> response = client.send(request,
    BodyHandlers.discarding());
  System.out.println(response.statusCode());
}

Wysłanie całej listy żądań i otrzymanie asynchronicznych odpowiedzi:

Wysyłamy całą listę żądań, zgarniamy listę CompletableFuture i czekamy na zakończenie wszystkich:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public void getURIs(List<URI> uris) {
  HttpClient client = HttpClient.newHttpClient();
  List<HttpRequest> requests = uris.stream()
    .map(HttpRequest::newBuilder)
    .map(reqBuilder -> reqBuilder.build())
    .collect(toList());

  CompletableFuture.allOf(requests.stream()
    .map(request -> client.sendAsync(request, ofString()))
    .toArray(CompletableFuture<?>[]::new))
    .join();
}

To jedynie niektóre z możliwych scenariuszy użycia HttpClient. Kod klienta http można teraz napisać w czystej Javie bez żadnych zewnętrznych zależności. Jeśli dodamy do tego możliwośc uruchamiania jednoplikowych programów, to okaże się, że w Javie stało się możliwe pisanie prostych skryptów pobierających dane z sieci. Popatrzcie:

Uruchamianie jednoplikowych programów

Nowy tryb uruchomienia

W Javie 10 istniały trzy tryby uruchomienia kodu przez program uruchomieiowy java:

  • uruchomienie klasy
  • uruchomienie klasy głównej w jarze
  • uruchomienie klasy głównej w module

Java 11 wprowadziła możliwość uruchamiania plików .java bezpośrednio przy użyciu programu java (do programu java został dodany nowy tryb uruchomienia). Nie trzeba teraz wykonywać (jawnego) etapu kompilacji przy użyciu kompilatora javac. Wcześniej należało najpierw plik .java skompilować, a dopiero w kolejnym kroku uruchamiać. Tę nową możliwość opisuje JEP-330.

… z wiersza poleceń

  • przed Java 11 źródło Simple.java musiało zostać jawnie skompilowane:
1
2
3
4
5
$ javac Simple.java
$ ls
Simple.java Simple.class
$ java Simple
Hello
  • w Java 11 kompilacja jest niejawna:
1
2
3
4
$ java Simple.java
Hello
$ ls
Simple.java

… jako plik wykonywalny

W systemach wywodzących się z Uniksa (MacOS, Linux) istnieje mechanizm wskazania w pliku wykonywalnym, w jego pierwszym wierszu, ścieżki i ew. opcji do programu, do którego zostanie ten plik przekazany do uruchomienia/interpretacji. Można go wykorzystać do utworzenia “skryptów” pisanych w czystej javie. Plik ze źródłem musi jednak spełnić kilka warunków:

  • musi być plikiem wykonywalnym, a więc należy nadać plikowi odpowiednie uprawnienia (chmod +x Simple.java)
  • dodać shebang (#!):
  • wskazując ścieżkę do binarki java w pierwszym wierszu kodu oraz
  • dodając - obowiązkową w tym przypadku - opcję --source wskazującą na wersję źródła Javy
  • zmienić nazwę (plik z pierwszym wierszem rozpoczynającym się od shebang nie może mieć rozszerzenia .java)
1
2
3
4
$ echo -e '#!/usr/bin/java --source 11' | cat - Simple.java > Simple
$ chmod +x Simple
./Simple
Hello

Trzeci powyższy punkt (konieczność zmiany nazwy) jest dość interesujący i wynika z nowego trybu działania programu-launchera (java): przekaże on do kompilatora treść pliku bez pierwszego wiersza (a więc zawierającą poprawny CompilationUnit) tylko wówczas, gdy pierwszy wiersz będzie wierszem z shebangiem i jednocześnie nie będzie miał standardowego roszerzenia dla pliku źródłowego javy (.java).

Szczegóły: JEP-330: shebang files

Zastosowania

Szczerze mówiąc, nie wyobrażam sobie, aby ktoś powaźnie traktował możliwość pisania skryptów w Javie. Jeśli życie zetknęło kogoś z koniecznością oskryptowania czegokolwiek, to z pewnością był to język powłoki, python lub coś podobnego.

Nowy tryb uruchomienia może być jednak przydatny, głównie dla osób dopero uczących się programowania w Javie. Być może im spodoba się ta nowa możliwość.

Podobne wpisy: