Kolumne

Enterprise Tales: Nur eine Zeile Code …

Arne Limburg, Thorben Kuck

© S&S Media

Eine Annotation, wie z. B. @Transactional, ist während der Entwicklung (in der Regel) nur eine Zeile Code. In Enterprise Java (sei es JEE oder Spring) löst diese eine Zeile Code aber häufig viel Magie aus, die im Hintergrund passiert, wenn das Programm gestartet wird. Welche Auswirkungen hat das aber auf die Entwicklungsperformance? Wie kriegt man es hin, dass der lokale Entwicklungsserver einen Live Reload durchführt, ohne dass man lange Zeit auf das Rekompilieren und Neustarten der Applikation warten muss?

Annotations sind aus der Enterprise-Java-Entwicklung nicht mehr wegzudenken. Damit eine Annotation, die ich an eine Java-Klasse, ein Feld oder eine Methode schreibe, überhaupt eine Auswirkung hat und nicht nur schmückendes Beiwerk zu meinem Code ist, muss es etwas geben, das diese Annotation auswertet. Das passiert durch Metaprogrammierung.

Metaprogrammierung – die Grundlagen

Ganz allgemein ist Metaprogrammierung der Entwurf von Programmen, die andere Programme als ihre Daten behandeln. Im Grunde entwirft man ein Metaprogramm, das ein sogenanntes Objektprogramm inspiziert und/oder verändert. Das heißt ein Metaprogramm untersucht bestehende Codefragmente (z. B. um Annotations zu finden), verändert diese und erzeugt ggf. neue, eigene Codefragmente.

Das klingt zunächst kompliziert, erweist sich bei näherem Hinsehen aber als alltäglicher Vorgang. Metaprogrammierung wird von den Enterprise-Java-Technologien wie Spring oder Java EE schon seit vielen Jahren genutzt. Es ist nämlich genau der Mechanismus, der dafür sorgt, dass ein Feld tatsächlich befüllt wird, wenn wir @Inject dranschreiben, oder dass eine Transaktion gestartet und später committet wird, wenn wir @Transactional an eine Methode anfügen.

Technisch verwenden die Frameworks dafür Reflection als Werkzeug. Mit Reflection können zum Beispiel private Felder ausgelesen und verändert, private Methoden aufrufbar gemacht werden usw.
Metaprogrammierung wird unter anderem unterteilt in statische und dynamische Metaprogrammierung. Sie unterscheiden sich nur darin, wann die Metaprogrammierung durchgeführt wird. Werden die Metaprogramme zur Kompilierzeit oder erst zur Start-up- oder Laufzeit ausgeführt?

Auch wenn „dynamisch“ im ersten Moment besser klingt, da das Wort „statisch“ immer viele negative Konnotationen hat, wird dynamische Metaprogrammierung im Gegensatz zur statischen erst zur Start-up- oder Laufzeit durchgeführt. Das führt dazu, dass solche Programme lange brauchen, bis sie deployt/hochgefahren sind. Statische Metaprogrammierung hingegen passiert bereits zur Kompilierzeit, was die Start-up-Zeit deutlich beschleunigt.

Aktuelle Enterprise-Java-Frameworks setzen überwiegend auf dynamische Metaprogrammierung, was in der Praxis besagten Nachteil hat.

Dynamische Metaprogrammierung

Gute Beispiele für dynamische Metaprogrammierung sind CDI und Spring. Beim Starten werden am Anfang alle in der Laufzeit enthaltenen Klassen untersucht (das sogenannte Classpath Scanning). Dann werden auf Basis der gefundenen Annotationen weitere Klassen aufgebaut, die als Proxy-Klassen fungieren. So wird zum Beispiel, wenn eine Methode mit @Transactional annotiert ist, die Methode in einer erbenden Klasse überschrieben. In dieser Methode wird dann eine Transaktion gestartet.
Das Ganze erlaubt erst einmal, dass der Entwickler, der das Framework nutzt, sehr viel weniger Code schreiben muss als zuvor. Anstelle händisch mit der Datenbank zu interagieren, reicht eine Annotation, also eine Zeile Code.

Diese eine Zeile ermöglicht es, eine Transaktion zu starten und korrekt zu beenden, wobei automatisch je nach Ergebnis der Methode entschieden wird, ob die Transaktion committet oder zurückgerollt wird. Unter dem Ergebnis einer Methode wird hier verstanden, dass die Methode sich entweder „normal“ beendet oder eine Exception wirft.

Jedoch bedeutet dieses Vorgehen auch, dass es zur Laufzeit Klassen gibt, die nur dazu existieren, andere Klassen zu analysieren, neue Klassen zu erstellen und Fallunterscheidungen mit mehreren Annotationen zu machen. Der dabei entstehende Overhead wird besonders ersichtlich, wenn man sich große, monolithische Applikationen anschaut. Das Deployment einer solchen klassischen Java-EE-Applikation dauert in der Regel sehr lange. Zusätzlich sind alle Klassen, die für den Anfang zur Analyse erforderlich sind, danach unbrauchbar, bzw. sie werden nicht mehr benötigt. Aber was kann man da besser machen?

Statische Metaprogrammierung

Statische Metaprogrammierung ist das Pendant zu dynamischer Metaprogrammierung. Sie ermöglicht fast alle Dinge, die auch mit dynamischer Metaprogrammierung möglich sind, nur schon zur Kompilierzeit.

Auch hier haben wir ein Metaprogramm, das Objektprogramme untersucht. Jedoch brauchen wir in der endgültigen Applikation keinen Berg von Klassen, die Sachen machen, nur damit die Applikation starten kann. Stattdessen können wir das alles schon erledigt haben.

In Java gibt es ein relativ einfaches Mittel, das uns genau das erlaubt. Das Pluggable Annotation Processing API. Es existiert seit Java 6 und ist direkt in den Compiler integriert. Hier können wir eine Art Abstract-Syntax-Tree-Repräsentation der Klassen analysieren und neue Klassen erzeugen. Alles, was in aktuellen Frameworks wie CDI oder Spring beim Startup passiert, kann nach dem Kompilieren bereits erledigt sein. Zusätzlich können beim Kompilieren auch direkt Konfigurationsfehler, wie z. B. fehlende Beans für Injection Points, identifiziert werden. Solche Fehler treten dann direkt als Compile-Fehler auf, was es dem Entwickler ermöglicht, sie vor dem Starten der Applikation zu erkennen und zu beheben.

Vorteile statischer Metaprogrammierung

Aber statische Metaprogrammierung hat weitere Vorteile: Bei dynamischer Metaprogrammierung wird sehr stark Reflection eingesetzt. Das ist auch notwendig, denn die Objekte müssen zur Laufzeit analysiert werden. Wenn das aber bereits zur Kompilierzeit geschehen ist, so sparen wir auf verschiedenen Ebenen.
Bei dynamischer Metaprogrammierung müssen alle Klassen der Applikation bereits zu Beginn in den Class Loader (und damit in den RAM) geladen werden, um analysiert werden zu können. Das entfällt bei statischer Metaprogrammierung. Bei dieser muss nicht einmal der Code zur Analyse der Annotations und zur Generierung von Code mitdeployt werden. Er wird später eh nicht mehr benötigt. Auch Klassen der Applikation bzw. von verwendeten Bibliotheken, die zur Laufzeit nicht angesprochen werden, werden dann gar nicht erst in den Class Loader geladen.

Das spart sowohl Speicher als auch die Zeit, die der Class Loader benötigt, um alle Klassen in den Speicher zu laden. Ein weiterer Faktor, mit dem sich bei statischer Metaprogrammierung Ausführungszeit sparen lässt, ist das Fehlen der Reflection, die bei der dynamischen Metaprogrammierung benötigt wird. Reflection ist ein mächtiges Werkzeug, aber eines, das eigentlich nicht optimiert werden kann. Für gewöhnlich kann der Just-in-Time-Compiler der JVM einiges an Geschwindigkeit aus der Applikation herausholen. Bei der Verwendung von Reflection ist das aber nicht so leicht möglich.

Die Vorteile für den Entwickler sind also offensichtlich: nicht mehr auf den grünen Knopf drücken, einen Kaffee holen und hoffen, dass die Applikation dann schon läuft.

Warum also dynamisch?

Wenn die statische Metaprogrammierung der dynamischen also gerade in puncto Performance so überlegen ist, warum verwenden die Enterprise-Java-Standards dann überhaupt dynamische Metaprogrammierung? Darauf können wir mit der klassischen Antwort eines Entwicklers reagieren, der gefragt wird, warum die Architektur seiner Applikation so seltsam ist: „Das ist historisch gewachsen“.

Zu den Zeiten als Enterprise Java noch J2EE hieß war es das größte „Feature“ der Application Server, dass sie die oben beschriebene Magie übernahmen. Und genau das war auch das größte Diversifizierungsmerkmal zwischen den verschiedenen konkurrierenden Servern. Es gab noch keine Annotations und erst recht keinen Annotation Processor. Zudem war die ursprüngliche Idee von J2EE, plattformunabhängige Komponenten zur Verfügung zu stellen (der EJB-2-Standard lässt grüßen). Um Plattformunabhängigkeit mit statischer Metaprogrammierung zu erzielen, hätte man entweder viel von der Magie standardisieren müssen (also nicht nur was passiert, sondern auch wie), oder die Komponenten hätten je nach Server neu kompiliert werden müssen. Beides war nicht gewünscht.

In der heutigen Zeit sind diese Ziele aber obsolet. Die geplanten Komponentenmarktplätze, auf denen man EJBs kaufen und verkaufen kann, hat es nie gegeben. Stattdessen steht bereits zur Entwicklungszeit fest, auf welchem Server meine Applikation später laufen wird.

Aus heutiger Sicht spräche also nichts dagegen, bereits zur Entwicklungszeit Optimierungen vorzunehmen, die ein schnelleres Start-up- und Laufzeitverhalten ermöglichen. Aber man kann auch noch einen Schritt weiter gehen: Mit einem Annotation Processor werden die Klassen bereits zur Compile-Zeit generiert, warum also nicht die veränderten Klassen direkt zur Laufzeit austauschen? Und genau hier kommt Quarkus ins Spiel, wie wir gleich sehen werden.

Einige Entscheidungen in den JEE Specs haben allerdings auch dazu geführt, dass der statische Ansatz mittlerweile im JEE-Standard schwierig zu realisieren ist. Dazu zählen z. B. CDI-Extensions, die es ermöglichen, die Menge der verfügbaren Beans und die vorhandenen Annotations zur Start-up-Zeit zu verändern. Oder die Möglichkeit der CDI Spec, Injection-Informationen zur Laufzeit abzufragen. Hier wurde bei der Erstellung der Spec leider nicht genug Weitsicht an den Tag gelegt.

Das Projekt Quarkus hat diesen Schritt zurück mittlerweile aber gemacht und festgestellt, dass die Optimierung zur Kompilierzeit häufig tatsächlich die bessere Variante ist. Hier geht man sogar noch einen Schritt weiter. Quarkus erlaubt es, die Applikation einmal zu starten und Änderungen dynamisch nachzuladen. Ähnliches hat in der Vergangenheit nur JRebel erreicht, allerdings mit weniger medialer Aufmerksamkeit, was auch damit zusammenhängen könnte, dass es sich von vornherein um ein kommerzielles Produkt handelte.

Ein weiteres Feature von Quarkus ist es, native Images zu erstellen, die sich die statische Optimierung zunutze machen. Sie können dann über GraalVM ausgeführt werden. Das führt dann zu unglaublich geringen First-Response-Zeiten.

Fazit

Enterprise Java verwendet seit jeher dynamische Metaprogrammierung, um zur Startup-Zeit Annotations auszulesen und darauf basierend verschiedene Features wie Dependency Injection oder Transaktions-Handling zu realisieren. Das hat den Nachteil, dass die Start-up-Zeit solcher Anwendungen extrem hoch ist.

Sinnvoller wäre es, die dafür benötigten Klassen bereits zu generieren, während der Entwickler noch Code schreibt, um ihm einerseits direktes Feedback bei Fehlern zu geben und andererseits den Start-up der Anwendung deutlich zu beschleunigen.

In klassischen Enterprise-Java-Anwendungen ist ein solches Vorgehen eigentlich nicht vorgesehen. Quarkus geht jetzt aber genau diesen Weg und ermöglicht dadurch einerseits eine deutlich höhere Entwicklungsperformance und andererseits eine geringe Start-up-Zeit und hohe Laufzeitperformance. Der Nachteil ist, dass nicht alle JEE-Standardbibliotheken out ouf the box verwendet werden können.
Nachdem in Enterprise Java jahrelang der Ansatz der dynamischen Metaprogrammierung gesetzt war, auch weil die Start-up-Zeit eine untergeordnete Rolle spielte, kommt nun im Zuge von Cloud und Serverless mit GraalVM und Quarkus Bewegung in den Enterprise-Java-Markt. Der Trend scheint in die richtige Richtung zu gehen. Wie weit er sich fortsetzt, bleibt abzuwarten. In diesem Sinne, stay tuned.

Geschrieben von
Arne Limburg
Arne Limburg
Arne Limburg ist Softwarearchitekt bei der open knowledge GmbH in Oldenburg. Er verfügt über langjährige Erfahrung als Entwickler, Architekt und Consultant im Java-Umfeld und ist auch seit der ersten Stunde im Android-Umfeld aktiv.
Thorben Kuck

Thorben Kuck ist Enterprise Developer bei der OPEN KNOWLEDGE GmbH in Oldenburg. Er arbeitet an Microservices-Architekturen, dabei baut sein aktuelles Projekt auf Kafka. Zu seinen Interessen gehören verteilte Systeme und Metaprogrammierung.

Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: