Polyglott – asynchron – modular

vert.x – alles wird alles

Eberhard Wolff

Viele werden auf vert.x [1] durch die Diskussion rund um die Rechte an dem Projekt aufmerksam geworden sein. Am Ende sind Red Hat und VMware übereingekommen, das Projekt an die Eclipse Foundation zu geben. Wenn solche Firmen und Organisationen an der Diskussion beteiligt sind, stellt sich die Frage, was diese Technologie überhaupt zu bieten hat. Und vert.x hat tatsächlich einige sehr spannende Ansätze.

Vorab: vert.x ist nicht noch ein Webframework. Es verfolgt vielmehr einen grundlegend anderen Ansatz für die Entwicklung von Anwendungen:

  • vert.x ist asynchron. Technologien wie z. B. WebSockets (Kasten: „WebSockets“) stellen völlig neue Anforderungen an Skalierbarkeit. Außerdem müssen sie viele Netzwerkverbindungen handhaben. Ein Modell, bei dem eine Verbindung mit einem Thread versehen wird, wie Servlet-Container das tun, ist dafür nicht ausreichend. Daher setzt vert.x auf ein asynchrones Aktoren-Modell wie es auch Akka, Node.js oder Erlang tun.
  • Polyglotte Programmierung auf der Java Virtual Machine ist natürlich möglich. Dennoch sind die meistens Libraries speziell für Java gedacht und können nur schwer in andere Sprachen integriert werden. Und viele JVM-Sprachen bringen ihre eigenen Bibliotheken mit. vert.x hingegen ist von Anfang an darauf ausgelegt, mit verschiedenen JVM-Sprachen genutzt zu werden.
  • Modularisierung ist in Java zwar möglich – beispielsweise durch JARs. Aber Module unabhängig voneinander zu deployen, ist nur mit Technologien wie OSGi machbar. Gerade bei Java-EE-Anwendungen sieht man oft Lösungen, bei denen einzelne WARs auf demselben Server über Web Services miteinander kommunizieren. Dann können die einzelnen Module als WARs implementiert und unabhängig voneinander deployt werden. Dabei wird aber den Entwicklern zusätzlicher Aufwand aufgebürdet und gleichzeitig frisst eine solche Lösung Performance durch Socket-Kommunikation und Serialisierung/Deserialisierung. vert.x bietet ein völlig anderes Modell, um mit diesen Problemen umzugehen.

WebSockets
Normalerweise muss der Browser bei einer Webanwendung einen Request schicken, den der Server dann mit einer Response beantwortet. Wie kann ein Server aber beispielsweise Börsenkurse direkt an den Browser schicken? Dazu gibt es zwar einige Lösungen, aber erst WebSockets als Teil von HTML5 hilft wirklich weiter. Es erlaubt den Aufbau von bidirektionalen Verbindungen, durch die der Server dem Client Nachrichten schicken kann. Auf Basis dieser Verbindung können dann Daten ausgetauscht werden. So ist es beispielsweise möglich, dem Browser aktuelle Börsenkurse oder andere aktuelle Informationen zu schicken. Bei klassischen Webanwendungen muss ein Server für einen Client nur dann eine Netzwerkverbindung offen halten, wenn ein Request bearbeitet werden muss. Bei WebSockets ist das anders: Jeder Client muss jederzeit erreichbar sein und daher eine offene Verbindung haben. Daher muss der Server wesentlich mehr offene Verbindungen ermöglichen, als das bei einfachen Webservern der Fall ist. Und es kann vorkommen, dass viele, eher kleine Nachrichten an eine Vielzahl Clients geschickt werden müssen – beispielsweise die aktuellen Kurse. Auch dazu muss der Server entsprechend ausgelegt sein.

Asynchron

Wie schon erwähnt, wird im klassischen Programmiermodell ein Thread für eine Verbindung genutzt. Eine solche Lösung funktioniert nur bis zu einer bestimmten Grenze, weil das Betriebssystem nur eine begrenzte Anzahl Threads verwalten kann. Bei einer großen Anzahl wird das System auch ineffizient, weil beispielsweise jeder Thread einige Ressourcen alloziert.

vert.x geht daher grundlegend anders vor. Der Entwickler schreibt so genannte Verticles, die dann von vert.x aufgerufen werden, konkret vom Event Loop. Das ist ein Thread, der Verticles aufruft, weil beispielsweise Netzwerkpakete eingetroffen sind. In einer JVM können mehrere Event Loops parallel laufen. Dabei ist die Anzahl der Event Loops typischerweise genauso groß wie die Anzahl der CPU-Cores, sodass die Hardware vollständig ausgenutzt werden kann.

Da ein Thread für viele Verticles zuständig ist, darf ein Verticle auf keinen Fall blockieren, weil es beispielsweise auf I/O wartet, denn dadurch kommen auch alle anderen Verticles des Event Loops zum Stillstand. Stattdessen muss ein Callback registriert werden, der aufgerufen wird, wenn der I/O erfolgt ist. So kann das Verticle immer die CPU vollständig auslasten.

Listing 1 zeigt ein einfaches Beispiel für ein Verticle: Es handelt sich um einen HTTP-Server, wie er auch als Beispiel zu vert.x mitgeliefert wird. Die Klasse erbt von Verticle und kann daher mit der Instanz-Variablen vertx auf die vert.x-Infrastruktur zugreifen. Dort wird mit der Methode requestHandler() als Callback ein Handler installiert, der für jeden HTTP-Request ein HTML-Dokument zurückgibt. Dieses Dokument enthält den notwendigen JavaScript-Code, um mit dem WebSocket-Server zu interagieren. Daraufhin wird der Webserver mit listen() gestartet. Die Anwendung kann dann mit vertx run http/ServerBeispiel.java gestartet werden. vert.x kümmert sich um das Kompilieren und stellt die Anwendung zur Verfügung. Für solch einen einfachen Fall ist das Modell nicht deutlich anders, als es beispielsweise bei Servlets genutzt wird.

Listing 2 zeigt ein komplexeres Beispiel, nämlich einen WebSockets-Server (Kasten: „WebSockets“). In Listing 2 wird zunächst ein HTTP-Server initialisiert. Er liefert eine statische HTML-Seite aus, die den notwendigen JavaScript-Code für den Browser enthält und dann eine Kommunikation über WebSockets anstoßen kann. Der WebSockets-Handler untersucht, ob die Anfrage an den richtigen Pfad geht. Dann wird ein Buffer-Handler installiert, der auf die jeweiligen Anfragen reagiert. Jeder Client bekommt also seinen eigenen Buffer-Handler, aber alle werden von demselben Event Loop aufgerufen. Bei dem WebSocket-Beispiel wird deutlich, dass die vielen Callbacks in Java durchaus unübersichtlich werden können.

Listing 1: Einfacher HTTP-Server

public class ServerBeispiel extends Verticle {

  public void start() {
    vertx.createHttpServer()
     .requestHandler(new Handler<HttpServerRequest>() {
        public void handle(HttpServerRequest req) {
          req.response.headers().put("Content-Type", "text/html; charset=UTF-8");
      req.response.end("<html><body><h1>Hallo von vert.x!</h1></body></html>");
    }
      }).listen(8080);
  }

}

Listing 2: WebSockets-Beispiel

 


public class WebsocketsBeispiel extends Verticle {

  public void start() {
    vertx.createHttpServer()
     .requestHandler(new Handler<HttpServerRequest>() {
       public void handle(HttpServerRequest req) {
         if (req.path.equals("/")) req.response.sendFile("websockets/ws.html");
       }
     }).websocketHandler(new Handler<ServerWebSocket>() {
       public void handle(final ServerWebSocket ws) {
    if (ws.path.equals("/myapp")) {
       ws.dataHandler(new Handler<Buffer>() {
         public void handle(Buffer data) {
           ws.writeTextFrame(data.toString());
         }
      });
     } else {
       ws.reject();
     }
  }
    }).listen(8080);
  }
}




Polyglotte Programmierung

Die JVM ist aber polyglott und unterstützt verschiedene Sprachen. vert.x nutzt das aus und unterstützt Groovy, JavaScript und CoffeeScript mit der JavaScript-Engine Rhino, Python mit Jython und schließlich Ruby mit JRuby. Dabei hat vert.x einen sprachspezifischen Layer für jede der Sprachen. Dadurch fühlt sich die Nutzung von vert.x in diesen Sprachen recht natürlich an. Beispielsweise werden die Callbacks, die in Java mit Inner Classes implementiert werden, auf Funktionen bzw. Closures abgebildet. Listing 3 zeigt denselben WebSockets-Server wie Listing 2 – nur dieses Mal mit JavaScript. Für den HTTP-Request wird nun eine Funktion als Callback registriert, die das HTML-File zurückgibt. Genauso werden die Handler für WebSockets durch Funktionen implementiert. Dadurch wird der Code wesentlich kürzer und übersichtlicher. Der Code ist den vert.x-Beispielen entnommen – für Ruby, Python oder CoffeeScript sieht der Code recht ähnlich aus.

Listing 3: WebSockets-Beispiel mit JavaScript
load('vertx.js')

vertx.createHttpServer().requestHandler(function(req) {
  if (req.uri == "/") req.response.sendFile("websockets/ws.html")
}).websocketHandler(function(ws) {
  ws.dataHandler( function(buffer) { ws.writeTextFrame(buffer.toString()) });
}).listen(8080)


Modularisierung

Für die Modularisierung bietet vert.x so genannte Modules an. Dabei handelt es sich um ein Verzeichnis, in dem der Code des Moduls abgelegt wird und eine JSON-Konfigurationsdatei, die definiert, was gestartet werden soll.

Um also den oben gezeigten Code in ein Modul zu verwandeln, muss er in ein Verzeichnis kopiert werden. Per Konvention enthält das Verzeichnis einen Namen, der sich an den Internetdomänennamen orientiert und außerdem die Version enthält – also beispielsweise com.ewolff.websockets-v0.1. Die ebenfalls notwendige JSON-Konfiguration zeigt Listing 4. Wie man sieht, wird die entsprechende Datei, die das zu startende Verticle enthält, angegeben. Außerdem wird definiert, dass das Modul erneut deployt werden soll, wenn irgendwelche Dateien aus dem Modul sich ändern. In der Konfiguration können auch andere Module mit includes referenziert werden. So kann also ein ganzes System aus einzelnen Modulen aufgebaut werden. Gestartet wird das Modul dann mit vertx runmod com.ewolff.websockets-v0.1. Dabei werden auch die abhängigen Module entsprechend geladen. vert.x führt dann die Verticles aus den Modulen aus – im konkreten Fall also den WebSockets-Server.

Listing 4: mod.json: Konfigurationsdatei eines Moduls
{
    "main": "ws_server_beispiel.js",
    "auto-redeploy" : true
}


Es gibt auch einige fertige Module. [2] stellt das zentrale Repository dar. Unter anderem gibt es einen Webserver und einen MongoDB Persistor. Diese Module können mit vertx install direkt in die lokale vert.x-Installation übernommen werden.

Kommunikation

Die Module müssen natürlich miteinander kommunizieren. Dazu enthält vert.x einen Bus, mit dem sich Verticles Nachrichten schicken können. Das funktioniert natürlich auch, wenn die Vertciles nicht Bestandteil eins Moduls sind. Die Nachrichten werden asynchron empfangen und bearbeitet – was dem vert.x-Programmiermodell entspricht. Der Bus unterstützt sowohl Point-to-Point-Kommunikation mit einem Empfänger als auch Publish/Subscribe, wobei die Nachrichten an mehrere Empfänger geschickt werden. Der Bus kann lokal in einer JVM laufen oder auch in einem Cluster, in dem der Bus dann über mehrere Rechner verteilt ist. Die Nachrichten sind transient – also nur im Speicher abgelegt und überleben den Neustart eines Rechners nicht.

Der Bus unterstützt die primitiven Java-Datentypen, Strings, vert.x Buffer und JSON-Objekte. JSON (JavaScript Object Notation) ist das empfohlene Datenformat, denn damit können auch komplexe Objekte unabhängig von der Sprache dargestellt werden. Listing 5 zeigt ein Beispiel: Das Verticle registriert zunächst einen Handler am Event-Bus. Dabei wird angegeben, auf welche Adresse der Handler reagieren soll. Der Handler liest dann Informationen aus dem JSON-Objekt aus und das Verticle schickt an die Adresse des Handlers ein passendes JSON-Objekt.

Listing 5: Beispiel für den Event-Bus
public class Sender extends Verticle {

  public void start() throws Exception {
    vertx.eventBus().registerHandler("com.ewolff.adress",
      new Handler<Message<JsonObject>>() {
        public void handle(Message<JsonObject> msg) {
          System.out.println("Empfangen : " + msg.body);
          System.out.println("Name : " + msg.body.getString("name"));
          System.out.println("Vorname : " + msg.body.getString("vorname"));
    }
  });
vertx.eventBus().send(
  "com.ewolff.adress",
  new JsonObject()
  .putString("name", "Wolff")
  .putString("vorname", "Eberhard") );
  }

} 




Der Bus hat gerade im Zusammenspiel mit Modulen einige Vorteile. Module können so mit JSON oder einfachen Daten miteinander kommunizieren. So können Module Daten austauschen, selbst wenn sie in verschiedenen Sprachen geschrieben sind, da JSON von jeder Sprache interpretiert werden kann. Außerdem haben die Module keine gemeinsamen Klassen, sodass sie unabhängig voneinander deployt werden können. Und natürlich passt der Bus gut zu der asynchronen Kommunikation, die sich durch das gesamte vert.x-System zieht. Als Alternative zu dem Bus bietet vert.x auch an, dass Module sich Speicher teilen können. Das ist zum Beispiel sinnvoll, wenn sich Module einen Cache teilen wollen.

Fazit

vert.x führt mehrere Innovationen ein, die vor allem Auswirkungen auf die Strukturierung von Anwendung und die Architektur haben. Dabei könnte es wegweisend sein, weil es Trends wie polyglotte Programmierung und asynchrone Systeme aufgreift – und mit einem guten Modulkonzept kombiniert. Dabei nutzt es die Ansätzen, die von anderen Programmiersprachen und Systemen auch genutzt werden. So ergibt sich eine komplett neue Welt, die mit klassischem Java nicht mehr viel zu tun hat. Wer tiefer einsteigen will, dem seien die Beispiele aus der Distribution und die Dokumentation unter [3] ans Herz gelegt. Die Beschäftigung mit vert.x erweitert definitiv den Horizont!

Mehr zum Thema
Der Entscheidung, vert.x zur Eclipse Foundation zu bringen, ging eine hitzige Diskussion um die Rechte an dem Projekt und um Open-Source-Software im Allgemeinen voraus. Alle Hintergrundinformationen zur Entwicklung der Diskussion finden sich auf www.jaxenter.de (z. B. unter [4]). Ein Interview über vert.x mit Stuart Williams (VMware) gibt es unter http://bit.ly/Ye1u7J.

Geschrieben von
Eberhard Wolff
Eberhard Wolff
Eberhard Wolff ist Fellow bei innoQ und arbeitet seit mehr als fünfzehn Jahren als Architekt und Berater, oft an der Schnittstelle zwischen Business und Technologie. Er ist Autor zahlreicher Artikel und Bücher, u.a. zu Continuous Delivery und Microservices und trägt regelmäßig als Sprecher auf internationalen Konferenz vor. Sein technologischer Schwerpunkt sind moderne Architektur- und Entwicklungsansätze wie Cloud, Continuous Delivery, DevOps, Microservices und NoSQL.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.