Eine Einführung in Vert.x, das schnellste Java-Framework von heute

Wenn Sie kürzlich das „beste Webframework“ gegoogelt haben, sind Sie möglicherweise auf die Techempower-Benchmarks gestoßen, bei denen mehr als dreihundert Frameworks eingestuft sind. Dort haben Sie vielleicht bemerkt, dass Vert.x zu den am besten bewerteten gehört, wenn nicht sogar zu den ersten.

Reden wir also darüber.

Vert.x ist ein polyglottes Webframework, das gemeinsame Funktionen für die unterstützten Sprachen Java, Kotlin, Scala, Ruby und Javascript verwendet. Unabhängig von der Sprache arbeitet Vert.x auf der Java Virtual Machine (JVM). Da es modular und leicht ist, ist es auf die Entwicklung von Microservices ausgerichtet.

Techempower-Benchmarks messen die Leistung beim Aktualisieren, Abrufen und Bereitstellen von Daten aus einer Datenbank. Je mehr Anfragen pro Sekunde bearbeitet werden, desto besser. In einem solchen E / A-Szenario, in dem nur wenig Datenverarbeitung erforderlich ist, hätte jedes nicht blockierende Framework einen Vorteil. In den letzten Jahren ist ein solches Paradigma fast untrennbar mit Node.js verbunden, das es mit seiner Single-Threaded-Ereignisschleife populär gemacht hat.

Vert.x betreibt wie Node eine einzelne Ereignisschleife. Vert.x nutzt aber auch die JVM. Während Node auf einem einzelnen Kern ausgeführt wird, verwaltet Vert.x einen Thread-Pool mit einer Größe, die der Anzahl der verfügbaren Kerne entspricht. Mit einer besseren Unterstützung für Parallelität eignet sich Vert.x nicht nur für E / A-Prozesse, sondern auch für CPU-lastige Prozesse, die paralleles Computing erfordern.

Ereignisschleifen sind jedoch die halbe Wahrheit. Die andere Hälfte hat wenig mit Vert.x zu tun.

Um eine Verbindung zu einer Datenbank herzustellen, benötigt ein Client einen Connector-Treiber. Im Java-Bereich ist JDBC der häufigste Treiber für SQL. Das Problem ist, dass dieser Treiber blockiert. Und es blockiert auf Socket-Ebene. Ein Thread bleibt dort immer hängen, bis er mit einer Antwort zurückkehrt.

Es ist unnötig zu erwähnen, dass der Treiber ein Engpass bei der Realisierung einer vollständig nicht blockierenden Anwendung war. Glücklicherweise wurden bei einem asynchronen Treiber mit mehreren aktiven Gabeln (wenn auch inoffizielle) Fortschritte erzielt, darunter:

  • //github.com/jasync-sql/jasync-sql (für Postgres und MySql)
  • //github.com/reactiverse/reactive-pg-client (Postgres)

Die goldene Regel

Vert.x ist ziemlich einfach zu handhaben, und ein http-Server kann mit ein paar Codezeilen aufgerufen werden.

In der Methode requestHandler liefert die Ereignisschleife das Anforderungsereignis. Da Vert.x keine Meinung hat, ist die Handhabung frei. Beachten Sie jedoch die einzige wichtige Regel für nicht blockierende Threads: Blockieren Sie sie nicht.

Wenn wir mit Parallelität arbeiten, können wir auf so viele Optionen zurückgreifen, die heute verfügbar sind, wie Promise, Future, Rx sowie Vert.x 'eigene Redewendung. Mit zunehmender Komplexität einer Anwendung reicht es jedoch nicht aus, nur über asynchrone Funktionen zu verfügen. Wir brauchen auch die Leichtigkeit, Anrufe zu koordinieren und zu verketten, die Rückrufhölle zu vermeiden und Fehler ordnungsgemäß zu übermitteln.

Scala Future erfüllt alle oben genannten Bedingungen mit dem zusätzlichen Vorteil, dass es auf funktionalen Programmierprinzipien basiert. Obwohl dieser Artikel Scala Future nicht ausführlich behandelt, können wir ihn mit einer einfachen App ausprobieren. Angenommen, die App ist ein API-Dienst, mit dem ein Benutzer anhand seiner ID gefunden werden kann:

Es gibt drei Operationen: Überprüfen des Anforderungsparameters, Überprüfen, ob die ID gültig ist, und Abrufen der Daten. Wir werden jede dieser Operationen in eine Zukunft einwickeln und die Ausführung in einer "zum Verständnis" -Struktur koordinieren.

  • Der erste Schritt besteht darin, die Anforderung einem Dienst zuzuordnen. Scala verfügt über eine leistungsstarke Mustervergleichsfunktion, die wir für diesen Zweck verwenden können. Hier fangen wir jede Erwähnung von "/ user" ab und geben sie an unseren Service weiter.
  • Als nächstes folgt der Kern dieses Dienstes, bei dem unsere Zukunft in einem sequentiellen Verständnis angeordnet ist. Die erste zukünftige f1- Wraps-Parameterprüfung. Wir möchten speziell die ID aus der get-Anfrage abrufen und in int umwandeln. (Scala erfordert keine explizite Rückgabe, wenn der Rückgabewert die letzte Zeile in der Methode ist.) Wie Sie sehen, kann diese Operation möglicherweise eine Ausnahme auslösen, da id möglicherweise kein int ist oder nicht einmal verfügbar ist, aber das ist vorerst in Ordnung .
  • Die zweite Zukunft f2 überprüft die Gültigkeit von id. Wir blockieren jede ID unter 100, indem wir Future.failed explizit mit unserer eigenen CustomException aufrufen. Andernfalls übergeben wir eine leere Zukunft in Form von Future.unit als erfolgreiche Validierung.
  • Die letzte Zukunft f3 ruft den Benutzer mit der von f1 angegebenen ID ab . Da dies nur ein Beispiel ist, stellen wir keine Verbindung zu einer Datenbank her. Wir geben nur einen Scheinstring zurück.
  • map führt die Anordnung aus, die die Benutzerdaten von f3 liefert, und druckt sie dann in die Antwort.
  • Wenn nun in einem Teil der Sequenz ein Fehler auftritt, wird ein Throwable zur Wiederherstellung übergeben . Hier können wir seinen Typ einer geeigneten Wiederherstellungsstrategie zuordnen. Rückblickend auf unseren Code haben wir mehrere potenzielle Fehler erwartet, z. B. fehlende ID oder ID, die nicht int oder ungültig war und bestimmte Ausnahmen auslösen würde. Wir behandeln jeden von ihnen in handleException, indem wir eine Fehlermeldung an den Client übergeben.

Diese Anordnung bietet nicht nur einen asynchronen Fluss von Anfang bis Ende, sondern auch einen sauberen Ansatz für die Behandlung von Fehlern. Und da es für alle Handler optimiert ist, können wir uns auf wichtige Dinge wie die Datenbankabfrage konzentrieren.

Verticles, Event Bus und andere Fallstricke

Vert.x bietet auch ein Parallelitätsmodell namens Verticle an, das dem Actor-System ähnelt. (Wenn Sie mehr erfahren möchten, lesen Sie meinen Akka Actor-Leitfaden.) Verticle isoliert seinen Status und sein Verhalten, um eine thread-sichere Umgebung bereitzustellen. Die einzige Möglichkeit, mit ihm zu kommunizieren, ist über einen Ereignisbus.

Für den Vert.x-Ereignisbus müssen die Nachrichten jedoch String oder JSON sein. Dies macht es schwierig, beliebige Nicht-POJO-Objekte zu übergeben. In einem Hochleistungssystem ist der Umgang mit der JSON-Konvertierung unerwünscht, da dadurch einige Rechenkosten entstehen. Wenn Sie E / A-Anwendungen entwickeln, ist es möglicherweise besser, weder Verticle noch Event Bus zu verwenden, da für solche Anwendungen nur wenig lokaler Status erforderlich ist.

Die Arbeit mit einigen Vert.x-Komponenten kann ebenfalls eine große Herausforderung sein. Möglicherweise finden Sie mangelnde Dokumentation, unerwartetes Verhalten und sogar Funktionsstörungen. Vert.x könnte unter seinen eigenen Ambitionen leiden, da für die Entwicklung neuer Komponenten eine Portierung in viele Sprachen erforderlich wäre. Dies ist ein schwieriges Unterfangen. Aus diesem Grund wäre es am besten, am Kern festzuhalten.

Wenn Sie eine öffentliche API entwickeln, sollte vertx-core ausreichen. Wenn es sich um eine Web-App handelt, können Sie vertx-web hinzufügen, das die Behandlung von http-Parametern und die JWT / Session-Authentifizierung bietet. Diese beiden dominierten ohnehin die Benchmarks. Bei einigen Tests zur Verwendung von vertx-web ist eine gewisse Leistungsminderung zu verzeichnen. Da dies jedoch auf die Optimierung zurückzuführen zu sein scheint, wird es in den nachfolgenden Versionen möglicherweise ausgebügelt.