Es war einmal, da galten J2EE und insbesondere die Application Server als zu aufgebläht und schwergewichtig. Es konnte ziemlich mühsam und entmutigend für Softwareentwickler sein, diese Technologie zu benutzen. Aber spätestens seitdem der Name zu Java EE geändert wurde, ist diese Annahme nicht mehr wahr. Wo genau liegt der Unterschied von Java EE zu anderen Enterprise Frameworks und was macht ein Framework überhaupt leichtgewichtig?
Einer der wichtigsten Aspekte bei der Auswahl einer Technologie ist der Entwicklungsprozess und die daraus resultierende Produktivität. Programmierer sollten möglichst viel Zeit mit Lösen von Problemen und Implementieren von Use-Cases verbringen. Denn genau das schafft am Ende des Tages den Mehrwert eines Produktes bzw. den Profit einer Firma.
Das bedeutet wiederum, dass die Technologien und Methoden die Zeit, die Entwickler mit Warten auf Build-Prozesse, Tests, Deployments, aufwändigem Konfigurieren von Applikationen und Implementieren von nicht-Use-Case-relevanten „Klempnerarbeiten“, sowie Konfigurieren der Build-Umgebung und Abhängigkeiten der Applikation verbringen, minimieren sollten.
Standards
Einer der größten Vorteile, den Java EE gegenüber anderen Frameworks bietet, ist die Standardisierung der APIs. Standards klingen schwergängig und innovationsscheu — und genau das sind sie im Grunde auch, denn der JCP (Java Community Process) hält in den JSRs (Java Specification Requests) fest, was sich über längere Zeit davor in der Industrie etabliert hat. Doch genau diese Standards bieten eine Reihe von Vorteilen.
Zusammenspiel der einzelnen Technologien
Die verschiedenen APIs innerhalb des Java EE Umbrellas, wie zum Beispiel CDI, JPA, JAX-RS, JSONP oder Bean-Validation spielen sehr gut zusammen und können out-of-the-box miteinander kombiniert werden. Allen voran sorgt dafür CDI, das als „Kleber“ zwischen Komponenten fungiert. Die JSRs beinhalten Voraussetzungen wie: Sobald eine bestimmte Technologie auf dem Container zur Verfügung steht, muss diese auch mit der anderen Technologie nahtlos zusammenspielen.
Zwei Beispiele: JAX-RS unterstützt JSONP-Typen wie JsonObject als Request und Response Entities, beziehungsweise integriert nahtlos Bean Validation — passende HTTP-Status-Codes im Falle einer fehlgeschlagenen Validierung inklusive. Validatoren wiederum können per @Inject CDI managed Beans injizieren und so weiter und so fort — Integration der Spezifikationen ist ein großes Augenmerk des Java EE Umbrellas.
Diese Ansätze schaffen ein entwicklerfreundliches und produktives Arbeiten, da sich die Programmierer darauf verlassen können, dass der Application-Server die Integrations- und Konfigurationsarbeit leistet. Der Fokus kann somit auf der eigentlichen Businesslogik liegen.
Deklarative, von Convention-over-Configuration-getriebene Entwicklung
Der Convention-over-Configuration-Ansatz von Java EE sorgt dafür, dass das Gros der Anwendungsfälle ohne jegliche Konfiguration auskommt. Die Tage von Unmengen an XML-Descriptoren sind vorbei. Für die einfachste Java-EE-Anwendung ist keine einzige XML-Datei mehr notwendig.
Dank der deklarativen Annotations wird ein einfaches, annotiertes POJO (Plain-old-Java-Object) zum HTTP-Endpoint @Path bzw. zur Enterprise Bean @Stateless inklusive Transaktionen, Monitoring und Interceptoren. Diese Ansätze haben sich in der Vergangenheit in vielen Frameworks etabliert und wurden in Java EE zum Standard erhoben.
Externe Abhängigkeiten
Die wenigsten Enterprise-Projekte in der realen Welt kommen ganz ohne im Deployment-Artefakt mitgelieferte, externe Bibliotheken aus. Dazu gehören jedoch erfahrungsgemäß eher technisch-, als Use-Case-begründete Abhängigkeiten, allen voran Logging- oder Entity-Mapping-Frameworks oder Common-Purpose-
Libraries, wie Apache Commons oder Google Guava.
Java EE 7 – insbesondere in Verwendung mit Java 8 – liefert jedoch ausreichend APIs und Funktionalitäten mit, um die gängigen Use-Cases im Enterprise-Umfeld abzudecken. Was nicht out-of-the-box vorhanden ist, kann meist mit minimalem Code gelöst werden, z. B. benötigte, injizierte Configuration per CDI-Producer, Circuit Breaker per Interceptoren oder aufwendige Collection-Operationen mit Java 8 Lambdas und Streams.
Natürlich könnte man an dieser Stelle argumentieren, das Rad nicht neu zu erfinden. Es ergibt jedoch wenig Sinn, für eine handvoll eingesparter Lines of Code Megabytes an Code-Bibliotheken in das Projekt und Deployment-Artefakt zu ziehen. Dabei liegt erfahrungsgemäß das größte Problem noch nicht einmal in der Größe der damit eingeführten direkten Abhängigkeiten, sondern in den transitiven Abhängigkeiten. Letztere kollidieren in vielen Fällen mit anderen im Deployment-Artefakt oder dem Application-Server vorhandenen Versionen und sorgen für aufwändig zu lösende Konflikte.
Im Endeffekt verbringen die Entwickler mehr Zeit im Konfigurieren und Managen von Abhängigkeiten und eventuellem Ausschließen z. B. per Maven <exclude> Direktive, als die Zeit, die Programmierung und Test des Features in Anspruch genommen hätte. Das gilt selbstverständlich in erster Linie für die einfacheren (und doch meisten) Fälle und erfahrungsgemäß dann, wenn Abhängigkeiten technisch und nicht Use-Case begründet sind.
Ein Beispiel: In den wenigsten Fällen sieht die Business-Anforderung einer Applikation ein bestimmtes Logging-Framework vor, jedoch durchaus eine Integrations-Bibliothek eines externen Systems. Der Unterschied und die Legitimation, ob eine externe Abhängigkeit benutzt werden soll, liegt – neben der Komplexität -also in der fachlichen Notwendigkeit der Lösung. Generell kann ich die Empfehlung geben, auf Dependencies wenn es möglich ist zu verzichten, vor der Einführung die Vor- und Nachteile gut abzuwägen und in allen Fällen den Dependency-Tree des Projektes im Auge zu behalten.
Programmieren gegen APIs
Genauso wie die Empfehlung, auf Javacode-Ebene wenn möglich gegen vorhandene Interfaces statt Implementierungen zu entwickeln, hängen Java EE Applikationen im Idealfall nur von der API und nicht der Container-Implementierung ab. Das minimiert Fehler und Risiken und gewährleistet eine Portabilität zwischen Implementierungen. Auch wenn in Projekten die Portabilität meist nicht in Anspruch genommen wird oder falls doch, nicht unbedingt reibungslos vonstattengeht, ist sie eher als Rettungsboot denn als Garantie zu verstehen. Gerade für langfristige Projekte im Enterprise-Umfeld ist es unabdingbar, sich auf eine abwärtskompatible, möglichst portable Technologie verlassen zu können. Damit erreicht man langfristig die geringeren Wartungsbzw. Portierungsaufwände.
Da die Java EE API den Application-Servern bekannt ist, muss sie außerdem nicht beim Deployment mitgeliefert werden.
Im Artefakt ausgeliefert wird nur die tatsächliche Businesslogik – mit nur geringem Anteil an „Glue-Code“ und Cross-cutting-Concerns.
Schlanke Deploymentartefakte
Dadurch, dass in einem modernen Java-EE-Projekt die API nur als provided Dependency verwendet wird, landen im Deployment- Artefakt auch nur die projekteigenen Klassen. Das sorgt für sehr schlanke Artefakte im Kilobyte-Bereich, welche wiederum schnelle Build-Zeiten ermöglichen, da der Build-Prozess ohne ständiges Kopieren der Abhängigkeiten auskommt. Der Unterschied kann in der Praxis viele Megabyte und im Build einige Sekunden ausmachen. Summiert man die Zeit, die alle Entwickler bzw. die Continuous-Integration-Server im Laufe der Projektzeit dadurch mehraufwenden, so werden die Unterschiede deutlich. Je häufiger man das Projekt zusammenbaut, desto größer diese Ersparnis – gerade im Hinblick auf Continuous-Delivery.
Abgesehen von Build-Zeiten sorgen schlanke Artefakte auch für schnellere Deployment- bzw. Publish-Zeiten. In allen Fällen sind die Moving-Parts gering, da die Implementierung des Frameworks eben schon auf dem Application-Server vorhanden ist und nicht jedes Mal mit ausgeliefert wird.
Das ideale Framework für Docker
Für modernen, Container-basierte Umgebungen bietet Java EE aus diesem Grund die ideale Struktur. Das Base-Image enthält schon das Betriebssystem, die Java Runtime und den Application- Server und das Zusammenbauen des Container-Images fügt nur noch die letzte, wenig Kilobyte schlanke Image-Schicht des
Deployment-Artefakts hinzu. Diese Zeit- und Speicherplatz-Ersparnis im Vergleich zu Fat-WAR, bzw. Standalone-JAR-Ansätzen betrifft sowohl das Zusammenbauen als auch das Ausliefern und Versionieren der Images.
Moderne Applikationsserver
J2EE Application-Server waren der Inbegriff von schwergewichtiger Software in Hinblick auf Start- und Deployment-Zeiten, Installationsgröße und Ressourcen-Footprint. Doch seit der neuen Welt von Java EE entspricht diese Annahme nicht mehr der Wahrheit.
Alle moderne Java EE 7 Application-Server, wie Wildfly, Payara, Websphere Liberty Profile oder TomEE starten und deployen in wenigen Sekunden. Dafür sorgen fortgeschrittene Modul-Systeme, die nur die benötigten Komponenten initialisieren und die Tatsache, dass die Deployment-Artefakte sehr schlank gehalten sind.
Die Installationsgröße und der Footprint halten sich sehr in Grenzen. Ein Application Server verbraucht nicht viel mehr Speicher als ein Servlet-Container und liefert dann aber schon alle benötigten Enterprise-Funktionalitäten mit. So ist es mit den heutigen Servern möglich und sinnvoll, statt mehrere Deployments
auf einem Server, einfach mehrere Server mit jeweils nur einer Applikation laufen zu lassen – entweder in einem Container oder als herkömmliche Installation.
Packaging
Was das Packaging angeht, gibt es keinen wirklichen Grund, der noch für EAR-Archive spricht. Diese Archive beinhalten die Web- und Enterprise-Komponenten noch einmal verpackt als WAR-Archiv, bzw. EJB-JAR. Da der Ansatz, die komplette Applikation auf einem einzigen, dedizierten Application-Server laufen zu
lassen, voraussetzt, dass ohnehin alle Komponenten vorhanden sein müssen, lässt sich durch den Verzicht auf EAR-Archive und die zusätzliche „Verpackung“ Build- und Deployment-Zeit einsparen. Davon abgesehen vermeidet man so alle Arten von Classloader-Hierarchie-Fehlern, da alle Klassen in einem Kontext vorhanden sind.
In den neuen Microservices- und Cloud-Ansätzen werden Anwendungen vermehrt als Standalone-JAR, welches sowohl die Applikation als auch die Laufzeit beinhaltet, ausgeliefert. Das ist in der Java-EE-Welt dank Technologien wie Wildfly Swarm oder TomEE Embedded möglich.
Aus bereits genannten Gründen ist es jedoch sinnvoll, wenn möglich die Geschäftslogik, sprich die Anwendung, von der Implementierung zu trennen. Standalone-JARs sind meiner Meinung nach genau dann ein hilfreicher Workaround, wenn kein Einfluss auf die vorhandene Infrastuktur genommen werden kann, z. B. welche Versionen von Application-Servern zu Verfügung stehen oder die Installations- und Operations-Prozesse zu schwergängig sind. In diesen Fällen setzen die Standalone-JARs nur eine Java Runtime voraus und liefern den Rest selbst mit.
Dennoch ist die zu empfehlende Art des Packagings das WAR-Archiv, welches idealerweise nur die Applikation ohne externe Bibliotheken enthält. Ein sehr gutes Beispiel liefert der Maven Java EE 7 Essentials Archetype von Adam Bien.
Fazit
Die Tage von „schwergewichtigem“ J2EE sind schon länger vorbei. Die API des Java-EE-Umbrella bietet ein sehr gutes Entwicklungserlebnis und nahtlose Integration der vorhandenen Standards. Gerade der Ansatz, die Applikationen gegen die APIs zu programmieren und die Implementierung separat zu halten, sorgt in der Praxis für gut performende Entwicklungs- und Auslieferungsprozesse. Mit heutigem, modernen Java EE 7 zusammen mit Java 8 haben Entwickler daher eine produktive und ausgereifte Technologie zum Realisieren von Enterprise-Software an der Hand.
[accordion][/accordion]
Diesen Artikel finden Sie auch in der Erstausgabe der JAVAPRO. Einfach hier kostenlos bestellen.