Leichtgewichtiges, businessorientiertes Java EE 8
Worauf sollten sich Enterprise-Projekte konzentrieren? Was genau ist leichtgewichtig an der Java EE Platform? Wie unterstützt Java EE business-orientierte Entwicklung und Konzepte von Domain-Driven Design? Und wie hat sich die Plattform entwickelt? Und in welche Richtung geht es weiter? Dieser Artikel wirft einen Blick auf die neuen Features und die Integration in Container-Umgebungen.
Softwareentwicklung: Fokus von Enterprise-Projekten
Enterprise-Software sollte sich in erster Linie auf Features konzentrieren, die am Ende des Tages Umsatz generieren. Den Mehrwert bringt die Realisierung der Anwendungsfälle, der Geschäftslogik, weniger die Art und Weise der technischen Implementierung oder die gewählte Technologie. Enterprise-Projekte sollten daher in erster Linie auf die Implementierung von Anwendungsfällen fokussiert sein.
Entwickler sehen das oft anders. Sie sorgen sich um Implementierungsdetails, die Qualität und auch Eleganz von technischen Lösungen. Es wird designed, gefachsimpelt, Code überarbeitet und umstrukturiert. Das ist sehr wohl ein wichtiger Aspekt der Softwareentwicklung – allerdings sollten die Anforderungen des Kunden das erste Augenmerk sein, noch bevor Zeit und Aufwand mit Detailarbeit verwendet wird.
Wenn Ingenieure die Enterprise-Welt durch die Augen von zahlenden Kunden oder Projektmanagern mit endlichem Budget sehen, können sie das auch oft nachvollziehen.
Java Enterprise - Geschäftslogik first!
Deshalb konzentrieren wir uns darauf, die Domänen- und Geschäftslogik zu implementieren. Erst danach sind weitere technisch-motivierte Notwendigkeiten dran. Java Enterprise unterstützt diesen Ansatz. Die Domäne – der Kern der Anwendung – ist zunächst in reinem Java implementiert. Da wir uns im Enterprise-Umfeld bewegen, sind jedoch auch querschneidende Funktionalitäten wie Kommunikation und Persistenz von Nöten. Mit modernem Java EE ist es möglich, diese deklarativ, mit möglichst wenig Overhead, zu realisieren. Vor allem das "Konvention vor Konfiguration"-Prinzip der Java EE Platform unterstützt Entwickler darin.
Die Java EE-Spezifika werden mit wenigen Annotationen zu unserer Geschäftslogik, den reinen Java-Klassen, hinzugefügt. Die Business-Logik können wir daher mit weniger Aufwand per Unit-Tests verifizieren. Da in modernem Java EE die meiste Funktionalität in Annotationen deklariert wird, können die Klassen in Tests direkt instanziert werden – auch ohne Enterprise-Container.
Java EE und Domain-Driven Design (DDD)
Wir implementieren den Einstiegspunkt eines Anwendungsfalles, eine facade, als annotierte Java-Klasse. In der Sprache von Domain-Driven Design repräsentiert diese Klasse einen service. Das folgende Beispiel zeigt einen service, implementiert als EJB:
@Stateless public class CarManufacturer { @Inject CarFactory carFactory; @PersistenceContext EntityManager entityManager; public Car manufactureCar(Specification spec) { Car car = carFactory.createCar(spec); entityManager.persist(car); return car; } }
Die Klasse CarFactory erzeugt neue Car-Objekte anhand gegebener Spezifikationen.
Der "EntityManager" verwaltet die Persistenz dieser Objekte in der Datenbank.
Er repräsentiert damit das Domain-Driven Design-Konzept von repositories. Der "EntityManager"-Typ bietet eine ziemlich klare API. Eine zusätzliche Fassade, alà DAO's (Data Access Object) wie in der J2EE-Welt, wird nicht mehr benötigt.
Das Vorgehen, Geschäftsprozesse in Software abzubilden, sollte sein, diese zuerst direkt in der Fassade, wie hier CarManufacturer zu implementieren. Wenn die Methoden und Prozesse zu komplex werden, wandert Funktionalität in delegates, wie hier die CarFactory. Unit-Tests verifizieren das korrekte Verhalten, indem sie die facade instanzieren und delegates entsprechend mit mocks simulieren.
Klassen der Geschäftsdomäne, zum Beispiel Car, sind ebenfalls POJOs ("plain old Java objects"), die mit Annotationen versehen werden können, um technisch motivierte Funktionalitäten wie Persistenz abzubilden:
@Entity @Table(name = "cars") public class Car { @Id @GeneratedValue private long id; @OneToOne(optional = false, cascade = CascadeType.ALL) private Engine engine; @OneToMany(cascade = CascadeType.ALL) private Set<Seat> seats = new HashSet<>(); // getters & setters }
Domain-Driven Design definiert entities als die identifizierbaren Entitäten unserer Domäne; die Kernobjekte unserer Anwendung. JPA ermöglicht es uns, das Persistenz-Mapping der Entitäten mit wenig Extra-Aufwand – durch Annotationen – zu spezifizieren. Die Bezeichnung der Annotation passt ebenfalls in die DDD-Welt: @Entity. JPA erfordert, dass ein @Entity per @Id identifizierbar ist.
JPA unterstützt ebenfalls das Konzept von aggregates. In unserem Beispiel besteht ein Auto unter anderem aus mehreren Sitzen. Die individuellen Sitze sind in unserer Domäne identifizierbar. Persistenz-Operationen werden vom Root-Object Car zu allen zugehörigen Seat-Instanzen kaskadiert. Daher können wir ein komplettes Auto inklusive aller Komponenten durch seinen Identifikator aus der Datenbank abrufen.
DDD-factories, die die Erzeugung komplexer Objekte in Methoden oder Klassen kapseln, implementieren wir in der Java EE-Welt entweder in reinem Java, in Methoden die Teil der Domänenentitäten sind, oder mit CDI. Vor allem CDI producer methods sind ein sehr schlankes und hilfreiches Mittel. Das folgende Beispiel zeigt zunächst eine factory als Teil der Auto-Entität:
public class Car { ... public LogBook createDriverLog() { // create logbook statement } }
Factories in die Entitäten zu packen, macht vor allem dann Sinn, wenn die Erzeugung bestimmter Objekte viele Eigenschaften der entsprechenden Entitäten benötigt. Das folgende Beispiel zeigt die Erzeugung einer abgeleiteten CarFactory durch CDI:
public class CarFactoryProducer { @Produces public CarFactory exposeCarFactory() { CarFactory factory = new BMWCarFactory(); // use custom logic return factory; } }
Die Instanz der BMWCarFactory kann wie im ersten Beispiel durch den Typ CarFactory injiziert werden.
Die Beispiele demonstrieren, wie modernes Java EE einen leichtgewichtigen Entwicklungsansatz, ohne viel technischen Aufwand, ermöglicht. Der größte Augenmerk liegt auf der Geschäftslogik, die mit wenig Extraaufwand mit den technischen Notwendigkeiten einer Enterprise-Anwendung erweitert wird.
Java EE Kommunikation: JAX-RS, JMS, WebSocket und JSON
Server-Endpunkte mit JAX-RS, JMS oder WebSocket sind weitere Beispiele, wie schlanke, deklarative APIs den Entwicklern das Leben erleichtern. Java EEs "Konvention vor Konfiguration" ist für die meisten Anwendungsfälle ausreichend. Für weitere kann das Verhalten programmatisch erweitert werden.
JSON Binding per JSON-B ist eine Neuerung, die Java EE 8 mit sich bringt. Analog zu JAXB für XML können mit JSON-B Java-Klassen deklarativ zu JSON und zurück abgebildet werden. In Java EE 8 integriert sich JSON-B nahtlos in JAX-RS; die JAX-RS Runtime deligiert das JSON-Mapping von Referenztypen an JSON-B. Die Klassen in JAX-RS-Ressourcen verwendeter Typen können daher JSON-B-Annotationen definieren. Diese Annotationen werden ohne weiteren Konfigurationsaufwand in HTTP-Requests und -Responses beachtet. Folgendes Beispiel zeigt eine User-Klasse, die per JSON-B serialisiert wird:
public class User { @JsonbTransient private long id; @JsonbProperty("username") private String name; ... }
Eine JAX-RS Ressource, die einen HTTP-Endpunkt zu einem Anwendungsfall repräsentiert, spezifiziert den "User"-Typ als Request- oder Response-Body:
@Path("users") @Produces(MediaType.APPLICATION_JSON) public class UsersResource { @Inject UserStore userStore; @GET public List<User> getUsers() { return userStore.getUsers(); } }
Die JSON-B-Serialisierung greift automatisch: die über HTTP ausgelieferte Liste von Benutzern enthält in den JSON-Objekten eine Property username und keine id.
Besonders die deklarativen Ansätze und das Zusammenspiel der Java EE APIs ermöglichen eine Enterprise-Entwicklung, die sich auf das Wesentliche konzentriert.
Java EE und Zero-dependency-Applikationen: Docker und Kubernetes
Die Tatsache, dass sich Softwareingenieure auf eine produktive Entwicklung konzentrieren sollen, spiegelt sich auch in dem Build- und Deployment-Modell wider. Java EE-Applikationen trennen traditionell die Anwendung von der Implementierung. Die Klassen importieren ausschließlich die APIs – der Enterprise-Container liefert die Implementierung. Dieses Vorgehen ermöglicht per Definition die kürzesten Build-, Auslieferungs- und Deployment-Zeiten. Bei jeder Ausführung der Continuous Delivery-Pipeline werden lediglich die Inhalte der Applikation kompiliert, zusammengebaut und ausgeliefert – keine Framework-Implementierung. Letztere ist schon Bestandteil der Runtime.
Das ist der Grund, weswegen Java EE gerade in der Welt von Containern und Orchestrierung alà Docker und Kubernetes immense Vorteile mit sich bringt. Durch das Copy-On-Write-Dateisystem [1] und Cachen der einzelnen Container-Image-Schichten führt die Infrastruktur nur die allernötigsten Schritte aus. Nur die kompilierten Klassen unserer Anwendung werden in jeder Iteration ausgeliefert, nicht die Technologie. Sogenannte zero-dependency-Applikationen, als WAR-Archiv verpackt, ermöglichen diesen Ansatz. Dabei enthält die Anwendung ausschließlich Abhängigkeiten mit Scope provided, welche nicht im Deployment-Artefakt landen.
Das folgende Beispiel zeigt ein Docker-File, dass das Basis-Image von GlassFish 5 und eine Java EE-Applikation enthält. Potentielle Abhängigkeiten der Applikation werden nicht im Deployment-Artefakt ausgeliefert, sondern in vorherigen Schritten im Docker-File.
FROM oracle/glassfish:5.0 # add potential provided dependencies COPY ...jar $GLASSFISH_HOME/glassfish/domains/domain1/lib/ext/ COPY target/cars.war $GLASSFISH_HOME/glassfish/domains/domain1/autodeploy
Das resultierende Docker-Image führt beim Starten automatisch unsere Applikation aus.
Zero-dependency-Applikationen und diese Konzepte im Docker-File setzen das Copy-On-Write-Konzept wirksam um und beschleunigen unsere Continuous Delivery-Pipeline erheblich. Wenn sich die Runtime sowie die Abhängigkeiten zwischen mehreren Builds nicht ändern, wie meistens der Fall, werden ausschließlich die Klassen unserer Applikation – im cars.war-Archiv – gepackt und übertragen.
Traditionell hatten Enterprise-Projekte oft Schwierigkeiten, Versionen und Konfiguration von Applikationsservern mitzubestimmen. Server enthielten mehrere Anwendungen, was Deployments, Konfiguration und Neuinstallation erschwert und einiges an Abstimmung erfordert. Container a là Docker lösen das Problem. Da die Container – inklusive Runtime und Konfiguration – alles enthalten, was eine Anwendung zum Laufen benötigt, ist die Wahl der Technologie unabhängig von anderen Applikationen. Versionsupgrades, zum Beispiel von Java EE 7 zu 8, bringen weniger Aufwand und Risiko mit sich. Dennoch müssen die Deployment-Artefakte nicht den gesamten Stack und Megabytes an Abhängigkeiten ausliefern – das ist Teil des entsprechenden Basis-Images [2].
Java EE 8 und weiter
In Java EE 8 sind einiges neue Features zu finden. Vor allem der neue JSON-B-Standard vereinfacht die Enterprise-Entwicklung erheblich. In der Vergangenheit sorgten JSON Mapping-Frameworks wie Jackson für Versions-Konflikte zwischen third-party-Abhängigkeiten, deren transitiven Abhängigkeiten und Applikations-Server-Bibliotheken. Die Konflikte zu lösen benötigte einiges an Zeit und Aufwand. Mit Java EE 8 können Applikationen auf diesen Mehraufwand verzichten.
Neben JSON-B ist die Security API als neuer Standard eingeführt worden und es wurden einige bestehende APIs aktualisiert: CDI, JSON-P, Bean Validation, JPA, Servlet und JSF. Von den Neuerungen begrüße ich vor allem Server-Sent Events und reaktive Clients in JAX-RS, die neuen Features von JSON-P 1.1. und Bean Validation 2.0 und viele Verbesserungen in der Integration verschiedener Standards. Die Richtung, in der sich die Java EE API in den letzten Monaten und Jahren bewegt hat, ist sehr zum Vorteil von Entwicklern und mit der Übergabe der Java EE Platform an die Eclipse Foundation als Eclipse Enterprise for Java (EE4J) wird sich die zukünftige Weiterentwicklung offener und vermutlich schneller gestalten.
Fazit
Modernes Java EE ermöglicht Softwareingenieuren, sich auf die relevanten Geschäftsanwendungsfälle zu konzentrieren und den technischen Overhead minimal zu halten. Die Ansätze schlanker Deployment-Artefakte integrieren sich optimal in moderne, containerisierte Umgebungen wie Docker, Kubernetes oder OpenShift.
Am Ende des Tages sind es die Anwendungsfälle, die den Startups und großen Unternehmen den Umsatz generiert. Das ist es, was modernes Java EE für Firmen so interessant macht: die leichtgewichtige, business-orientierte Entwicklung – kurzum Produktivität.
- Informatik Aktuell: Ralph Tandetzky: cow_ptr<T> – Der Smartpointer für Copy-On-Write
- Sebastian Daschner, 2017: Architecting Modern Java EE Applications: Designing lightweight, business-oriented enterprise applications in the age of cloud, containers, and Java EE 8; Packt Publishing; Auflage: 1