Hallo allerseits, mein Name ist Dmitry, und heute werde ich darüber sprechen, wie ich aufgrund der Produktionsanforderungen einen
Beitrag zum Micronaut-Framework
geleistet habe . Sicher haben viele von ihm gehört. Kurz gesagt, dies ist eine leichte Alternative zu Spring Boot, bei der der Schwerpunkt nicht auf der Reflexion liegt, sondern auf der vorläufigen Zusammenstellung aller erforderlichen Abhängigkeiten. Eine detailliertere Bekanntschaft kann mit der offiziellen
Dokumentation beginnen .
Das Micronaut-Framework wird in mehreren internen Yandex-Projekten verwendet und hat sich recht gut etabliert. Was haben wir also vermisst? Ich kann sofort sagen: Das Framework unterstützt sofort alle Funktionen, die ein Programmierer theoretisch zur Entwicklung von Backends benötigt. Es gibt jedoch seltene Fälle, die nicht sofort unterstützt werden. Eine davon ist, wenn Sie nicht über HTTP arbeiten müssen, sondern mit der HTTP-Erweiterung. Zum Beispiel mit zusätzlichen Methoden. In der Tat sind solche Fälle viel mehr als es scheint. Darüber hinaus sind einige dieser Protokolle Standards:
- Webdav ist eine Erweiterung für den Zugriff auf Ressourcen. Zusätzlich zu Standardmethoden erfordert HTTP die Unterstützung zusätzlicher Methoden wie LOCK, PROPPATCH usw.
- Caldav ist eine Webdav-Erweiterung für die Arbeit mit Ereignissen vom Typ Kalender. Dieses Protokoll mit hoher Wahrscheinlichkeit befindet sich in den Anwendungen auf Ihrem Smartphone: zum Synchronisieren von Kalendern, Terminen usw.
Und die Liste ist nicht darauf beschränkt. Wenn Sie sich die
Registrierung von HTTP-Methoden ansehen, werden Sie feststellen, dass die HTTP-Methoden, die nur durch die RFC-Standards beschrieben werden, derzeit 39 sind. Und wie viele weitere Fälle gibt es ein selbstgeschriebenes Protokoll über HTTP. Daher ist die Unterstützung für nicht standardmäßige HTTP-Methoden weit verbreitet. Es kommt auch häufig vor, dass das von Ihnen verwendete Framework solche Methoden nicht unterstützt. Hier ist eine Diskussion zum Stapelüberlauf für
ExpressJS . Und hier ist die Pull-Anfrage auf dem Github für
Tornado . Nun, da Micronaut oft als leichte Alternative zu Spring positioniert ist, ist dies das gleiche Problem für
Spring .
Es ist nicht verwunderlich, dass wir in einem der Projekte, als wir Unterstützung für ein Protokoll benötigten, das HTTP in Bezug auf Methoden erweitert, mit demselben Problem für Micronaut konfrontiert waren, das wir seit langem für dieses Projekt verwenden. Es stellte sich heraus, dass es ziemlich schwierig ist, Micronaut dazu zu bringen, nicht standardmäßige Methoden zu verarbeiten.
Warum? Wenn Sie sich die Definition von HTTP-Methoden in Micronaut im Moment ansehen, werden Sie
feststellen, dass sie mit Enum und nicht mit einer Klasse festgelegt werden, wie dies beispielsweise in Netty der Fall ist (ich erwähne Netty nicht versehentlich, später wird es angezeigt) mehr als einmal). Um die Sache noch schlimmer zu machen, erfolgt der gesamte Serveraufrufabgleich durch Filtern nach Aufzählung und nicht nach dem Zeichenfolgennamen der Methode. Wenn Sie also eine nicht standardmäßige HTTP-Methode benötigen, müssen Sie diese in Enum schreiben. Dies ist eigentlich keine so gute Lösung für das Problem. Erstens muss jedes Mal, wenn Sie eine neue Methode benötigen, ein Commit für das Repository durchgeführt werden. Zweitens sind HTTP-Methoden nicht standardmäßig standardisiert und ihre Liste ist nirgendwo festgelegt. Daher ist es unrealistisch, alle möglichen Situationen vorherzusehen. Es ist notwendig, Micronaut zu zwingen, Methoden zu verarbeiten, die zuvor nicht von den Entwicklern bereitgestellt wurden.
Lösung eins: Stirn

Die erste und naheliegendste Lösung bestand darin, Micronaut überhaupt nicht zu berühren und nichts darin neu zu schreiben. Warum, weil Sie nronx wie wir vor Micronaut stellen können, ausgehend von einem
Beispiel :
http { upstream other_PROPPATCH { server ...; } upstream other_REPORT { server ...; } server { location /service { proxy_method POST; proxy_pass http://other_$request_method; } } }
Was ist der Punkt? Wir können nginx für nicht standardmäßige Methoden zwingen, auf den benötigten Proxy zuzugreifen, während wir die Fähigkeit von nginx nutzen, die Methode zu ändern. Das heißt, wir greifen über die POST-Methode zu, und Micronaut kann sie verarbeiten.
Was ist schlecht Zunächst machen wir alle Anfragen aus Sicht von Micronaut tatsächlich nicht idempotent. Vergessen Sie nicht, dass es bei nicht standardmäßigen Methoden auch eine solche Trennung gibt. Zum Beispiel ist REPORT idempotent, PROPPATCH nicht. Infolgedessen kennt das Framework die Art der Anforderung nicht, und der Programmierer, der den Code dieser Handler betrachtet, kann dies ebenfalls nicht ermitteln. Dies ist jedoch nicht einmal der Fall. Wir haben bereits eine Reihe von Tests, die die Anwendung automatisch auf Übereinstimmung mit dem gewünschten Protokoll überprüfen. Damit diese Tests mit einer solchen Lösung in einem Projekt funktionieren, müssen Sie eine von zwei Optionen auswählen:
- Erhöhen Sie das Nginx-Image zusätzlich zur Anwendung selbst mit den erforderlichen Einstellungen, damit die Tests auf Nginx und nicht auf Micronaut selbst zugreifen. Obwohl die Yandex-Infrastruktur es Ihnen sicherlich ermöglicht, zusätzliche Komponenten zu erhöhen, sieht es in diesem Fall so aus, als ob Overengineering nur für Tests gedacht ist.
- Schreiben Sie die Tests so um, dass sie nicht das gewünschte Protokoll testen, sondern sich auf die Pfade beziehen, zu denen Nginx umleitet. Das heißt, wir testen nicht das Protokoll, sondern den Mut seiner spezifischen Krückenimplementierung.
Beide Optionen sind nicht sehr schön, daher kam die Idee: Warum nicht Micronaut für den richtigen Zweck reparieren, umso mehr, dass eine solche Bearbeitung nicht nur für uns nützlich sein wird. Das heißt, ich wollte so etwas:
@CustomMethod("PROPFIND") public String process( // Provide here HttpRequest or something else, as standard micronaut methods ) { }
Und ich nahm diese Aufgabe fröhlich an, aber was ist am Ende passiert?
Lösung zwei: Schreiben wir alles neu!

In der Tat ist es viel einfacher, als es auf den ersten Blick scheint.
Das Commit ändert einfach HttpMethod von enum in class. Als Nächstes haben wir statische Methoden (hauptsächlich valueOf) innerhalb der Klasse erstellt, die für enum aufgerufen wurden. Und IDEA zusammen mit Gradle sorgte dafür, dass nichts kaputt ging.
Das Schwierigste dabei war DefaultUriRouter, da davon ausgegangen wurde, dass die Menge fixiert war, und ein Array von Pfadlisten für mögliche Methoden erstellt wurde. Dies musste für eine neue Implementierung aufgegeben werden. Aber im Allgemeinen stellte sich heraus, dass alles recht einfach war. Beachten Sie, dass Sie 240 Zeilen hinzufügen und 116 löschen mussten.
Das Problem ist, dass dies eine große Änderung ist. Ja, in der Praxis verwenden Sie in einem regulären Projekt mit Micronaut höchstwahrscheinlich nicht HttpMethod direkt im Code, und wenn Sie es verwenden, ist es unwahrscheinlich, dass Sie dort die Ordnungsmethode und andere spezifische Aufzählungsmethoden verwenden. Dies macht jedoch eine solche Änderung in Version 1.x immer noch nicht zulässig, insbesondere angesichts der Tatsache, dass all dies gestartet wurde, um einen eher seltenen Fall zu unterstützen. Aber für 2.x ist dies eine normale Bearbeitung, aber Sie müssen immer noch bis zu 2.x leben. Deshalb musste ich mehr Code schreiben ...
Lösung drei: evolutionär handeln

Tatsächlich können Sie die entsprechende
Pull-Anfrage für Version 1.3 sehen. Wie Sie sehen, musste ich ungefähr fünfmal mehr Code schreiben als für eine größere Änderung, und das ist kein Zufall. Hier möchte ich die Standardmethoden in Schnittstellen loben, die in achtem Java eingeführt wurden. Für ein solches Refactoring, das die Abwärtskompatibilität nicht beeinträchtigt, ist dieses Ding unersetzlich, und ich kann mir nicht vorstellen, wie ich diese Änderungen für Java bis zur achten Version vornehmen würde (obwohl seltsamerweise eine größere Änderung vor der achten vorgenommen werden könnte).
Die grundlegenden Änderungen basierten auf der Tatsache, dass die HttpRequest-Schnittstelle eine getMethod-Methode hatte, die zum Filtern verwendet wurde. Er kehrte zurück, wie Sie sich vorstellen können, enum. Daher wurde der Schnittstelle die Standardmethode getHttpMethodName hinzugefügt, die standardmäßig den Namen des Aufzählungswerts zurückgibt. Dann fanden sie heraus, wo die ursprüngliche Methode für den Pfadabgleich verwendet wurde, und dort wurde sie durch Aufrufe der neuen Methode ersetzt. In den Implementierungen der Schnittstelle für den Netty-Server wurde die Schnittstellenmethode neu definiert, um den tatsächlichen Wert der HTTP-Methode zu verwenden.
Es enthielt eine Falle, die
in der Diskussion zu sehen ist, und betrifft die deklarativen Kunden von Micronaut. Sie verwenden die Konvertierung des Namens des Aufzählungswerts in eine Instanz der HttpMethod-Klasse für Netty. Wenn Sie sich die Dokumentation zur
valueOf- Methode in dieser Klasse
ansehen , werden Sie feststellen, dass der zwischengespeicherte Wert für Standardmethoden zurückgegeben wird und für nicht standardmäßige Methoden jedes Mal eine neue Instanz der Klasse zurückgegeben wird. Das heißt, wenn Sie eine hohe Last haben und sich millionenfach mit einer nicht standardmäßigen HTTP-Methode an den Server wenden, erstellen Sie gleichzeitig eine Million neuer Objekte. Natürlich sollten moderne GCs damit umgehen, aber ich möchte trotzdem keine zusätzlichen Objekte wie diese erstellen. Dann kam die Idee auf,
ConcurrentHashMap.computeIfAbsent für das Caching zu verwenden, aber auch hier ist es nicht so einfach: Das Problem liegt im Defekt von
Java 8 , der dazu führt, dass Streams blockiert werden, selbst wenn keine Aufzeichnung durchgeführt wird. Infolgedessen haben wir eine Zwischenentscheidung getroffen:
- Für Standardmethoden verwenden wir das Instanz-Caching, das Netty bereitstellt (tatsächlich wie zuvor).
- Lassen Sie bei nicht standardmäßigen Methoden neue Instanzen erstellen. Diejenigen, die nicht standardmäßige Methoden wählen, sollten sicherstellen, dass der Garbage Collector die Erstellung von Objekten verarbeiten kann (wir verwenden beispielsweise Shenandoah).
Schlussfolgerungen

Was kann am Ende gesagt werden?
- Die bekannte Fehlerkorrekturkostenkurve in verschiedenen Stadien der Softwareentwicklung hat sich hier sehr deutlich gezeigt. Insbesondere sprechen wir von einer Fehlkalkulation in einem sehr frühen Stadium der Entwicklung von Micronaut, als beschlossen wurde, Enum für HTTP-Methoden zu verwenden. Es ist schwer zu sagen, wie diese Entscheidung gerechtfertigt ist, da Micronaut sich auf Netty dreht, wo die Klasse für dieselbe verwendet wird. Im Wesentlichen wäre es die zusätzliche Arbeit nicht wert, eine Klasse anstelle von Enum zu unterhalten. Aus diesem Grund stellte sich heraus, dass es einfacher war, eine größere Änderung an diesem Plan vorzunehmen, als ihn mit Unterstützung der Abwärtskompatibilität zu beheben.
- Die bekannte Achillesferse von Open-Source-Projekten (dies kann jedoch auch bei Industrieprojekten mit geschlossenem Code beobachtet werden) - sie haben keine Projektdokumentation. Gleichzeitig verfügt Micronaut tatsächlich über eine sehr gute Dokumentation: Welche Verwendungsmöglichkeiten gibt es und dergleichen? Hier geht es jedoch darum zu dokumentieren, wie Entwurfsentscheidungen getroffen wurden. Infolgedessen ist es für den Programmierer von außen ziemlich schwierig, sich an der Entwicklung des Projekts zu beteiligen, selbst wenn eine leichte Verbesserung erforderlich ist.
- Vergessen Sie nicht, die Tatsache zu berücksichtigen, dass das eine oder andere Open Source-Projekt in Hochlast- und Multithread-Umgebungen verwendet wird. Hier war es notwendig, dies auch für eine kleine Verbesserung zu berücksichtigen.
PS
Während dieser Artikel für die Veröffentlichung vorbereitet wurde, wurde die Pull-Anforderung in den Zweig des Micronaut-Assistenten aufgenommen und wird in Version 1.3 veröffentlicht.