Vert.x 2: das reaktive, modulare Framework
Seite 2: Architektur
Die Architektursicht
Doch wie geht Vert.x mit diesen Problemen um. Verdeutlicht sei das an einer groben Architekturskizze.
Vert.x ist ein modularisiertes Framework – offensichtlich von OSGi inspiriert. Module und ihre Bestandteile folgen dabei etwas anderen Regeln als zum Beispiel in einem Java-EE-Container:
- Ein Modul kann Abhängigkeiten zu anderen Modulen definieren.
- Ein Modul kann entweder "runnable" sein und eigene Verticles starten oder "non-runnable" und als Dependency-Quelle dienen.
- Module sind versioniert.
- Module werden in Repositories verwaltet (derzeit werden Maven und Bintray unterstützt).
Die Parallelen zu OSGi werden beim Blick auf die Classloader-Strategie noch deutlicher. Statt des Parent-First-Vorgehens der JVM wird in Vert.x zuerst der eigene Classloader befragt, bevor an den Parent delegiert wird. Dieses Vorgehen ergibt nur Sinn, wenn man auch eine entsprechende Classloader-Hierarchie hat. Abbildung 3 zeigt, wie diese in Vert.x aussieht.
Jedes Modul hat seinen eigenen Classloader. Das ermöglicht den Parallelbetrieb mehrerer Version des Moduls – ohne die üblichen Probleme. Der Parent eines Moduls ist in den meisten Fällen der Vert.x-Classloader. Sollte ein Modul andere Module inkludieren, werden deren Classloader als Parent hinzugefügt. Die von einem Modul gestarteten Verticles haben pro Typ einen gemeinsamen Classloader. Alle Instanzen von Verticle 1 haben also einen anderen gemeinsamen Classloader als die von Verticle 2. Durch diese zusätzliche Ebene werden die Verticle-Typen voneinander isoliert. Das verhindert, dass statische Variablen zum Datenaustausch missbraucht werden.
Verticles und Handler
Verticles sind die ausführende Einheit in Vert.x. Sie können Server starten, Module deployen beziehungsweise entfernen, andere (Worker)Verticle starten und Handler registrieren. Ihre wichtigste Eigenschaft liegt allerdings darin, wie der Container mit ihnen umgeht: Vert.x garantiert, dass der Code eines Verticles immer von exakt dem gleichen Thread ausgeführt wird. Somit ist alles, was in einem Verticle oder einem von ihm registrierten Handler passiert, sicher vor den üblichen Problemen bei der Serverprogrammierung. Zuerst aber mal ein Codebeispiel:
import org.vertx.java.core.Handler;
import org.vertx.java.core.http.HttpServerRequest;
import org.vertx.java.platform.Verticle;
public class JavaServerVerticle extends Verticle {
private long requestsReceived = 0l;
@Override
public void start() {
vertx
.createHttpServer()
.requestHandler(new Handler<HttpServerRequest>() {
@Override
public void handle(HttpServerRequest request) {
requestsReceived++;
request.response().write("Requests received:↵
"+requestsReceived);
}
})
.listen(8080);
}
}
Der Reihe nach passiert Folgendes:
- createHttpServer(): Vert.x erzeugt einen Netty-basierten HTTP-Server.
- requestHandler(..): Es wird ein Handler registriert, der auf eingehende Requests durch das Hochzählen eines Zählers und das Zurückschicken einer entsprechenden Antwort reagiert.
- listen(8080): Durch diesen Aufruf wird der Server an Port 8080 gebunden und gestartet.
Zum Vergleich die gleiche Funktionalität als Servlet umgesetzt:
import java.io.*;
import javax.servlet.http.*;
import javax.servlet.*;
import java.util.concurrent.atomic.AtomicLong;
public class ExampleServlet extends HttpServlet {
private AtomicLong requestsReceived = new AtomicLong(0l);
protected void doGet( HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("Requests received:" + requestsReceived.incrementAndGet());
out.close;
}
}
Was sofort ins Auge sticht, ist die unterschiedliche Implementierung des requestsReceived-Zählers. In der Vert.x-Variante muss sich der Entwickler um Thread-Sicherheit nicht kümmern. Wie bereits erwähnt wird ein Verticle samt der von ihm registrierten Handler nie von mehr als einem Thread gleichzeitig aufgerufen.
Ganz anders die Servlet-Variante. Hier gibt es keine solche Garantie. Tatsächlich müssen sämtliche Servlet-Methoden (doGet, doPost ...) Thread-sicher implementiert sein. In diesem Fall bedeutet das, dass beliebig viele Threads die doGet-Methode parallel nutzen und der requestReceived-Zähler entsprechend zu schützen ist.
Über den Lebenszyklus hinweg
Den Lebenszyklus eines Verticle macht eine einfach API zugänglich:
public abstract class Verticle {
protected Vertx vertx;
protected Container container;
public void start() ... ;
public void start(Future<Void> startedResult) ... ;
public void stop() ... ;
}
Drei Lifecycle-Methoden stehen dem Entwickler zur Verfügung. Durch den Aufruf von start()/stop signalisiert der Container dem Verticle, dass es seine Arbeit aufnehmen oder einstellen soll. Manchmal kann das Starten eines Moduls etwas länger dauern. In solchen Fällen muss der Entwickler die start(Future startedResult)-Methode überschreiben und durch den Aufruf von startedResult.setResult(..) oder startedResult.setFailure(..) den Ausgang der Start-Operation signalisieren.
Die vertx-Variable bietet Zugriff auf die Vert.x-Instanz, in der das Verticle läuft. Über diese lassen sich Server, Netzwerk-Clients und Timer starten, und sie bietet Zugriff auf den EventBus, der später noch mal zur Sprache kommt.
Über die container-Variable bekommt man Zugriff auf Management-Operationen wie das Deploy oder Undeploy eines Moduls und das Starten beziehungsweise Stoppen von Verticles.