HttpClient – Grundlagen

Schon seit den Anfangszeiten von Java ist es möglich, HTTP Requests zu machen mittels den Klassen URL und URLConnection. An diesen Klassen hat sich über die Jahre wenig Neues getan und so sind viele Entwickler auf Bibliotheken anderer Hersteller umgestiegen (z.B. von Apache). Mit Java 9 gibt es das httpClient API, welches elegant ist und zudem HTTP2 unterstützt.

Java 8 und früher

Zuerst wollen wir uns anschauen, wie man vor Java 9 HTTP Requests gemacht hat.

Wir erstellen eine URL und öffnen mittels openStream() einen Stream und lesen dann Daten. Das Lesen der Daten haben wir in eine eigene private Methode gepackt:

String pageUrl = “https://www.puzzle.ch”;
URL url = new URL(pageUrl);
String content = readContent(url.openStream());

private String readContent(final InputStream is) throws IOException {
  try (InputStreamReader isr = new InputStreamReader(is); 
       BufferedReader br = new BufferedReader(isr)) {

    return br.lines().collect(Collectors.joining("\n"));
  }
}

Einfach und kompakt. Aber man sieht nicht wirklich, dass man HTTP Requests macht. Auch fehlt Support für “Advanced Features” wie Unterstützung für Proxies, Cookies, URL Redirection etc. Dies ist auch der Grund, dass viele Entwickler auf Libraries wie Apache HttpComponenents gewechselt haben.

Java 9

Dies ändert sich aber mit Java 9. Es gibt ein neues API, den HttpClient. Das API bietet viele Vorteile:

  • Sprechendes” und Fluent API
  • Unterstützung Advanced Features
  • Support für HTTP2
module puzzle.blog.module.http {
	exports ch.puzzle.http2;
	requires jdk.incubator.httpclient;
}

Mit Java 9 ist noch zu erwähnen, dass der HttpClient noch nicht offizieller Teil des JDKs ist. Er ist im sogenannten “Incubator Status”. Aus Sicht der JDK Entwickler ist das API noch nicht abschliessend und kann sich bis zu seiner offiziellen Integration in Java 10 noch ändern.

Wir haben aber jetzt schon mit Java 9 die Möglichkeit, mit dem neuen API etwas zu experimentieren.

Erste Berührung mit den Java Modulen

Incubator Status heisst konkret, dass der HttpClient im JDK internen Modul jdk.incubator.httpclient implementiert ist. Seit Java 9 gibt es ja die Modularisierung und diese verhindert nun, dass wir direkt auf internen Module zugreifen können. Es gibt aber die Möglichkeit, unseren Code selbst in ein Modul zu packen und damit Zugriff auf den HttpClient zu bekommen. Ein eigenes Modul schreiben ist nicht wirklich schwierig:

Dazu erstellen wir ein module-info.java File im Root Level unserer Source Code Files. Es ist ein File mit der Endung “.java”, ist aber nicht ein offizielles Java File sondern ein Modul Descriptor. Folgende Informationen sind nötig:

  • Keyword “module”
  • Name des Moduls (kann selbst gewählt werden)
  • Eine Liste von Java Packages die ich nach aussen exportieren (d. h. sichtbar machen) möchte
  • Eine Liste von anderen Modulen, auf die ich Abhängigkeiten habe (d.h. auf die ich in meinem Code zugreifen möchte)

Unser Modul Descriptor sieht dann folgendermassen aus:https://www.puzzle.ch/wp-admin/post.php?post=10952&action=trash&_wpnonce=a3c9751b9d

module puzzle.blog.module.http {
exports ch.puzzle.http2;
requires jdk.incubator.httpclient;
}

Eclipse Tip

In Eclipse kann man sich den Modul Descriptor auch automatisch erstellen lassen. Einfach auf ein bestehendes Projekt klicken und im Kontext Menü unter “Configure” den Eintrag “Create module-info.java” auswählen.

Mit diesen Vorbereitungen können wir nun mit dem HttpClient starten. Als ersten Schritt möchten wir unser URL Beispiel mit Java 9 umsetzen. Wir nennen es mal “Hello HTTP2”.

Hello HTTP2

Das API besteht aus folgenden Hauptklassen:

  • HttpRequest
  • HttpClient
  • HttpResponse

Die wir uns etwas genauer anschauen möchten:

Zentral ist dabei der HttpClient, der einen HttpRequest als Parameter nimmt und als Resultat eine HttpResponse zurückgibt. Die Verarbeitung des HttpRequests kann dabei synchron oder asynchron erfolgen.

Der HttpRequest bietet eine Vielzahl von Optionen an und ist als Builder implementiert. Wir starten mit einem einfachen GET Request:

String pageUrl = “https://www.puzzle.ch”; 
HttpRequest request = 
      HttpRequest.newBuilder(new URI(pageUrl))
         .GET()
         .build();

Sicherlich mehr Code als beim Legacy Beispiel, aber man sieht direkt was man macht (einen GET Request) und das “Fluent API” ist wirklich schön geworden.

Der HttpClient bietet verschiedene Methoden an zum Versenden des Requests. Wir starten mit der synchronen Variante:

API

public abstract  HttpResponse
	send(HttpRequest req, HttpResponse.BodyHandler responseBodyHandler)
	throws IOException, InterruptedException;

	Sends the given request using this client, blocking if necessary to get the response. The returned HttpResponse contains the response status, headers,and body.

Neben dem Request braucht die send() Methode noch einen BodyHandler. Mit diesem Interface kann man angeben, wie die Daten der Antwort verarbeitet werden sollen: ignorieren, als Bytes oder als String interpretieren etc. Es gibt im Interface selbst bereits verschiedene (statische) Implementationen. Da wir gerne einen String als Resultat haben möchten, nehmen wir hier die asString() Implementation. Und so erhalten wir eine HttpResponse<String>.

HttpResponse response = HttpClient.newHttpClient().send( 
    request, 
    HttpResponse.BodyHandler.asString());

Kombiniert und etwas kompakter geschrieben, können wir einen GET Request mit folgenden Code machen:

HttpResponse response = HttpClient.newHttpClient().send( 
    HttpRequest.newBuilder(new URI(pageUrl)).GET().build(), 
    HttpResponse.BodyHandler.asString());

Aber was genau ist eine HttpResponse?

Das HttpResponse Object bietet Zugriff auf:

  • die URI die die Response gesendet hat via uri()
  • den HTTP Status Code via statusCode()
  • die HTTP Version die zum Versenden der Response verwendet wurde via version()
  • den Content via body(). Wobei das Resultat abhängig ist vom gewählten BodyHandler Typ
  • die HTTP Headers via headers(). Das Resultat ist ein Object vom Typ HttpHeaders, welches selber wieder verschiedene Methoden bietet zum Zugriff auf die einzelnen Headers.

Hier als Beispiel eine Implementation einer Hilfsmethode, die eine HttpResponse in einen String umwandelt:

public static String toString(final HttpResponse response) {
  StringBuilder sb = new StringBuilder();
  sb.append("Uri    : " + response.uri()).append("\n");
  sb.append("Status : " + response.statusCode()).append("\n");
  sb.append("Version: " + response.version()).append("\n");
  sb.append("Body   : " + response.body()).append("\n");
  sb.append("Headers: " + toString(response.headers())).append("\n");
  return sb.toString();
}

private static String toString(HttpHeaders headers) {
  return headers.map() //
      .entrySet() //
      .stream() //
      .map(e -> String.format("\"%s\":\"%s\"", e.getKey(), e.getValue())) //
      .collect(Collectors.joining("\n"));
}

Wenn wir obigen Code ausführen und ein GET auf die Puzzle Homepage machen, erhalten wir folgenden Output (hier etwas gekürzt):

Url    : https://www.puzzle.ch/de/
Status : 200
Version: HTTP_1_1
Body   : <!doctype html&gt
Total 24908 bytes
Headers:
"cache-control":"[max-age=3600]"
"content-type":"[text/html; charset=UTF-8]”
"date":"[Sat, 02 Jun 2018 07:29:25 GMT]” ...

Selber ausprobieren

Wenn man in obigem Beispiel statt BodyHandler.asString()) den BodyHandler.asFile() verwendet und als Parameter einen Path mitgibt, erhält man als Response eine HttpResponse<Path>, d.h. das in der Url angegebene File (z.B. index.html) wird direkt als File heruntergeladen.

Asynchrone Verarbeitung

Der HttpClient bietet auch die Möglichkeit einen “asynchronen” Http Request zu machen. Statt send() verwenden wir sendAsync() und bekommen als Resultat nicht mehr eine HttpResponse sondern eine CompletableFuture. Dies hat den Vorteil, dass der Aufruf von sendAsync() nicht blockierend ist.

API

public abstract  CompletableFuture<HttpResponse>
	  sendAsync(HttpRequest req, HttpResponse.BodyHandler 
                                responseBodyHandler);

	Sends the given request asynchronously using this client and the given response handler.

Das CompletableFuture enthält (sobald eine Response vom Server gekommen ist) die HttpResponse.

CompletableFuture<HttpResponse> asyncResponse =      	
	HttpClient.newHttpClient().sendAsync( 
    		HttpRequest.newBuilder(new URI(pageUrl)).GET().build(), 
    	HttpResponse.BodyHandler.asString());

asyncResponse 
    .thenApply(response -> ResponseHelper.toShortString(response)) 
    .thenAccept(System.out::println);

CompletableFutures gibt es seit Java 8. Es ist ein API zur Verarbeitung von asynchronen Java Calls. Für unser Beispiel haben wir thenApply() und thenAccept() verwendet: sobald das asynchrone Server Response vom Typ HttpResponse da ist, können wir diese mittels thenApply() in einen String konvertieren und dann mittels thenAccept() auf der Console ausgeben.

Um zu zeigen, dass die Verarbeitung wirklich asynchron ist, können wir nach dem Aufruf von sendAsync() in einem Loop Daten (z.B. “.”) mittels System.out.println() ausgeben. Man sieht dann, dass das Resultat von sendAsync() irgendwann (nicht blockierend) dazwischen kommt.

Soviel zu den Grundlagen des neuen HttpClient APIs. In der nächsten Folge der Java Perlen werden wir uns dann einigen “Advanced Features” widmen.

Referenzen

  • Michael Inden: Java 9 – Die Neuerungen, dpunkt.de
  • Source Code: https://github.com/puzzle/java9_blog/tree/master/ch/puzzle/http
Kommentare sind geschlossen.