Vielfach hört man die Meinung, dass moderne Anwendungen oder Microservices mit Jakarta EE nicht machbar seien und man daher seine Codebasis auf Spring Boot migrieren müsse. Wir werden im Folgenden sehen, dass das so nicht stimmt. Um Missverständnissen vorzubeugen: Es geht nicht darum, ob Spring Boot oder Jakarta EE das beste Enterprise-Framework ist – so etwas gibt es ohnehin nicht. Es geht vielmehr darum zu zeigen, dass man bei größtenteils unveränderter Codebasis moderne, schnelle und kleine (oder auch große) Anwendungen erstellen kann und dass das sogar richtig Spaß macht – pardon: eine gute „Developer Experience“ hat.
Programmiermodell
Werfen wir zunächst einen Blick darauf, wie wir Anwendungen mit Jakarta EE schreiben. Im Listing 1 ist ein beispielhafter Ausschnitt einer Web-Service-Klasse zu sehen. Wir arbeiten in JEE stark deklarativ, das heißt wir geben dem Code mit Annotationen wie @ApplicationScoped Eigenschaften mit, statt sie selbst zu implementieren. Die Verknüpfung der verschiedenen Komponenten untereinander programmieren wir ebenso nicht aus, sondern lassen benötigte Services per Dependency Injection mit @Inject anliefern.
Möglich wird diese Vorgehensweise dadurch, dass in einer JEE-Plattform standardisierte Subsysteme zur Verfügung stehen, die die jeweilige technische Implementierung enthalten und dabei die angeführten Annotationen als Steuerparameter konsumieren. So übernimmt z. B. RESTEasy als eine der sogenannten kompatiblen Implementierungen für RESTful Web Services die Aufgabe, Fachdaten wie Person-Objekte in HTTP-Requests zu packen und dabei die Annotationen @Path, @GET und @Produces für die Konfiguration von Zugriffspfaden, Methoden und Serialisierungsformen zu berücksichtigen. Für uns Entwickelnde bedeutet das, dass wir uns im Wesentlichen auf unsere Fachlogik konzentrieren können und uns die notwendige Technik ohne großen Aufwand zur Verfügung gestellt wird.
@ApplicationScoped @Path("persons") public class PersonResource { @Inject PersonRepository personRepository; @GET @Produces(MediaType.APPLICATION_JSON) public List<Person> get() { return this.personRepository.findAll();
Stay tuned
Regelmäßig News zur Konferenz und der Java-Community erhalten
Das klassische Laufzeitmodell
Davon unabhängig ist die Art und Weise, wie Anwendungen betrieben werden. Die klassische Weise nutzt einen Application Server wie WildFly, Open Liberty oder Payara. Die Anwendung wird von unserem Build-Tool – z. B. Maven – in ein Deploy-File paketiert, heute meist ein .war-File. Da es nur die Anwendung, einige wenige Konfigurationsdateien und eventuelle Third-Party Libraries enthält, ist das .war-File vergleichsweise klein. Es wird dann in den Server geladen – deployt –, der die Implementierung der Standardsysteme enthält und dadurch deutlich größer ist.
Diese Vorgehensweise wird heutzutage als schwergewichtig empfunden, was aber kaum an der Größe oder der Startgeschwindigkeit des Servers liegt – einige Hundert MB Installationsgröße schrecken uns kaum ab und die oben erwähnten Server starten alle in Sekunden. Es liegt vielmehr daran, dass ein Server zunächst installiert und konfiguriert werden muss, bevor man mit der Anwendung um die Ecke kommen kann. Es kommt erschwerend dazu, dass wir die Möglichkeit, mehrere Anwendungen auf einem Server zu deployen, meist aus organisatorischen Gründen gar nicht nutzen. Vielmehr betreiben wir häufig nur eine Anwendung pro Server, um so die Abhängigkeiten untereinander und zu Server- und Java-Versionen zu minimieren. Ein Server für jede Anwendung ist dann schon ein „schweres Gewicht“.
Viele setzen nun Jakarta EE mit Application Server gleich. Da letzterer schwergewichtig ist, ist Jakarta EE eben auch schwergewichtig. Das wäre mathematisch korrekt, wenn die Einstiegsannahme stimmen würde. Das ist aber nicht so: Wie wir im Folgenden sehen werden, können wir JEE-Anwendungen auch ohne Application Server betreiben.
Migration auf Quarkus
Wenn wir schon nur eine Anwendung pro Server haben, können wir das Ganze auch zu einer kompakten Einheit verschmelzen. Das Modell, den Server in die Anwendung zu integrieren, ist durch Spring Boot im großen Stil bekannt geworden. In seinem Schatten haben sich auch im JEE-Bereich ähnlich ausgerichtete Frameworks entwickelt, die aber nie annähernd die gleiche Popularität erlangt haben. Das ändert sich gerade mit Quarkus. Das Open-Source-Framework hat ähnliche Features wie Spring Boot, aber mit nahezu unveränderten JEE-Programmquellen. Zudem werden durch diverse Optimierungen Ressourcenverbrauch und Startzeiten erheblich reduziert und der Komfort während der Entwicklung deutlich verbessert.
Aber dazu später mehr – schauen wir uns zunächst an, welche Änderungen an einer bestehenden Jakarta-EE-Anwendung zu machen sind, um sie auf Quarkus zu migrieren.
In [1] finden Sie eine Anwendung in zwei Varianten: Im Verzeichnis std-jee liegt der Quellcode einer Standard-Jakarta-EE-Anwendung, die einen zwar kleinen, aber kompletten Stack aus RESTful Web Service, Injection-Container und Jakarta Persistence enthält. Als kleines Schmankerl ist sogar ein mittels Jakarta Faces gebautes Web-UI enthalten – sicher heute kein Mainstream, aber in vielen Altanwendungen durchaus enthalten. Details zur Anwendung können Sie aus der Projektdatei README.adoc entnehmen.
Im Verzeichnis quarkus finden Sie die gleiche Anwendung auf Basis von Quarkus. Schauen wir mal, welche Änderungen für die Migration von std-jee zu quarkus nötig waren. Die größte Änderung ergibt sich in der Projektdefinition für Maven: In der pom.xml wird jetzt statt des .war-Files ein .jar-File gebaut und wir fordern einzelne sogenannte Extensions für die zu nutzenden Basisdienste an – hier REST- und Injection-Container, Hibernate und PostgreSQL-Treiber (Listing 2).
<dependencies> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-reactive-jsonb</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-arc</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-hibernate-orm</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-jdbc-postgresql</artifactId> </dependency>
Weiterhin wird das quarkus-maven-plugin in den Build-Zyklus eingebaut. Es übernimmt die Aufgabe, die gebaute Anwendung in target/quarkus-app mit den notwendigen Bibliotheken zu sammeln und darüber hinaus einige Optimierungen zur Build-Zeit durchzuführen – dazu später mehr. Natürlich müssen Sie die pom.xml nicht selbst schreiben. Für den Bootstrap von Projekten gibt es Kommandozeilentools oder Erweiterungen Ihrer Lieblings-IDE. Details finden Sie unter [2]. Möchten Sie sich lieber das Projekt auf einer Webseite zusammenklicken? Dann schauen Sie mal auf [3].
Konzeptionell anders ist auch die Konfiguration der Anwendung. Bei einer klassischen JEE-Anwendung werden Logging-Einstellungen, DB-Verbindungen etc. auf dem Server konfiguriert. Das geschieht nun in der Projektdatei application.properties (Listing 3).
quarkus.log.level=WARN quarkus.log.category."io.quarkus".level=INFO quarkus.log.category."de.gedoplan.showcase".level=DEBUG %prod.quarkus.datasource.jdbc.url=jdbc:postgresql://${db.host:localhost}:5432/showcase %prod.quarkus.datasource.username=showcase %prod.quarkus.datasource.password=showcase
Das Präfix %prod. deutet auf ein sogenanntes Configuration Profile hin. Die so benannten Propertys gelten nur im Produktionmodus, das heißt wenn die Anwendung ganz normal gestartet wird. Welche anderen Configuration Profiles es gibt und warum dann auch eine DB-Verbindung verfügbar ist, sage ich Ihnen später. Sie finden .properties-Dateien doof und möchten lieber .yaml einsetzen? Geht auch!
Die Java-Klassen der Anwendung bleiben im Wesentlichen unverändert. Es sind allerdings ein paar Vereinfachungen möglich und sinnvoll, die später beschreibe.
NEUES AUS DER JAVA-ENTERPRISE-WELT
Serverside Java-Track entdecken
Bau und Start
Mit mvn package wird die Anwendung gebaut. Das oben erwähnte Plug-in erzeugt das Verzeichnis target/quarkus-app, in dem ein .jar-File zum Start und diverse Unterverzeichnisse mit den Anwendungsklassen und Bibliotheken liegen.
Durch java -jar target/quarkus-app/quarkus-run.jar wird die Anwendung gestartet (Listing 4). Wenn Sie das ausprobieren wollen, müssen Sie die PostgreSQL-DB zuvor gestartet haben.
java -jar target/quarkus-app/quarkus-run.jar __ ____ __ _____ ___ __ ____ ______ --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ 2023-10-27 14:07:57,406 INFO [io.quarkus] (main) quarkus 1.0-SNAPSHOT on JVM (powered by Quarkus 3.4.1) started in 2.954s. Listening on: http://0.0.0.0:8080 2023-10-27 14:07:57,416 INFO [io.quarkus] (main) Profile prod activated. 2023-10-27 14:07:57,417 INFO [io.quarkus] (main) Installed features: [agroal, awt, cdi, hibernate-orm, itext, jdbc-postgresql, myfaces, narayana-jta, poi, primefaces, resteasy-reactive, resteasy-reactive-jsonb, servlet, smallrye-context-propagation, vertx, websockets, websockets-client]
Vereinfachungen
Wenngleich der Code nahezu unverändert übernommen werden kann, gibt es doch ein paar Dinge, die gegenüber dem Standard kürzer oder einfacher sind. Für Jakarta Persistence (aka JPA) ist es zwar möglich, weiterhin einen Deployment Descriptor zu verwenden. Die Definition von Parametern für die DB-Verbindung und anderer Features des Persistenzproviders ist allerdings mit den oben gezeigten Properties viel kürzer, sodass Quarkus-Anwendungen i. A. keine persistence.xml mehr enthalten. Weiterhin enthält die Persistence Extension von Quarkus bereits einen Producer für einen Standard-Entity-Manager, sodass die Klasse EntityManagerProducer aus der Standardanwendung in der Quarkus-Anwendung entfällt.
Der Injection-Container ArC ist eine Implementierung von CDI Lite. Gegenüber CDI Full, das auf Standard-JEE-Servern angeboten wird, gibt es Einschränkungen, aber auch Erweiterungen [4]. So gibt es beispielsweise Erleichterungen für die Konstruktoren einer CDI Bean. Wenn Sie, wie viele unserer Mitstreitenden, statt der im Listing 1 genutzten Field Injection mit einem Konstruktor arbeiten wollen, müssen Sie im Standard
- einen passenden Konstruktor mit @Inject annotieren
- und einen Konstruktor ohne Parameter mindestens in protected-Sichtbarkeit ergänzen.
Für Quarkus dürfen Sie den No-argument Constructor weglassen. Wenn dann der andere Konstruktor der einzige ist, kann auch @Inject darauf entfallen (Listing 5).
@ApplicationScoped @Path("persons") public class PersonResource { private PersonRepository personRepository; // not necessary for Quarkus // @Inject public PersonResource(PersonRepository personRepository) { this.personRepository = personRepository; } // not necessary for Quarkus // protected PersonResource() { // }
Die REST-Klassen bleiben komplett unverändert. Eine Besonderheit stellt die sogenannte Application Class dar – im Beispielprojekt RestApplication. Sie dient häufig nur dazu, den Einstiegspfad für das REST API mit Hilfe der Annotation @ApplicationPath festzulegen. In Quarkus-Anwendungen kann die Klasse entfallen, wenn dieser Pfad / ist.
Hat die zu migrierende Anwendung Webressourcen wie .html-Files, .css-Files etc. im Quellordner src/main/webapp, bleiben diese wiederum unverändert, müssen aber an anderer Stelle abgelegt werden, da es den erwähnten Ordner nur für .war-Files gibt. Der Deployment Descriptor web.xml – soweit überhaupt vorhanden – gehört nun in src/main/resources/META-INF. Der restliche Inhalt von src/main/webapp kommt in src/main/resources/META-INF/resources.
Für die Nutzung von Jakarta Faces gibt es eine Quarkus Extension im Apache-Projekt myfaces. Und im sogenannten Quarkiverse, einer Sammlung von weiteren Quarkus Extensions, gibt es eine Extension für Primefaces. Beide werden im Beispielprojekt verwendet, sollen hier aber nicht weiter erläutert werden, um den Rahmen des Artikels nicht zu sprengen – vielleicht nur so viel: Auch das Faces-UI der Standardanwendung konnte im Wesentlichen unverändert für Quarkus übernommen werden.
Next Level
Das bisher Gezeigte ist schon mal sehr schön: Jakarta-EE-Anwendungen können also mit weitgehend unverändertem Code nach Quarkus migriert werden, was den JEE-Kundigen das weite Feld der Microservices und Cloud-Anwendungen eröffnet, ohne substanziell umlernen zu müssen. Dieser Punkt kann eigentlich nicht deutlich genug gesagt werden, bedeutet er doch nicht weniger, als dass wir unsere umfangreichen Kenntnisse und Erfahrungen mit Jakarta EE weiter nutzen können.
Aber wie war das denn nun mit dem Komfort in der Softwareentwicklung, der „Developer Experience“? Da bietet Quarkus uns tatsächlich einiges! Ich picke mir mal drei Dinge heraus:
Developer Mode
Quarkus bietet einen sogenannten Developer Mode, der mit mvn quarkus:dev gestartet wird. In diesem Modus überwacht die laufende Anwendung die Programmquellen. Sollten sich zum Zeitpunkt eines externen Requests (z. B. REST Call) Änderungen ergeben haben, erfolgt ein Hot Reload der Anwendung. Dadurch kann bei laufender Anwendung weiterentwickelt werden. Im Vergleich zum häufigen Redeployment einer klassischen Serveranwendung ist das wirklich eine große Erleichterung – wobei zur Ehrenrettung der Application Server gesagt werden muss, dass diese mittlerweile vergleichbare Modi anbieten.
Dev Services
Ist Ihnen aufgefallen, dass es im Developer Mode für unsere Beispielanwendung keine DB-Verbindungsparameter gibt? Die sind ja nur im Configuration Profile prod eingetragen, während der Dev Mode das Profil dev nutzt. Wieso funktioniert die Anwendung trotzdem und welche DB nutzt sie? Des Rätsels Lösung ist ein sogenannter Dev Service: Sind im Dev Mode keine Verbindungsparameter konfiguriert, wird eine passende Instanz als Container gestartet. Sie kennen dieses Verfahren vermutlich als Testcontainers aus dem Testumfeld. Quarkus bietet Dev Services für die meisten Datenbank-Extensions an, aber auch für Message Broker, Identity Manager etc. Durch Dev Services wird die Einstiegshürde für die Entwicklung von Anwendungsteilen mit Zugriff auf externe Subsysteme deutlich verringert: Ich muss nicht zuerst eine lokale Installation von PostgreSQL, Kafka oder Keycloak vornehmen, sondern kann direkt losentwickeln.
Test-Support
Werfen wir schließlich einen Blick auf (Integrations-)Tests. Die sind im Standardumfeld nicht gerade einfach zu schreiben, muss doch der Lifecycle der zu testenden Anwendung gesteuert werden: Server starten, Anwendung deployen, Tests laufen lassen, Anwendung undeployen, Server stoppen. Tools wie Arquillian [5] können das, sind aber durch die Vielfalt der Serverprodukte und Testverfahren vergleichsweise komplex. Man könnte auch sagen: So richtig Spaß macht’s nicht.
Für Quarkus-Anwendungen kann man stattdessen einfache JUnit-Testklassen schreiben und sie mit @QuarkusTest annotieren, wodurch aus dem Unit-Test ein Integrationstest wird. Die zu testende Anwendung erhält ihre Konfiguration aus dem Profil test mit eigenen Ports, Dev Services etc. Wenn man z. B. einen REST Endpoint testen möchte, ist das mit dem als Extension verfügbaren REST Assured möglich (Listing 6), das bereits auf die im Test genutzte Portnummer autokonfiguriert ist. Andere Clientbibliotheken sind aber natürlich auch verwendbar. Die Testklasse darf auch Injektionsziele enthalten, so dass neben Blackbox- auch Whitebox-Tests möglich sind (Listing 7).
@QuarkusTest public class PersonResourceTest { @Test void testGetAverageAge() { given() .when().get("/api/persons/avgAge") .then() .statusCode(200) .body(is(Double.toString(PersonRepositoryMock.getAvgAge()))); } }
@QuarkusTest public class PersonServiceTest { @Inject PersonService personService; @Test void testGetAverageAge() { assertEquals(PersonRepositoryMock.getAvgAge(), this.personService.getAverageAge(), 0.1); } }
Ein besonderes Goodie stellt aus meiner Sicht das sogenannte Continuous Testing dar, das im Developer Mode aktiviert werden kann. Dann laufen während der Entwicklung stets die Tests, die durch die aktuellen Codeänderungen berührt sind. Ein Blick auf die Shell, in der die Anwendung läuft, offenbart also ständig: „grün: alles OK“ und „rot: ups!“. Schneller geht ein Testfeedback kaum.
Augmentation
Eines bin ich Ihnen noch schuldig: Es war die Rede von Optimierung zur Build-Zeit – worum handelt es sich dabei? Wenn eine klassische JEE-Anwendung deployt wird, läuft zunächst eine Initialisierungsphase, in der insbesondere der CDI-Container die Anwendung nach Beans, Scopes, Producern, Observern u. Ä. scannt, Proxies generiert, Zuordnungen zu Injection Points vornimmt etc. Das ist durchaus zeitaufwendig – und es basiert im Wesentlichen auf Informationen, die schon zur Entwicklungszeit bekannt sind. Das Maven-Plug-in von Quarkus führt diese Aufgabe bereits zur Build-Zeit durch. Die Anwendung wird um Code angereichert (daher der Name Augmentation), der zur Laufzeit anstelle der oben angesprochenen Initialisierung ausgeführt wird. Im Ergebnis startet die Anwendung deutlich schneller.
Stay tuned
Regelmäßig News zur Konferenz und der Java-Community erhalten
Fazit und Ausblick
Mit Quarkus steht uns ein Serverframework zur Verfügung, das es ähnlich wie Spring Boot ermöglicht, Serveranwendungen aus Bausteinen aufzubauen. Bestehender Jakarta-EE-Code – und wichtiger vielleicht sogar, vorhandenes JEE-Wissen – kann weiterverwendet werden. Der Komfort bei der Softwareentwicklung steigt durch das angebotene Tooling. Alles ist leichtgewichtiger, geht schnell von der Hand, macht (wieder) Spaß.
Ich konnte in diesem Artikel Quarkus natürlich nicht erschöpfend behandeln. Es gibt mittlerweile ein großes, ständig wachsendes Ökosystem und Quarkus-Anwendungen lassen sich unter bestimmten Umständen auch nativ mit Startzeiten im Millisekunden-Bereich ausführen. Genug Futter für weitere Artikel also – stay tuned!
Links & Literatur
[1] https://github.com/GEDOPLAN/next-lvl-jee
[2] https://quarkus.io/guides/tooling