Runtime-Modelle für Java EE bzw. Jakarta EE

Beim ersten Release des damals noch J2EE genannten Standards war das Betriebsmodell ganz klar so ausgerichtet, dass ein oder einige wenige Application Server als Träger von Enterprise-Applikationen genutzt werden sollten. Wie eine Art Betriebssystem stellten die Server die Infrastruktur – Transaktionen, Persistenz, Kommunikation, Isolation etc. – zur Verfügung, die dann von Anwendungen nach dem Deployment genutzt werden konnte.

Aus organisatorischen Gründen wurden Server aber zunehmend mit nur einer Anwendung betrieben. Hardware-Virtualisierung und leichtgewichtige Server machten es möglich, entsprechend viele Instanzen effektiv betreiben zu können.

Wir können heute also davon ausgehen, dass Server und Anwendung nicht unbedingt scharf voneinander getrennt sind, sonder eher verschiedene Aspekte einer Einheit darstellen. Man hört häufig die Meinung, dass damit das Konzept des Application Servers unbrauchbar geworden ist und moderne Serveranwendungen nur mit dedizierten Frameworks wie Spring Boot aufgebaut werden können. Ich möchte im Folgenden darstellen, dass das so nicht stimmt und JEE-Anwendungen sehr wohl in modernen Betriebsumgebungen ihren Platz haben.

Klassisches Deployment

Das klassische Deployment-Format sind WAR-Files. Ursprünglich mal für Web-Anwendungen gedacht (daher das W) ist dies seit Java EE 6 das General Purpose Deployment Format. Ein WAR-File wird in einen dafür vorgesehen Deployment-Ordner gelegt oder über spezielle Werkzeuge im Server deployt.

Da ein WAR-File nur die kompilierten Anwendungsklassen und – falls nötig – Bibliotheken außerhalb des Standards – enthält, sind die Deployments vergleichsweise klein. Eine Demo-Anwendung, die Sie in https://github.com/GEDOPLAN/jee-runtimes-demo finden, bringt gerade einmal 12 kB als WAR-File auf die Waage. Dabei enthält die Anwendung einen zwar einfachen, aber technisch kompletten Stack aus REST-API, Injektion mittels CDI und Persistenz mit JPA.

Das WAR-File kann auf einen beliebigen Jakarta-EE-Server deployt werden, z. B. auf einen WildFly 24. Der belegt im Vergleich viel Platz auf der Festplatte: ca. 250 MB.

Angenehm ist die Trennung von Anwendung und Infrastruktur. Sämtliche Laufzeit-Container befinden sich im Server, die Anwendung koppelt sich daran nur schwach über Interfaces und Standardklassen. Die Konfiguration von Subsystemen wie Datenbanken (oder Messaging, Mail, …) geschieht im Server. Die Anwendung referenziert die Konfiguration über logische Namen ohne konkrete Kenntnisse von bspw. Passwörtern.

Deployment als Kommando-Parameter

Der klassische Weg umfasst zwei Schritte: Installation eines Servers und Deployment der Anwendung. Manchen ist das zu aufwändig: sie würden einen einfachen Start des Servers inklusive der Anwendung in nur einem Schritt bevorzugen. Das ist mit JEE-Produkten auch recht leicht möglich.

Payara Micro ist ein solches Produkt. Es stellt einen Server in Form eines JAR-Files dar. Es ist nur ca. 77 MB groß. Die zu deployende Anwendung wird beim Start einfach als Parameter mitgegeben:

java -jar payara-micro.jar jee-runtimes-demo.war

Ähnlich funktioniert WildFly Bootable JAR. Hier kann der Server-Anteil sogar an die Bedürfnisse der Anwendung angepasst werden (im Beispiel-Projekt JAX-RS+CDI+JPA). Auch hier wird das Deployment durch einen Startparameter erledigt:

java -jar wildfly-bootable.jar --deployment=jee-runtimes-demo.war

Darüber hinaus bietet das zum Build genutzte Plugin sogar einen Development Mode an. Wird die Anwendung mit

mvn wildfly-jar:dev-watch

gestartet, überwacht sie kontinuierlich die Sourcen und deployt Änderungen automatisch.

Quarkus

Die bisherigen Modi nutzen alle ein Standard-WAR-File als Deployment-Einheit. Es ist also – wenn man so will – auch für Payara Micro und WildFly Bootable JAR immer noch ein Server vorhanden, auf den eine Anwendung deployt wird.

Quarkus geht den Weg (wie auch Spring Boot), die Anwendung mit dem Server zu einer ausführbaren Einheit zu verbinden. Die benötigten Serverbestandteile wie REST-, CDI- oder JPA-Implementierung werden im Build als einfache Dependencies eingebunden. Heraus kommt ein JAR-File, das einfach auf der Kommandozeile gestartet werden kann:

java -jar quarkus-app/quarkus-run.jar

Das JAR-File enthält übrigens nicht den gesamten Code der Anwendung, sondern referenziert ihn in dedizierten Unterverzeichnissen für den eigentlichen Anwendungscode und die Infrastruktur. Diese Trennung macht es leichter, bspw. Docker-Images zu erstellen und dabei nicht die eher statische Infrastruktur-Schicht bei jedem Rebuild neu erstellen zu müssen.
Das gesamte Verzeichnis quarkus-app ist für unsere Demo-Anwendung nur ca. 31 MB groß, ist also im Vergleich mit den bisherigen Serverlösungen nochmals erheblich verkleinert.

Quarkus verlegt einige Dinge aus der Laufzeit in die Build-Zeit, wie z. B. Entscheidungen über die Zuordnung von CDI Beans zu Injektionszielen. Damit wird der im Standard beim Anwendungsstart laufende, auf Reflection basierende Container-Start erheblich beschleunigt.

Unsere Demo-Anwendung startet damit in ca. 1,5 s (im Vergleich zu 4,7 s für WildFly Bootable JAR). Wem das immer noch nicht schnell genug ist, kann die Anwendung auch noch in Native Code übersetzen lassen. Dann sind Startzeiten im Millisekunden-Bereich erreichbar.

Auch Quarkus hat einen Development Mode, der ähnlich funktioniert, wie es oben für WildFly Bootable JAR beschrieben ist. Start mit

mvn quarkus:dev

Fazit

Java EE und Jakarta EE sind weit weg von den zunächst schwergewichtigen Serverlösungen der Anfangszeit (seitdem sind ja auch gut 20 Jahre vergangen!).

Wer eine Deployment-Einheit in Form eines WAR-Files bevorzugt, kann sie in einen klassischen Server deployen oder aber mit nur einem Kommando Server und Anwendung zugleich starten.

Es ist jedoch auch möglich, bei unverändertem Anwendungscode eine Anwendung mit eingebetteten Serveranteilen zu bauen, die recht klein ist und äußerst schnell startet.

Wer also heute noch behauptet, Java EE sei für moderne Anwendungen ungeeignet, hat vermutlich einige Jahre der JEE-Entwicklung verschlafen.

Weitere Informationen

Wir versorgen Sie gerne mit weiterem Input:

  • Unser Expertenkreis Java ist eine regelmäßige Vortragsveranstaltung zu allem aus dem Java-Ökosystem. Melden Sie sich kostenfrei an unter https://gedoplan.de/java-events/.
  • In die Tiefe gehen wir in unseren Seminaren, z. B. Power Workshop Jakarta EE oder Microservices mit Quarkus – kompakt, zu finden auf https://gedoplan.de.
  • Zweimal im Jahr finden Sie in unserem Firmenmagazin GEDOPLAN aktuell Fachartikel zu den Themen, die uns bewegen. Fordern Sie die Print-Ausgabe an oder lesen Sie online unter https://gedoplan.de/publikationen/.

Dirk Weil, GEDOPLAN GmbH

Von Java EE zu Jakarta EE

Enterprise Java wird in diesem Jahr 22 – zumindest wenn man den Standard betrachtet. Der Name Java EE wird durch Jakarta EE ersetzt. Was hat es damit auf sich?

Java EE

Ursprünglich als J2EE im Jahr 1999 released ist Java EE der Standard für Serveranwendungen in Java. Aufgrund der überbordenden technischen Komplexität der frühen Versionen bis J2EE 1.4 entwickelte sich zeitparallel das Spring Framework als mächtige Alternative. In Java EE 5 übernahm man die Konzepte von Spring und machte die Plattform ebenso leichtgewichtig. Viele Java-Entwickler*innen sehen Java EE zwar immer noch als schwergewichtig an, aber das lässt sich mit Blick auf den Code schon lange nicht mehr rechtfertigen.

Was man allerdings kritisieren konnte, war die recht langsame Weiterentwicklung der Plattform. Jeweils 3 bis 4 Jahre von einem Major Release zum nächsten sind für die schnell wachsenden Anforderungen des Marktes (Stichworte: Microservices, Cloud) nicht akzeptabel.

Bei der Weiterentwicklung der Java EE 7 entstand in den Jahren 2016 und 2017 eine irritierende Pause, als insbesondere die von Oracle geleiteten Teilspezifikationen nicht voran zu kommen schienen.

Die Java-Community war sich nicht sicher, was da passiert – oder eben auch nicht passiert. Würde Java EE eventuell sogar aufgegeben, „eingestampft“? Die Erlösung folgte 2017, als einerseits Java EE 8 veröffentlicht wurde und andererseits Oracle verkündete, dass die Plattform als Open-Source-Projekt an die Eclipse Foundation gehen würde.

Jakarta EE

Dieser guten Nachricht folgten leider zwei „Aber“: Oracle behielt die Rechte am Namen Java EE. Im Open-Source-Projekt entschied man sich mit Hilfe eines Community Votings für den neuen Namen Jakarta EE.

Dramatischer ist jedoch, dass sich Oracle und Eclipse Foundation nicht auf die weitere Nutzung des häufig auftretenden Paketnamen-Präfixes javax einigen konnten. Die bisherigen Pakete dürfen zwar weiter verwendet aber nicht verändert werden. Die Folge: In einem Big Bang werden sämtliche JEE-Pakete von javax.XYZ in jakarta.XYZ umbenannt. Da der Paket-Präfix nicht nur im JEE-Bereich genutzt wird, kann man nicht etwa in seinen Programmquellen javax. durch jakarta. ersetzen. Glücklicherweise unterstützen die gängigen IDEs recht gut bei der Migration.

Ebenfalls geändert werden XML-Namespaces (aus http://xmlns.jcp.org wird https://jakarta.ee) und Konfigurations-Properties (z. B. javax.persistence.jdbc.driver -> javax.persistence.jdbc.driver).

Diese Änderungen wurden in abgegrenzen Schritten durchgeführt:

  • Jakarta EE 8 wurde 2019 code-gleich zu Java EE 8 veröffentlicht. Geändert haben sich ausschließlich die Spezifikationsdokumente, in denen die dem Urheberrecht von Oracle unterlegernden Namen durch neue ersetzt wurden.
  • Jakarta EE 9 wurde 2020 mit unveränderter Funktionalität released, wobei die zuvor beschriebenen Umbenennungen von Paketen, Namespaces und Properties durchgeführt wurde.
  • Jakarta EE 10 wird lt. Plan Ende 2021 oder Anfang 2022 veröffentlicht. Dies ist das erste Release seit Java EE 8, das Änderungen der Funktionalität einführt. Die Details sind noch nicht finalisiert, aber relativ klar ist, dass CDI um nahezu alle Features ergänzt wird, die bislang nur in EJB zu finden sind. Zudem wird vermutlich Config aus MicroProfile nach Jakarta EE übernommen. Gute Chancen hat zudem die Integration von NoSQL.

Soviel für heute. Beim nächsten Mal werfe ich einen Blick auf einige Compatible Products, und zwar mit klassischen Deployments, JAR Deployments und Micro(profile) Runtimes – stay tuned!

Weitere Informationen

Wir versorgen Sie gerne mit weiterem Input:

  • Unser Expertenkreis Java ist eine regelmäßige Vortragsveranstaltung zu allem aus dem Java-Ökosystem. Melden Sie sich kostenfrei an unter https://gedoplan.de/java-events/.
  • In die Tiefe gehen wir in unseren Seminaren, z. B. mit dem Power Workshop Jakarta EE, zu finden auf https://gedoplan.de.
  • Zweimal im Jahr finden Sie in unserem Firmenmagazin GEDOPLAN aktuell Fachartikel zu den Themen, die uns bewegen. Fordern Sie die Print-Ausgabe an oder lesen Sie online unter https://gedoplan.de/publikationen/.

Dirk Weil, GEDOPLAN GmbH

Angular E2E mit json-server

Ein E2E-Test dient dazu eine Anwendung „von Vorne bis Hinten“ durchzutesten. Dabei sind viele Hürden zu meistern, angefangen von erreichbarer Infrastruktur, über Authentifizierung bis hin zu konstanten Datenbeständen. Das mag in einigen Projekten nur schwer und mit viel Aufwand realisiert bar sein. Ein Unit-Testing in Angular ist hier in vielen Fällen kaum ein ausreichender Ersatz. Also muss ein Mock her, um die Datenschnittstellen abzulösen. Eine praktikable Lösung: json-server

„Get a full fake REST API with zero coding“ das ist json-server. Die Grundlage dafür ist ein Node-Projekt: https://github.com/typicode/json-server + in der einfachsten Form eine projektspezifische JSON Datei die unseren Daten enthält. Eine solche Datei „db.json“:

  {
    "data": [
      {
        "id": 1,
        "message": "Lorem ipsum"
      },
      {
        "id": 3,
        "message": "Clita kasd gubergren"
      },
      {
        "id": 4,
        "message": "Takimata sanctus est Lorem"
      }
    ],
    "demo": {
      "message": "Demo Object"
    }
  }
  

und der Start von json-Server (Default Port: 4000) :

json-server --watch db.json

Fertig ist unser „backend“. In unserem Beispiel generiert json-server basierend auf unserer json-Datei zwei Rest-Schnittstellen: http://localhost:4000/data und http://localhost:4000/demo die sich direkt Abfragen lassen und die angegebenen Daten liefern. Aber nicht nur das. Auch POST/PUT/DELETE Funktionen werden direkt unterstützt. Sollte die zu simulierende Schnittstelle komplexer sein gibt es die Möglichkeit individuelle Routen anzugeben oder sogar programmatisch auf bestimmte Anfragen zu reagieren.

+ Protractor

Um das zusammen mit Protractor zu nutzen sind folgende Schritte zu implementieren

  1. für unsere Tests benötigen wir den laufenden JSON-Server. Charmant: wir starten den Server in der prepare Methode von Protractor mittels _child_process_
  onPrepare() {
    const { spawn } = require('child_process')
    const serverPath = require('path').join(__dirname, './json/server.js');
    spawn('node', [serverPath]);
    ...

(protractor.conf.js , wir verwenden hier ein individuelles json-server-Script, um wie bereits angesprochen programmatisch auf einige Anfragen zu reagieren, siehe server.js-Demo + Doku „Module“ )

2. wir verwenden eine proxy-Konfiguration um alle Anfragen an bestimmte URLs (unsre Backend) auf unseren json-server um zu leiten:

{
  "/api/*": {
    "target": "http://localhost:4000",
    "secure": false,
    "changeOrigin": true,
    "logLevel": "debug",
    "pathRewrite": {
      "^/api/": "/"
    }
  }
}
alle Anfragen an "http://.../api/... werden umgeleitet auf http://localhost:4000/...

3. eine solche Proxy-Konfiguration lässt sich eigentlich beim serve-Befehl mittels „-proxy-config“ übergeben. Leider unterstützt das E2E-Ziel diese Option nicht ( s. github issue ). Stattdessen müssen wir die entsprechende Konfiguration ergänzen die wir dann beim E2E-Target verwenden( angular.json ).

      "architect": {
       ...
        "serve": {... },
        "serve-e2e": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "ng-json-server:build",
            "proxyConfig": "e2e/proxy.conf.json",
            "port": 4300
          },

....
        "e2e": {
          "builder": "@angular-devkit/build-angular:protractor",
          "options": {
            "protractorConfig": "e2e/protractor.conf.js",
            "devServerTarget": "ng-json-server:serve-e2e"
          },
          "configurations": {
            "production": {
              "devServerTarget": "ng-json-server:serve-e2e:production"
            }
          }
        }

Vollständiges Beispiel @GitHub

Das war’s. Ein

npm run e2e

Startet nun unseren JSON-Server, konfiguriert den Development Server mit unserem Proxy und lässt alle E2E-Tests Laufen.

Alles. Live. In Farbe. @GitHub

Angular mit Zeitstempel und Version

Selbst bei einfachstem Test- und Release-Vorgehen sollte immer klar sein welche Version der Anwendung man gerade vor sich hat. Hilfreich ist es dabei einen Zeitstempel und/oder die aktuelle Version innerhalb der Anwendung sichtbar zu machen.

Version

Die Version unserer Anwendung finden wir in unser package.json. Diese Datei ist aber natürlich nicht Teil unseres ausgelieferten Bundles. Diese Datei beim build einfach zu kopieren und somit verfügbar zu machen mag einfach sein, offenbart aber potenziellen Angreifern eine Vielzahl von Informationen über unser Projekt und dessen Abhängigkeiten. Stattdessen importieren wir in unserer Komponente die package.json und ermöglichen unserem TypeScript Compiler die Version zu extrahieren:

import {version} from '../../package.json';
...
@Component({})
export class AppComponent {
  version: string;

Damit TypeScript diesen Zugriff auf eine einfache JSON-Datei per Import erlaubt, reicht es aus in unserer tsconfig folgenden Eintrag zu ergänzen:

    "resolveJsonModule": true,

Zeitstempel

Der Zeitstempel stellt tatsächlich eine größere „Herausforderung“ dar. Unser Vorgehen: ein Eintrag in unserer Environment soll soll dazu dienen den Zeitstempel in der Anwendung verfügbar zu machen. Damit lässt sich eine Ausgabe in unserer Komponente ganz einfach realisieren

    this.version = `${version} - ${environment.buildTime}`

Unsere environments bereiten wir mit einem entsprechenden Eintrag vor:

export const environment = {
  buildTime: '',
...

Diese Variable muss aber nun natürlich beim Build auch gesetzt werden. Dazu verwenden wir ein NPM-Tool:

replace-in-file – npm (npmjs.com)

Damit können wir mittels node-Script, einen beliebigen Text-Ersatz innerhalb von Dateien ausführen. Ein entsprechendes Script könnte so aussehen. Dabei ermöglichen wir auch das „zurücksetzen“ auf einen Default Wert. Hintergrund: wir wollen keine unnötigen Dateiänderungen in unserem Versionskontrollsystem nach einem Build:

let replace = require("replace-in-file");
let buildTime = new Date().toISOString();
let revert = process.argv[2];

const options = {
  files: "src/environments/*.ts",
  allowEmptyPaths: false,
  from: /buildTime.*/g,
  to: `buildTime: '${revert ? '' : buildTime}',`
};

try {
  replace.sync(options);
} catch (error) {
  console.error("Error while setting build time:", error);
}

Anpassung an unserem npm-build-script:

"build": "node ./replace.build.js 
           &&  ng build --prod --base-href . 
           && node ./replace.build.js revert",

…und schon landet eine kleine nette Info in unserer Anwendung: