Java-Web-Frameworks von innen?

Im Maschinenraum

Michael Vitz, Phillip Ghadir

Web-Frameworks für Java gibt es nun bereits seit fast 20 Jahren. Diese nehmen dem Anwendungsentwickler eine Menge Arbeit ab und sorgen dafür, dass dieser sich nicht auf Infrastruktur konzentrieren muss, sondern die Anwendungslogik im Vordergrund steht. Doch was genau tut so ein Web-Framework eigentlich? Dieser Artikel zeigt, was heutige Web-Frameworks leisten und wo diese sich dann doch in Nuancen unterscheiden.

Obwohl REST [1] heute als Integrationsarchitektur gesetzt ist, besitzen viele Java-Entwickler noch immer recht wenig Verständnis für HTTP. Nur aus der Ferne betrachtet ist HTTP ein sehr einfaches Protokoll: Der Client schickt einen Request an den Server, den dieser verarbeitet und mit einer Response beantwortet.

Ein Blick in die Request for Comments (s. Kasten “HTTP RFCs”) macht deutlich, dass HTTP nicht ganz trivial ist. Abbildung 1 zeigt, welche Entscheidungen in der Request-Verarbeitung getroffen werden müssen, bevor die eigentliche Anwendungslogik aufgerufen werden darf.

HTTP RFCs

Abb. 1: HTTP-Decisions - unabhängig von der Fachlogik

Das richtige Web-Framework?

Häufig begegnet uns die Frage, welches Web-Framework für die Entwicklung von komplexen Systemen am Besten geeignet ist. Wie beantwortet man diese Frage, die so einfach klingt, aber so viele Facetten hat?

Wir drücken uns um eine Antwort, indem wir klären, welche Abstraktionen und Funktionen man sich von einem Web-Framework wünschen sollte. Aus dieser Perspektive heraus sollte jeder in der Lage sein, ein konkretes Framework auf dessen Eignung für die eigenen Zwecke auszuwählen.

Welche Abstraktionen braucht man?

Angenommen, wir müssten einen einfachen Internet-Service programmieren, der nur die denkbar einfachste Interaktion zulässt: Auf jede Anfrage antwortet er unmittelbar. Darüber hinaus gibt es keine weitere Interaktion. In diesem Fall genügt es, wenn unsere Funktionalität aufgerufen werden kann. Es gibt also keine speziellen Anforderungen an das Framework. Eigentlich könnte man alles HTTP-Spezifische ausblenden.

Aber wenn der Internet-Service komplexere Interaktionen unterstützen muss, und dafür gegebenenfalls mehrere Dialoge oder Zustände bietet, müssen die Dialogschritte oder Zustände identifiziert werden können.

HTTP nutzt zur Adressierung bekanntlich URIs. Ein Web-Server und gegebenenfalls auch das Web-Framework müssen zu diesen URIs die passende Implementierung finden können. Dies wird allgemein als Routing bezeichnet.

Das Routing liefert für einen HTTP-Request die aufzurufende Methode in der Laufzeitumgebung. Dazu betrachten wir zuerst einmal ein Beispiel für einen HTTP-Request:


GET /service/customers/1234 HTTP/1.1
Host: www.example.com
Accept: text/html

Grundsätzlich erlaubt HTTP die Adressierung beliebiger Dinge durch:

  • die Angabe des HTTP-Verbs (Zeile 1 des Requests),
  • die Adressierung der Ressource (Zeile 1),
  • die Angabe des Hosts (Zeile 2),
  • die Angabe des Formats (Content-Type des Request-Body),
  • die Angabe der zulässigen Antwortformate (Zeile 3).

Darüber hinaus bestehen zwei weitere Möglichkeiten:

  • beliebige eigene Header anzufügen, die dann möglicherweise nicht in jeder Infrastruktur funktionieren und daher stets nur optionale Informationen enthalten sollten, oder aber
  • sogenannte Cookies zu vergeben: Diese dienen im Guten wie im Schlechten zur Identifizierung von Browsern, Sessions und Nutzern im World Wide Web.

Cookies

“Cookies” ist ein spezieller HTTP-Header, der (zum Teil größenbeschränkt auf 4 KB) selbst Schlüssel-Wert-Paare enthalten kann. Das Besondere an Cookies ist, dass sie für einen Host (mit einem Ablaufdatum) gesetzt werden können, und dann innerhalb des Gültigkeitszeitraums automatisch bei jedem Request vom Browser an den Server mitgeschickt werden.

Arten von Web-Frameworks

Wir versuchen die Web-Frameworks anhand der Merkmale zu unterscheiden, von denen sie abstrahieren und nach ihren zentralen Konstruktionselementen zu benennen.

Zuallererst gibt es hier Action-basierte Web-Frameworks. Diese bilden den von HTTP geforderten Request-Response-Zyklus ab, indem der Anwendungsentwickler eine vom Framework vorgegebene Schnittstelle implementiert, die vom Framework aufgerufen wird, wenn ein passender Request ankommt. Als zusätzlichen Input bekommt man ein Objekt welches den HTTP-Request abbildet. Das Ergebnis dieses Aufrufes ist eine HTTP-Response. Aktuelle Vertreter dieser Art sind Grails [2], JAX-RS [3], das Play Framework [4] und SpringMVC [5].

Als zweite Art haben sich komponentenbasierte Web-Frameworks etabliert. Diese sind von den klassischen Rich-Client-Programmiermodellen inspiriert und legen ihren Fokus darauf, dass eine Oberfläche aus mehreren Komponenten besteht und die Anwendung auf Änderungen dieser Komponenten reagiert. Der Request-Response- Zyklus wird dabei im Allgemeinen vor dem Anwendungsentwickler versteckt. Dieser implementiert lediglich Callbacks der Komponenten. Das Framework übernimmt die komplette Verarbeitung von Request und Response, ohne dem Entwickler Einflussmöglichkeiten zu geben.

Komponentenbasierte Frameworks halten oder rekonstruieren typischerweise den Dialogzustand für jeden Request-Response-Zyklus. Neben dem JavaEE-Standard JSF [6] sind Apache Wicket [7] und Vaadin [8] populäre Vertreter dieser Art.

Neben diesen beiden Arten tritt in letzter Zeit eine dritte Art von Web-Frameworks in Erscheinung: ressourcenbasierte Web-Frameworks. Sie sind den Action-basierten Frameworks nicht unähnlich, allerdings unterscheiden sich die Programmiermodelle teilweise.

Eine definierte Ressource wird an eine URI gebunden. Wird diese URI aufgerufen, durchläuft das Framework strikt die in den HTTP RFCs beschriebene State-Maschine. Bei jeder Entscheidung wird auf der Ressource ein Callback aufgerufen, der den weiteren Verlauf des Requests beeinflusst. Somit entstehen Webanwendungen, die sich sehr genau an die HTTP-Spezifikation halten, und die Verarbeitung eines Requests wird auf mehrere Methoden aufgeteilt. Jede Methode muss hierbei allerdings nur einen einzelnen Aspekt der HTTP-Verarbeitung betrachten.

Leider gibt es auf der JVM für diese Art von Web-Frameworks noch nicht viele Implementierungen, das Konzept ist jedoch aufgrund der populären Erlang-Implementierung Webmachine [9] ausgereift. Auf der JVM kann man sich mit webster [10] eine Java-basierte oder mit liberator [11] eine Clojure-basierte Implementierung ansehen.

Routing – hin und zurück

Das Routing findet zur Laufzeit die richtige Implementierung für einen ankommenden Request. Je nach Anforderungen sind für das Routing beliebige Abstraktionen zulässig.

Action-basierte Web-Frameworks ziehen dafür zumindest mal die URI und in vielen Fällen auch das Verb und den Content-Type heran, während komponentenbasierte Web-Frameworks typischerweise vom Content-Type abstrahieren und den Fokus eher darauf legen, den Dialogzustand zwischen Client und Server zu synchronisieren.

Jedes Action-basierte Web-Framework besteht somit in seinem Kern aus einem Teil, der einen HTTP-Request entgegennimmt und anschließend den für diesen Request relevanten Code ausführt. Damit dies funktioniert, muss der Entwickler Routen definieren und diese dem Framework bekannt machen.

Routen registrieren

Die Registrierung von Routen und der Support der verschiedenen Routing-Merkmale unterscheidet sich je nach Framework. Servlets beispielsweise werden nur anhand der URI registriert – entweder per XML oder per Annotation. Gleiches gilt für SpringMVC. Bei SpringMVC lassen sich die Routen jedoch durch eine Kombination aus URI, HTTP-Methode, Accept- und Content-Type-Header registrieren:

...
@RequestMapping(method = RequestMethod.GET
                path="/clients/{id}")
public String show(@PathVariable Long id, Model model) {
    Client client = clientService.findById(id);
    model.addAttribute("client", client);
    return "showClient";
}
...

Das Play-Framework geht einen anderen Weg: Hier werden die Routen in einer DSL definiert, welche anschließend zu Scala-Code kompiliert wird:


# Verb Path         Action
GET    /clients/:id controllers.Clients.show(id: Long)

Die angegebene Action hierzu sieht folgendermaßen aus:

package controllers

import play.api._
import play.api.mvc._

object Clients extends Controller {
    def show(id: Long) = Action {
        Client.findById(id).map { client =>
            Ok(views.html.Clients.display(client))
        }.getOrElse(NotFound)
    }
}

Das Routing erfolgt auch hier per HTTP-Methode und URI.

Reverse-Routing

Wenn ein Service Antworten generieren soll, die per Link auf andere Serviceelemente oder Ressourcen verweisen, möchte man als Entwickler sich nicht selbst die URI zusammenbauen müssen, sondern die geeignete Handler-Funktion oder Klasse angeben und für diese automatisch einen Link vom Framework generieren lassen. Eine manuelle String-Konkatenation für den Zusammenbau von URIs würde nur zu großem Änderungsaufwand bei Anpassungen führen. Das Erzeugen eines Links für eine bestehende Implementierung nennt man Reverse-Routing.

Auch für diese Funktionalität gibt es unterschiedliche Ansätze und Unterstützung durch die aktuell verfügbaren Web-Frameworks. SpringMVC bietet mit dem MvcUriComponentsBuilder die Möglichkeit zu sagen welche Controllermethode man aufrufen möchte, und baut anschließend die passende URI hierfür:

UriComponents uriComponents = MvcUriComponentsBuilder
        .fromMethodCall(on(ClientsController.class).show(1)).buildAndExpand();

URI uri = uriComponents.encode().toUri();

Einen anderen Weg geht das Play-Framework, indem es aus den Routen spezielle Klassen generiert, die für das Reverse-Routing genutzt werden können:

...
def showFirstClient = Action {
    Redirect(routes.Clients.show(1))
}
...

Andere Frameworks bieten einem hierfür keinerlei Unterstützung und der Entwickler muss sich seine eigene Abstraktion hierfür schaffen.

Ist das Verlinken innerhalb des Systems gelöst, treten ganz andere Fragen in den Vordergrund.

Session-Management

HTTP ist zustandslos. Ein Client sendet einen Request und erhält eine Response. Das Protokoll selbst sieht keinen Aufbau eines Dialoggedächtnisses vor. Jeder Request soll alle Informationen enthalten, die für dessen Antwort erforderlich sind.

In HTTP ist nicht definiert, wie ein Server erkennen kann, ob zwei Requests zusammengehören oder unabhängig sind. Das trägt wesentlich dazu bei, dass das Gesamtsystem World Wide Web so skalierbar und damit auch verfügbar ist.

Für die Anwendungsentwicklung würde man sich - je nach Anwendung - aber schon eine zustandsbehaftete Konversation wünschen.

Das Konzept einer Benutzer-Sitzung lässt sich gut Framework-seitig unterstützen, da es üblicherweise unabhängig von der Fachlogik ist.

Ein Web-Framework, das die Grundideen des World Wide Web trägt, sollte sich nicht darauf verlassen, dass sich die mit dem Client ausgetauschten Formate dazu eignen, eine Session-ID mitzuführen.

Somit bleiben in HTTP nur noch zwei Stellen, die eine Session-ID enthalten können:

  • in der URI oder
  • als Header.

Die Übertragung im Body entfällt, wenn alle HTTP-Verben unterstützt werden sollen, da nicht jedes Verb einen Request-Body unterstützt.

Nutzt man die URI zur Übertragung einer Nutzeridentifikation, so geschieht dies in der Regel im “query”-Teil, da der Pfad zum Routing benötigt wird und man meistens auch nicht für jeden Nutzer eine eigene URI für dieselbe Resource haben möchte. Dies ist möglich und wird auch teilweise genutzt (z.B. per jsessionid).

Die Session sollte nicht (ausschließlich) in der URI kodiert werden. Wenn die URI ausreicht, auf den gesamten Zustand einer Session zuzugreifen, kann jeder, der in den Besitz dieser URI kommt (durch Versenden per Copy&Paste oder Mitschneiden des Netzwerk-Verkehrs), die Session übernehmen und von da an als legitim angemeldeter User handeln.

Daher empfiehlt sich für das Prüfen der Authentizität eines Requests eigentlich nur eines.

Session-Cookies

Somit bleibt nur noch die Option das Merkmal per Header zu übertragen. Hierzu nutzt man Cookies. Der Server sendet hierbei in einer Response die Aufforderung an den Client, einen Wert in einem Cookie abzulegen. Bei jedem weiteren Request sendet der Client anschließend den Wert des Cookies als HTTP-Header mit. Hieraus entstehen folgende Anforderungen:

  • Der Wert sollte möglichst klein sein, um nicht bei jedem Request unnötig viel
    Bandbreite zu verbrauchen.
  • Der Wert sollte signiert sein, da dieser potenziell vom Client verändert
    werden könnte.
  • Das ganze funktioniert nur mit HTTPS, da ansonsten der Wert des Cookies im
    Netzwerk mitgeschnitten werden könnte.

Ein gutes Web-Framework versteckt all diese Funktionalität vor dem Anwendungsentwickler. Dieser kann demnach soviele Objekte wie er möchte in die Session packen. Das Framework speichert diese jedoch serverseitig (im Speicher oder zum Beispiel in einer Datenbank) und sendet an den Client lediglich eine signierte Session-ID.

Response-Rendering

Nachdem die Geschäftslogik ausgeführt wurde, muss häufig das Ergebnis dieser Logik an den Client übertragen werden. Hierzu dient der Body innerhalb einer HTTP-Response. Webanwendungen liefern hier klassischerweise HTML aus. Aber natürlich müssen auch andere Formate, wie JSON für APIs, ausgeliefert werden. Die Frameworks erfinden hierbei nichts Neues, sondern bedienen sich bei bestehenden Technologien und Libraries und bieten hierfür eine Integration an.

Spring beispielsweise erlaubt eine ganze Menge an Rückgabetypen in seinen Controllern (s. [12]). Das Framework reagiert anschließend unterschiedlich je nach Rückgabewert. Für HTML wird in der Regel eine Template Engine (z.B. Thymeleaf [13], JSP [14] oder Freemarker [15]) genutzt, wohingegen JSON-Responses durch Serialisieren des Rückgabewertes über jackson [16] erzeugt werden.

Das Play-Framework lässt nur einen validen Rückgabewert für Controller zu. Hier muss der Anwendungsentwickler seine Objekte vorher in das passende Format konvertieren. Play liefert hierzu allerdings für HTML eine eigene Templatesprache und empfiehlt, für JSON jackson zu nutzen [17].

Andere Frameworks wiederum bietet dem Entwickler hier jede Freiheit, indem diese keinerlei Unterstützung mitbringen und den Entwickler wählen lassen.

Request-Binding

Gerade bei Action-basierten Web-Frameworks wünscht man sich häufig die mit dem Request geschickten (Meta-)Daten als Objektmodell. Dafür bieten Frameworks unterschiedliche Möglichkeiten an.

SpringMVC zum Beispiel erlaubt eine fast unendliche Anzahl an Methodenargumenten (s. [18]). Das Play-Framework hingegen bietet innerhalb einer Controller Action Zugriff auf den Request durch die implizite Variable request:

...
def index() = Action {
    request.get("myHeader").orNull
}
...

Komponentenbasierte Frameworks brauchen kein für den Entwickler sichtbares Request-Binding. Diese geben dem Entwickler Zugriff auf die UI-Elemente der Seite und abstrahieren somit komplett vom Request.

Ressourcenbasierte Frameworks sind da anders. Sie geben dem Entwickler direkten Zugriff auf ein Request-Objekt. Dieser ist anschließend selbst dafür verantwortlich, die benötigten Daten aus dem Request zu extrahieren.

Sicherheitsaspekte

Von einem Web-Framework wünscht man sich, dass die typischen Angriffsvektoren auf Webanwendungen verhindert werden. Auf Anhieb fallen hier Dinge wie Cross-Site-Scripting, XSRF oder auch SQL-Injection ein.

Ein Framework sollte bei der Verwendung der HTTP-Parameter und beim Verarbeiten von Requests und Request-Parametern sowie beim Wiederherstellen von Sitzungsdaten sicherstellen, dass dies zuverlässig sicher funktioniert und illegale Zugriffe unterbunden werden.

Fazit

Die Frage nach dem geeigneten Web-Framework lässt sich nicht einfach beantworten. Je nach Anforderungen genügt zum Teil das einfachste, aber je nach Anforderungen steigt der Bedarf nach Funktionalität, die HTTP mit sich bringt.

Komponentenbasierten Web-Frameworks bieten ein Programmiermodell, das zwar Rich-Client-Entwicklern vertraut erscheinen mag, aber die Vorzüge des Webs so kapselt, dass sie sich nur schwerlich nutzen lassen. Zudem leidet bei diesen häufig die Skalierbarkeit, da sie meistens eine serverseitige Session vorraussetzen.

Action-basierte und ressourcenbasierte Frameworks sind da schon besser geeignet, auf zukünftige Qualitätsanforderungen mit HTTP-Mitteln zu reagieren.

In dieser Ausgabe haben wir ein paar der wesentlichen Eckpfeiler von Web-Frameworks betrachtet, um hier zumindest für die Serverseite eine Entscheidungshilfe zu skizzieren.

Links

  1. https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm>  ↩

  2. https://grails.org  ↩

  3. https://jax-rs-spec.java.net  ↩

  4. https://www.playframework.com  ↩

  5. http://projects.spring.io/spring-framework/  ↩

  6. http://www.oracle.com/technetwork/java/javaee/javaserverfaces-139869.html  ↩

  7. http://wicket.apache.org  ↩

  8. https://vaadin.com/home  ↩

  9. https://github.com/webmachine/webmachine  ↩

  10. https://github.com/pschirmacher/webster  ↩

  11. http://clojure-liberator.github.io/liberator/  ↩

  12. http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-return-types  ↩

  13. http://www.thymeleaf.org  ↩

  14. http://www.oracle.com/technetwork/java/javaee/jsp/index.html  ↩

  15. http://freemarker.incubator.apache.org  ↩

  16. https://github.com/FasterXML/jackson  ↩

  17. https://www.playframework.com/documentation/2.4.x/JavaJsonActions  ↩

  18. http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-arguments  ↩

Thumb michael vitz innoq

Michael Vitz ist Senior Consultant bei innoQ und verfügt über mehrjährige Erfahrung in der Entwicklung und im Betrieb von JVM-basierten Systemen.

Zur Zeit beschäftigt er sich vor allem mit den Themen Microservices, Cloud-Architekturen, DevOps, Spring-Framework sowie Clojure.

Weitere Inhalte

Thumb dsc08236

Phillip Ghadir, Mitglied der Geschäftsleitung der innoQ, berät Kunden in Fragen rund um Softwarearchitektur, -technik und -entwicklung. Darüber hinaus gibt er regelmäßig Trainings zum Thema Softwarearchitektur.

Weitere Inhalte

Java spektrum
Dieser Artikel ist ursprünglich in Ausgabe 02/2016 der Zeitschrift JavaSPEKTRUM erschienen. Die Veröffentlichung auf innoq.com erfolgt mit freundlicher Genehmigung des SIGS-Datacom-Verlags.

Kommentare

Um die Kommentare zu sehen, bitte unserer Cookie Vereinbarung zustimmen. Mehr lesen