New and noteworthy in Java EE 8

Am 18.09.2017 wurde nach langer Zeit des Wartens und auch der Unsicherheit über die Zukunft der Plattform die Version 8 der Java Enterprise Edition veröffentlicht. Diese im JSR 366 beschriebene Umbrella Specification umfasst diverse Änderungen an den in ihr enthaltenen Einzelspezifikationen, von den im Folgenden einige – die subjektiv wichtigsten – beschrieben werden:

Java API for JSON Binding 1.0 (JSON-B, JSR 367)

Durch diese neue Spezifikation enthält die Plattform erstmals die Möglichkeit der Serialisierung und Deserialisierung von Java-Objekten in bzw. aus JSON analog zu JAXB. Im Sinne von Zero Configuration gibt es Defaults für die Konvertierung von Attributen, natürlich inklusive der mit Java 8 eingeführten Datums- und Zeit-Typen. Von den Defaults kann abgewichen werden, indem Klassen und Attribute mit Annotationen versehen werden (z. B. @JsonbTransient zum Ausschluss von Attributen) oder indem eine global angepasste Konfiguration genutzt wird.

Java Servlet 4.0 (JSR 369)

Der Fokus der neuen Servlet-Spezifikation liegt auf der Unterstützung von HTTP/2. Der Datenaustausch zwischen Client (Browser) und Server kann dann mittels Header Compression, Request Multiplexing und Server Push effizienter gestaltet werden. Die API-Änderungen sind nur marginal, so dass Anwendungen in den Genuß der Optimierung einfach dadurch kommen, dass sie diese Version der Spezifikation nutzen. Die überwiegende Anzahl der Browser sind bereits HTTP/2-fähig.

JavaServer Faces 2.3 (JSF, JCP 372)

Das Paket javax.faces.bean ist nun endlich deprecated. Statt der JSF-eigenen Manage Beans setzt man nun vollständig auf CDI, was sich auch in der Injizierbarkeit von CDI Beans in JSF-Artefakte wie bspw. Konverter ausdrückt. Weitere Änderungen sind die Unterstützung von Java 8 Date/Time, Websocket Integration und die Möglichkeit feldübergreifender Validierung.

Contexts and Dependency Injection for Java 2.0 (CDI, JSR 365)

Ein CDI Container lässt sich nun auch im SE-Kontext betreiben. Dies drückt sich einerseits dadurch aus, dass die Spezifikation nun getrennte Abschnitte für SE und EE vorsieht. Andererseits ist mit Hilfe der Klasse SeContainer ein implementierungsneutraler Start und Stopp eines Containers möglich. Änderungen gibt es weiterhin bei der Verarbeitung von Events. Observer können nun eine Verarbeitungsreihenfolge erhalten oder asynchron bedient werden.

Bean Validation 2.0 (JSR 380)

Die Validierugsmöglichkeiten für Java Beans wurden um diverse neue vordefinierte Contraints ergänzt (bspw. @Email oder @Positive) und können nun auch auf Elemente von Collections angewendet werden. Schließlich werden nun auch Java-8-Klassen wie LocalDate oder Optional unterstützt.

Java API for RESTful Web Services 2.1 (JAX-RS, JSR 375)

Asynchronität ist in REST-Services schon seit der Vorversion ein Thema, das nun u. a. durch die Nutzung von CompletionStage weiter ausgebaut und resilienzfähig wird. Neu sind zudem der Support für PATCH Requests sowie Server Side Events.

Neben dieser willkürlichen Auswahl haben auch weitere Spezifikationen Änderungen erfahren.

Zu den genannten Punkten werden wir in lockerer Folge weitere Posts in diesem Blog machen. In diesem Sinne: Stay tuned!

Advertisements

Oracle schlägt Verlagerung von Java EE in die Eclipse Foundation vor

Seit nunmehr fast 20 Jahren ist die Java EE eine verlässliche Bank für den Aufbau und Betrieb von Enterprise-Anwendungen. Oracle – zuvor Sun – hat am Erfolg der Plattform zweifellos einen wesentlichen Anteil, zusammen mit den vielen Firmen, Einzelpersonen und der Community, die sich für Standards und deren Weiterentwicklung engagiert haben.

Nun hat sich die Welt aber weiter gedreht – manchmal scheint es, mit stetig wachsender Geschwindigkeit – und neue Anforderungen (Cloud, Microservices, … – you name it) verlangen nach einer Anpassung und Weiterentwicklung der Plattform in einer Taktung, die nicht mehr so recht zu einem in gewisser Weise zentralistischen Modell passen will.

Vor diesem Hintergrund erscheint die Ankündigung von Oracle, Java EE in die Eclipse Foundation zu verschieben, sehr sinnvoll. Sie ist vielleicht sogar überfällig, wenn man die Unsicherheit und Querelen der vergangenen Monate und Jahre betrachet.

Ich bin sehr zuversichtlich, dass dieser Schritt der richtige ist, um Java EE weiter verlässlich, stabil und zukunftsorientiert auszurichten.

Rightsize your Services mit WildFly Swarm

WildFly Swarm

Überblick

Klassische Application Server wie JBoss/WildFly oder Glassfish/Payara sind etablierte und ausgereifte Ablaufumgebungen für Enterprise-Applikationen. Dass mehrere Anwendungen bis zu einem gewissen Grade voneinander isoliert in einem Server berieben werden können, wird in der Praxis wegen der unvermeidlichen Gemeinsamnutzung von Java VM, CPU, Memory etc. selten ausgenutzt. Man trifft also meist Server an, auf denen genau eine Anwendung deployt ist. Durch den Trend zu Microservices kommt es zu einer deutlichen Vergrößerung der Serveranzahl mit den daraus resultierenden Anforderungen im Bereich Installation, Konfiguration und Betrieb. Populäre Alternativen zu Java EE – z. B. Spring und Dropwizard – nutzen ein anderes Betriebsmodell: Statt Anwendungen in jeweils einen Server zu deployen, wird der Server in die Anwendungen integriert. Für die Java-EE-Welt bietet das Open-Source-Projekt WildFly Swarm (wildfly-swarm.io>) einen solchen Ansatz.

Komposition der Runtime

WildFly Swarm ist vergleichbar mit einem in Stücke zerteilten WildFly Application Server. Diese Stücke – sog. Fractions – werden als Maven Dependencies zur Anwendung hinzugefügt:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.wildfly.swarm</groupId>
      <artifactId>bom-all</artifactId>
      <version>${version.wildfly.swarm}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.wildfly.swarm</groupId>
    <artifactId>jaxrs-cdi</artifactId>
  </dependency>
  <dependency>
    <groupId>org.wildfly.swarm</groupId>
    <artifactId>jpa</artifactId>
  </dependency>
  ...

Die derzeit (Dez. 2016) aktuelle Version ist 2016.12.0.

Aus den so gewählten Fractions und dem eigentlichen Anwendungscode wird dann ein Uber Jar gebaut. Dies geschieht mit Hilfe eines Plugins in der Maven-Konfiguration des Projektes:

<build>
  <plugins>
    <plugin>
      <groupId>org.wildfly.swarm</groupId>
      <artifactId>wildfly-swarm-plugin</artifactId>
      <version>${version.wildfly.swarm}</version>
      <configuration>
        <mainClass>de.gedoplan.micro.bootstrap.SwarmBootstrap</mainClass>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>package</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Die o. a. Main Class dient der programmatischen Konfiguration der Anwendung (s. u.). In einfachen Fällen wird sie nicht benötigt. Das entsprechende Element der Plugin-Konfiguration entfällt dann.

Durch den Build-Lauf (z. B. per mvn package) entsteht dann im target-Verzeichnis neben dem normalen Zielartefakt xyz.war eine Datei namens xyz-swarm.jar, die die Anwendung inkl. Server Runtime enthält.

Konfiguration der Anwendung

Wenn die Default-Konfiguration nicht ausreicht, weil bspw. DB-Verbindungen eingerichtet werden müssen, kann eine individuell prgrammierte Main-Klasse genutzt werden. Darin können die einzelnen Fractions mit Hilfe eines Fluent API parametrisiert werden. Alternativ kann eine externe Konfigurationsdatei referenziert werden:

public class SwarmBootstrap {

  public static void main(String[] args) throws Exception {
    Swarm swarm = new Swarm();

    System.setProperty("swarm.context.path", "micro-ohne-server");

    URL standaloneFullXml = SwarmBootstrap.class.getClassLoader().getResource("configuration/standalone-full.xml");

    swarm
        .withXmlConfig(standaloneFullXml)
        .start()
        .deploy();
  }

Die im Beispiel genutzte externe Konfigurationsdatei entspricht der vom WildFly Application Server bekannten standalone-full.xml, muss aber nur die für die zu konfigurierenden Fractions benötigten Einstelldaten enthalten.

Anwendungsstart

Die Anwendung wird durch Ausführung des durch das o. a. Plugin erzeugte Uber Jar gestartet: java -jar target/xyz-swarm.jar.

Weitere Infos

Microservices und das Uberjar

Ich habe gerade auf der W-JAX Thilo Frotschers Vortrag über Java EE Microservices verfolgt. Er stellte darin dar, dass bspw. mit Payara Micro sog. Uberjars gebaut werden können, die neben der Microservice-Anwendung auch die notwendige Server-Implementierzung enthalten. Motivation und Ziel: Start der Anwendung inkl. Umgebung als ausführbares Jar auf der Kommandozeile.

Ein Teilnehmer stellte dann die Frage „Muss ich denn für jede Instanz meines Microservices immer die komplette Runtime haben – mit dem entsprechnden Platzbedarf, Startzeiten etc.? Oder könnte ich auch eine Shared Runtime nutzen?“.

Das zeigt – wie ich meine – die derzeitige etwas unglückliche Vermischung der Diskussion über das Programmmodell für Anwendungen und ihr Betriebsmodell. Das was der Fragende nutzen könnte, wäre ein klassischer Application Server!

Wichtig ist aus meiner Sicht, dass die innere Struktur der Anwendungen nicht durch das Betriebskonzept eingeschränkt werden sollte. Das ist mit Java EE der Fall: Ich kann klassische War Files bauen und auf einem klassischen Application Server deployen – gerne auch in Umgebungen wie Docker, wo ebendieses Deployment auch nichts anderes ist als eine Datei an einen bestimmten Platz zu legen. Ich kann aber auch die gleiche Anwendung mit einer Server Runtime zu einem Uberjar kombinieren und dann als eine Kommandozeilen-Anwendung betreiben.

Zu dem Thema gibt es am 08.12.16 in unserem Hause einen kostenlosen Vortrag: http://gedoplan-it-consulting.de/expertenkreis-java/aktuelles/.

Docker – „Build, Ship, and Run“

Problemstellung

Ein großes Problem stellen beim Testen und Ausliefern von Software die leider häufig unterschiedlich konfigurierten Ablaufumgebungen dar. Die Entwicklerrechner, der Testserver und der Produktivserver werden sich höchst wahrscheinlich deutlich unterscheiden, was die installierte Software und weitere Konfiguration anbelangt. Dies führt immer wieder zu Problemen, welche nur auf einer dieser Umgebung auftreten und sich nicht einfach nachvollziehen lassen. Vor allem aber ist natürlich gefährlich, dass sich so ein einwandfreies Funktionieren in der Produktivumgebung nicht durch ein erfolgreiches absolvieren der Tests in einer Integrationsumgebung garantieren lässt.

Um dieses Problem zu lösen könnte man nun mit virtuellen Maschinen als Umgebung arbeiten, was allerdings den Nachteil mit sich bringt, dass diese relativ schwergewichtig sind. Diese Schwergewichtigkeit ergibt sich zum einen daraus, dass die Images ein komplettes Betriebssystem enthalten und daher entsprechend groß sind, zum anderen aber auch, das auf dem Host-Rechner entsprechend viel Ressourcen für das Bereitstellen der virtuellen Ablaufumgebungen benötigt wird. Container wie sie von Docker angeboten werden stellen hier eine leichtgewichtigere Alternative dar.

Docker

Docker ist eine offene Container-Plattform, welche es ermöglich Anwendungen isoliert in sogenannten Containern ausführen zu können. Die Applikationen werden dafür mit all ihren Abhängigkeiten und Konfigurationen in einem Image definiert, welches dann zwischen den verschiedenen Umgebungen ausgetauscht werden kann.

Umgesetzt ist Docker selbst, also die Docker-Engine, als eine Client-Server-Anwendung. Zum einen gibt es die serverseitige Laufzeitumgebung, die als Daemon-Prozess läuft und die eigentliche Arbeit verrichtet. Von dieser Komponente wird per REST eine API bereitgestellt, welche von dem clientseitigen CLI-Werkzeug angesprochen wird. Der Client kann entweder direkt auf dem Server laufen, oder sich remote verbinden.

Container teilen sich das Betriebssystem des Hostrechners und stellen nicht selber ein vollwertiges Betriebssystem bereit. Es werden lediglich die Prozesse isoliert ausgeführt. Dabei bringt jedoch jeder Container sein eigenen Dateisystem mit, was sich die Container also mit dem Host teilen ist somit hauptsächlich nur der Kernel. Unter der Haube verwendet Docker Linux-Technologien wie Namespaces und CGroups um die Prozesse in den Containern Isoliert ausführen zu können.

Installation

Derzeit läuft Docker nativ nur auf Linux, es gibt aber auch Installer für Windows und Mac-Os, welche dann allerdings auf eine Virtualisierung angewiesen sind. Auf Linux muss lediglich die docker-engine installiert werden, wo in der Regel schon entsprechende Pakete für die jeweiligen Distributionen bereitstehen.

Beispiel für Installation auf Debian:

apt-get update
apt-get install -y apt-transport-https ca-certificates
apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
cat <<EOF >> /etc/apt/sources.list.d/docker.list
deb https://apt.dockerproject.org/repo debian-jessie main
EOF

apt-get update
apt-get install -y docker-engine
service docker start

Images

Anwendungen werden definiert in Form von sogenannten Images. Ein Image beschreibt ein Dateisystem mit allen für die Anwendung relevanten Dateien und Einstellungen. Das Dateisystem setzt sich immer aus mehreren Schichten zusammen. Jede Änderung an einem Image wird in einer eigenen Schicht angelegt. Durch die Aufteilung in Layer ist es nun möglich, dass bei Änderungen an einem Image auch nur diese Änderungen ausgetauscht werden müssen.

Es wird in jedem Fall ein Basisimage benötigt, welches einer Linux Distribution entspricht wie z.B. Debian. Diesem Basisimage können nun eigene Dateien, Umgebungsvariable usw. in einer neuen Schicht hinzugefügt werden, wodurch dann ein neues Image entsteht. Images erhalten werden mit einem Namen getaggt über welchen sie dann verwendet werden können.

Auflisten von Images auf dem Docker-Host:

docker images

Entfernen von Images auf dem Docker-Host:

docker rmi image_name

Registries

Abgelegt und Verteilt werden Images von sogenannten Registries. Es gibt die öffentliche Standard-Registry DockerHub im Internet, in welcher eine Vielzahl verwendbarer Basisimages zu finden ist. Es besteht auch die Möglichkeit dort eigene Images hochzuladen. Falls Images nur unternehmensintern verfügbar sein sollen, besteht auch die Möglichkeit eine eigene Registry zu betreiben. Die Images werden über ihren Tagnamen von der Registry angefordert. Der Tagname kann die Adresse der Registry als Prefix enthalten, was dazu führt, dass Docker versucht das Image von der entsprechenden Adresse anzufordern. Wenn kein Prefix gegeben und das Image nicht lokal vorhanden ist, wird automatisch auf Dockerhub geschaut.

Docker Images können mit dem Pull-Befehl geladen und mit dem Push-Befehl abgelegt werden. Dies erinnert an Versionsverwaltungssysteme wie Git. Wenn ein Image benötigt wird um z.B. einen Container zu starten, so wird, falls das Image noch nicht geladen wurde, automatisch ein Pull durchgeführt.

Commands für Pull und Push:

Docker pull repo/image/version

Docker push repo/image/version

Containers

Container sind in Docker die lauffähigen Komponenten, die gestartet, gestoppt und auch wieder gelöscht werden können. Erzeugt werden Container immer von einem Image. Man kann sich daher Images auch als eine Art Templates für Container vorstellen. Da die Images nicht direkt beschreibbar sind (was auch gut ist!) wird für jeden Container nochmal ein drüber liegender Layer angelegt, auf welchen geschrieben werden kann. Dadurch können sich mehrere Container ein Image teilen, ohne sich dabei gegenseitig zu beeinflussen.

Ein Container wird mit dem Run-Befehl erzeugt und gestartet. Dieser benötigt als Information ein Image, welches im Bedarf noch gepulled wird. Weitere Startparameter können Netzwerkkonfiguration, Interaktiv- oder Hintergrund-Modus, Umgebungsvariablen, Volumes, Containername und auszuführender Prozess sein.

Beispiel startet das Image localhost:5000/mysql und gibt dem Container den Namen mysql_server:

docker run -d --name=mysql_server -t localhost:5000/mysql

Ein Docker Container stoppt, sobald der Prozess, welcher als Einstiegspunkt definiert ist, beendet wird. Ein Container kann manuell gestoppt werden über den Stop-Befehl

docker stop mysql_server

Falls nicht mehr benötigt kann der Container wieder entfernt werden:

docker rm mysql_server

Es besteht auch die Möglichkeit ein neues Image aus einem Container zu bauen. Dies enthält dann alle Informationen des Container-Images und zusätzlich alle Änderungen die im Container-Layer stattgefunden haben. Allerdings sollten Dockerfiles für das Erstellen von Images bevorzugt werden, da so die Änderungen besser dokumentiert sind.

Dockerfiles

Dockerfiles beschreiben die Änderungen die an einem Image durchgeführt werden sollen, um ein neues zu erstellen. Dabei gibt es nur eine Handvoll Anweisungen die in einem Dockerfile genutzt werden können:

· From: Gibt das Basisimage an, welches erweitert werden soll.

· ADD/COPY: Fügt eine Datei aus dem Dateisystem des Hosts oder von einer Netzwerkadresse hinzu

· ENV: Definiert eine Umgebungsvariable

· RUN: Führt Befehle aus, wobei jedes RUN zu einem neuen Layer führt, daher werden häufig viele Befehle gebündelt in einem RUN ausgeführt

· VOLUME: Definiert einen Pfad der für das Einbinden von Volumes genutzt werden kann

· EXPOSE: Gibt Ports an welche der Container nach außen zur Verfügung stellen soll

· CMD/ENTRYPOINT: Definiert die Aktion, die standardmäßig beim Starten eines Containers ausgeführt wird

Beispiel für ein Mysql Image:

FROM localhost:5000/base

ADD my.cnf /etc/mysql/my.cnf
ADD startmysql.sh /var/startmysql.sh

RUN apk add --update mysql && \
    mkdir -p /etc/mysql/conf.d && \
    mkdir /run/mysqld && \
    rm -rf /var/cache/apk/* && \
    chmod 644 /etc/mysql/my.cnf

VOLUME ["/var/data/mysql"]

EXPOSE 3306

CMD ["/var/startmysql.sh"]

Mit dem Build-Befehl kann mit Hilfe eines Dockerfiles ein neues Image gebaut werden. Dabei kann auch der Tagname, den das Image bekommen soll, definiert werden:

docker build –t mytagname /pathtodockerfile/

Volumes

Es ist nicht angebracht, Daten, wie z.B. die einer Datenbank, direkt in den Docker-Containern abzuspeichern. Diese Informationen würden nämlich verloren gehen, sobald der Container zerstört wird und neu aus dem Image heraus erzeugt wird (z.B. nach einem Update des Images).

Damit Daten erhalten bleiben, werden sie in sogenannten Volumes abgelegt. Diese Volumes werden dann in dem Container unter einem bestimmten Pfad eingehängt (z.B. Datenverzeichnis einer Datenbankinstallation). Das Zuweisen eines Volumes kann beim Erzeugen eines Container mit dem Run-Befehl erfolgen:

docker run -d --name=mysql_server -v /data/mysql:/var/data/mysql -t localhost:5000/mysql

Es gibt verschiedene Arten von Volumes. Bei einem Container der Volumes definiert, ohne dass im Run-Befehl explizit eines zugewiesen wird, bekommen einen temporären Speicher, der nicht nach Zerstören des Containers erhalten bleibt. Sollen die Daten persistent bleiben, so muss ein anderer Volume-Driver verwendet werden. Dies kann z.B. ein Verzeichnis auf dem Host sein, ein Netzwerk- oder Cloudspeicher.

Networking

Docker Container müssen in der Lage sein miteinander zu kommunizieren, außerdem müssen sie Verbindungen nach außen aufbauen und in einigen Fällen auch anbieten können. Es gibt daher in Docker eine Reihe von Möglichkeiten Netzwerke für die Container zu konfigurieren. Per Default gibt es in Docker ein Bridge und ein None Netzwerk, welche direkt bei der Installation der Docker Engine angelegt werden. Das Default-Bridge-Netzwerk wird standardmäßig verwendet. Es erlaubt es Container miteinander per Link zu Verknüpfen und Ports auf den Hostrechner zu Mappen, sodass die entsprechenden Services direkt über die Adresse des Hostrechners verfügbar sind.

Beispiel für Portmapping auf host:

docker run -d -p=3306:3306 --name=mysql_server -v /data/mysql:/var/data/mysql -t localhost:5000/mysql

Dies reicht für das Testen in der Regel erstmal aus. Es besteht aber auch die Möglichkeit ein eigenes Netzwerk zu definieren. Dies kann entweder ein Bridge-Netzwerk sein, welches genutzt werden kann, wenn alle Container auf demselben Hostrechner laufen, oder ein Overlay-Netzwerk, wenn die Container auf mehrere Rechner verteilt sind.

Netzwerke können mit Hilfe des docker network-Befehls verwaltet werden.

Eigene Registries

Es besteht die Möglichkeit eine eigene Registry für Images zu betreiben. Dafür muss eine entsprechende Registry-Software, welche Docker-Images in der standardisierten Form verwalten kann, installiert werden. Einfachste Möglichkeit das auszuprobieren ist es, eine Registry über das offizielle Image zu starten:

docker run -d -p 5000:5000 --name registry registry:2

Nun können Images in diese Registry hinzugefügt oder von da bezogen werden:

docker push localhost:5000/mysql
docker pull localhost:5000/mysql

Eine Registry kann auch als Cache für den zentralen Dockerhub definiert werden. In diesem Fall muss Docker auch noch entsprechend konfiguriert werden sodass nicht standardmäßig direkt bei Dockerhub angefragt wird, sondern bei der eigenen Registry.

Best-Practices

Images sollten immer mit Hilfe von Dockerfiles beschrieben werden, da somit immer alles Änderungen dokumentiert sind. Diese Dockerfiles können dann in einer Versionsverwaltung, z.B. ein Git-Repository, abgelegt werden.

Das wählen des richtigen Basisimages ist wichtig. Dabei sollte man darauf achten, dass alle benötigten Funktionen enthalten sind, aber auch, dass die Image-Größe nicht unnötig groß wird. Sollen die Images möglichst klein bleiben, so kann z.B. ein Alpine-Linux als Basis verwendet werden. Von Images kann immer weiter „abgeleitet“ werden, sodass man viel wiederverwenden kann, z.B. kann man ein eigenes Java-Basisimage erstellen, von dem alle Anwendungs-Images, die Java benötigen, abgeleitet sind.

Eine Anwendung sollte in einem eigenen Container Laufen. Bei zusammengesetzten Anwendungen, z.B. Webanwendung + Datenbank, bedeutet das, dass jeder Bestandteil in einem eigenem Container laufen sollte. Es soll so aufgebaut sein, dass die gleichen Images in allen Stages (Entwicklung, Test, Produktion) ohne Änderungen verwendet werden können. Alle zur Laufzeit der Anwendung entstehenden Daten und Konfiguration, die dauerhaft sein sollen, werden in Persistent-Volumes abgelegt.

Docker kann sowohl für das definieren von Infrastruktur, als auch für eigene Anwendungen verwendet werden. Es macht in beiden Fällen Sinn, da so die komplette Anwendungslandschaft gut Dokumentiert und durch die Images immutable definiert worden ist.

Docker-Compose

Bei Anwendungen die aus mehreren Teilen bestehen, z.B. Webanwendung und Datenbank, bietet es sich an mit dem zusätzlichen Werkzeug Docker Compose zu arbeiten. Damit ist es möglich die Container-Konfiguration, die benötigten Images, das Networking zwischen den Container usw. deklarativ in einer gemeinsamen Datei zu beschreiben. Diese kann dann z.B. mit dem Projekt in die Versionsverwaltung und wird dann von Docker-Compose genutzt um die Container mit einem einfachen Befehl zu erzeugen und zu starten.

Als Format für die Konfigurationsdatei wird YAML verwendet. Beispiel für ein Compose-File:

version: '2'
services:
  mysqldb:
    image: localhost:5000/mysql
    volumes:
      - "/data/mysql:/var/data/mysql"
    restart: always
    expose:
      - &amp;quot;3306&amp;quot;

  demoapp:
    depends_on:
      - mysqldb
    build: .
    image: localhost:5000/demo
    links:
      - mysqldb
    ports:
      - "8080:8080"
    restart: always
    environment:
      DB_CONNECTION_URL: mysqldb:3306
      DB_SCHEMA: demo
      DB_USER: demouser
      DB_PASSWORD: demopassword

Gestartet werden kann diese Anwendung jetzt einfach über den Up Befehl. Dieser kann als Parameter auch ein neues Bauen der Images anstoßen (Sofern im File ein Build-Pfad zu einem Dockerfile angegeben wurde). Es werden nur die Container die noch nicht Laufen, oder deren Image sich geändert hat gestartet.

docker-compose up -d --build

Beispiel Einsatz von Docker in Java-EE Projekt

Gehen wir von einer einfachen Anwendung aus, die auf einem Wildfly läuft und an eine MySql Datenbank angebunden ist. Die Anwendung und die Datenbank sollen in jeweils einem eigenen Container laufen. Es wird also einmal ein Image für die Datenbank definiert, welches als Einstiegspunkt die Datenbankanwendung startet. Als zweites benötigen wir ein Image mit dem Wildfly, welches als Einstiegspunkt den Applicationserver hochfährt. Der Wildfly in dem Image ist hier bereits so konfiguriert, dass er an die Datenbank, die in einem anderen Container läuft, angebunden ist. Dieses Wildfly-Image wird nun als Basisimage für das Image unserer eigentlichen Anwendung verwendet. Bei dem Projekt Handelt es sich um ein einfaches Webprojekt als WAR-File. Das Docker-Image für unsere Anwendung wird nun durch ein Dockerfile beschrieben, was nichts weiter tut, dem Wildfly-Image das War-File hinzuzufügen. Das Dockerfile wird einfach in dem Projekt neben dem Sourcecode miteingecheckt. Das Dockerfile wie es aussehen könnte:

FROM localhost:5000/wildfly

ADD target/demo.war $JBOSS_HOME/standalone/deployments/

Neben dem Dockerfile gibt es jetzt noch zusätzlich die Datei für Docker-Compose in welchem die Container-Konfiguration beschrieben ist. Um jetzt eine neue Version zu bauen und zu starten muss lediglich nach dem Maven-Build der Anwendung, der ein WAR hinterlässt, der docker-compose up –build Befehl abgesetzt werden. Dieser sorgt für ein Erstellen eines aktuellen Anwendungsimages, sowie ein Stoppen des alten Containers und anschließendes Erzeugen und Hochfahren des neuen Containers.

Demo

Ein Demoprojekt ist auf Github zu finden: https://github.com/GEDOPLAN/docker-demo
Das Projekt ist folgendermaßen aufgebaut:
Für jedes Image existiert hier ein Verzeichnis mit einem Dockerfile. Es gibt ein Basis-Image (Abgleitet von Apline-Linux) und darauf aufbauende Images für Java, Wildfly und Mysql. Das eigentliche Software Projekt ist in dem Ordner demo-project untergebracht. Es handelt sich um eine normale Webanwendung, welche als Maven-Projekt aufgesetzt wurde. Das Dockerfile des Projektes erzeugt einfach ein neues Image indem es dem Wildfly-Image das WAR hinzufügt. Ein Docker-Compose-File beschreibt die Container für Mysql und die Anwendung selbst.

Wie nun mit Docker gearbeitet werden kann, ist in dem Vagrant-File dokumentiert. Das File beinhaltet:

Installation von Docker
Starten von Docker
Starten einer eigenen Registry
Bauen der Images und pushen in Registry
Starten des Mysql-Containers (Danach Connecten und Datenbankeinrichten)
Stoppen des Mysql-Containers
Bauen des Projektes
Starten der Anwendung mit Docker-Compose

Für das Ausführen per Vagrant wird VirtualBox und Vagrant benötigt. Das Vagrant Plugin vagrant-vbguest muss ebenfalls installiert sein. Zum ausführen einfach in das Hauptverzeichnis wechseln (Wo das Vagrantfile liegt) und „vagrant up“ ausführen.

JPA mit dynamischer DB-Verbindung

In Java-EE-Anwendungen, die JPA zum DB-Zugriff nutzen, wird im Normalfall eine Datasource zur Spezifikation der Datenbankverbindung genutzt:

<persistence ...>
  <persistence-unit name="seminar">
    <jta-data-source>jdbc/seminar</jta-data-source>

Für eine so definierte Persistence Unit lässt man dann einen EntityManager injizieren – üblicherweise in einem CDI Producer:

public class EntityManagerProducer {
  @PersistenceContext(unitName = "seminar")
  EntityManager entityManager;

  @Produces
  public EntityManager getEntityManager() {
    return this.entityManager;

Das funktioniert so auch sehr gut, ist allerdings in Bezug auf die genutzte Datenbank recht statisch: Die Datasource – im Beispiel mit dem JNDI-Namen jdbc/seminar – muss zum Deployment-Zeitpunkt der Anwendung im Server konfiguriert sein. Zur Anwendungslaufzeit lässt sie sich nicht wechseln.

Um eine dynamische Zuordnung der DB zur Laufzeit zu ermöglichen, muss man die EntityManagerFactory per API initialieren und dabei die Connect-Parameter für die gewünschte Datenbank als Properties mitgeben:

<persistence ...>
  <persistence-unit name="showcase" transaction-type="JTA">
    <properties>
      <!-- Gemeinsame Parameter für die im Beispiel genutzten Datenbanken -->
      <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
      <property name="javax.persistence.jdbc.user" value="showcase" />
      <property name="javax.persistence.jdbc.password" value="showcase" />

      <!-- Diese Property wird eigentlich nicht benötigt, da sie beim Aufbau der EntityManagerFactory übergeben wird -->
      <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:" />
public class EntityManagerProducer {
  private EntityManagerFactory fetchEntityManagerFactory() {

    private ConcurrentHashMap<String, EntityManagerFactory> factoryMap = new ConcurrentHashMap<>();

    // Aktueller URL kommt aus anderem Service, z. B. "jdbc:h2:mem:showcase_1;DB_CLOSE_DELAY=-1"
    String url = ...; 

    // Falls nötig, Map-Eintrag für URL erstellen
    this.factoryMap.computeIfAbsent(url, u -> {
      Map<String, String> prop = new HashMap<>();

      // dabei URL als Property übergeben
      prop.put("javax.persistence.jdbc.url", url);

      // und DDL erlauben
      prop.put("eclipselink.ddl-generation", "create-or-extend-tables");
      prop.put("eclipselink.ddl-generation.output-mode", "database");
      prop.put("hibernate.hbm2ddl.auto", "update");

      return Persistence.createEntityManagerFactory("showcase", prop);
    });

    return this.factoryMap.get(url);
  }

...

Im Beispiel wird nur der Datenbank-URL dynamisch übergeben: Die Property javax.persistence.jdbc.url wird in Zeile 14 in ein Map-Obekt eingetragen und anschliessend zur Initialisierung der EntityManagerFactory verwendet. Im Beispiel wird ein ConcurrentHashMap genutzt, um die erzeugten Factories pro URL als Singletons zu führen.

Der Producer liefert nun einen EntityManager aus der so erzeugten EntityManagerFactory. Der Synchronisationstyp SynchronizationType.SYNCHRONIZED sorgt dafür, dass der EntityManager sich automatisch mit der JTA-Transaktion verbindet.
Da der EntityManager nun Application-managed ist, muss ein Disposer zum Schliessen vorgesehen werden:

...
  @Produces
  @RequestScoped
  EntityManager createEntityMnager()  {
    return fetchEntityManagerFactory().createEntityManager(SynchronizationType.SYNCHRONIZED);
  }

  void closeEntityManager(@Disposes EntityManager entityManager) {
    if (entityManager.isOpen()) {
      entityManager.close();

Auf diese Weise gelingt die Umschaltung der DB-Verbindung zur Laufzeit. Im Beispielprojekt sind die DB-URLs in einem separaten Service fest definiert. Sie könnten aber auch problemlos aus einer anderen, noch dynamischeren Quelle kommen.

Es zeigt sich hier einmal mehr, wie mächtig das Konzept der Producer in CDI ist: Die Dynamisierung der DBs geschieht alleine dort. Die konkreten DB-Zugriffe in der restlichen Anwendung bleiben unverändert.

Der sich nun möglicherweise einstellenden Euphorie muss allerdings ein „Aber“ entgegengestellt werden: Die gezeigte Lösung nutzt ein Feature aus, das in der JPA-Spezifikation nur schwach spezifiziert ist. Um einen EntityManager an JTA anbinden zu können, muss die Persistence Unit mit dem Parameter transaction-type="JTA" versehen sein. In dem Fall wird in der Spezifikation die Nutzung einer Datasource „erwartet“ – nicht etwa „verlangt“. Es ist sehr schade, dass die JPA-Spezifikation eine ganze Reihe solcher unterschiedlich interpretierbaren Sätze enthält.
In der Beispiellösung wird die DB-Verbindung über Properties angegeben, was Hibernate als JPA-Provider ohne Murren akzeptiert, während EclipseLink den Dienst verweigert. Insofern läuft die gezeigte Lösung nicht auf einem GlassFish (mit EclipseLink), wohl aber auf WildFly (mit Hibernate).

Den gezeigten Code finden Sie in einem kompletten Demoprojekt auf GitHub.

Detached Entities und Extended EntityManager

Bei unserem Workshop „Java EE 7 – Enterprise-Anwendungen ohne Ballast“ auf der JAX 2016 hatte ich das Problem, dass eine Liste von Daten, die als Lazy-Collection in einem Entity enthalten waren, trotz eines Extended EntityManager nicht geladen wurden, d. h. eine Lazyload-Exception auslösten. Im Workshop haben wir das Problem damit gelöst, dass die Daten mittels Entity Graph explizit geladen wurden. Die eigentlich offensichtliche Ursache, warum uns der Extended EntityManager in dem Fall gar nicht helfen konnte, versteckte sich sprichwörtlich in dem Wald, den man schon mal vor lauter Bäumen nicht sieht. Nach dem Workshop war es nach kurzer Zeit ruhigen Nachdenkens sofort klar, waran es lag:

Im Workshop wurde eine kleine Java-EE-Webanwendung zur Verwaltung von Konferenzbeiträgen entwickelt. Sie können Sie sich hier anschauen: https://github.com/dirkweil/javaee-workshop/tree/jax16. Betrachten Sie für das hier diskutierte Problem folgende Klassen und Facelets:

  • de.gedoplan.workshop.domain.Talk
    Dies ist die erwähnte Entity. Sie enthält die Lazy-Collection speakers
  • de.gedoplan.workshop.presentation.TalkPresenter
    Diese JSF-Präsentationsklasse sorgt für das Lesen der Talks in postConstruct
  • src/main/webapp/talk/talk.xhtml sowie …/talkEdit.xhtml
    Diese Facelets arbeiten auf Basis von TalkPresenter und bilden ein Master/Detail-View-Paar: talk.xhtml zeigt alle Talks an und verzweigt zur Bearbeitung eines einzelnen Talks nach talkEdit.xhtml.

Beim Bearbeiten eines Talks, d. h. beim Übergang von talk.xhtml zu talkEdit.xhtml kam es zu besagten Lazyload-Exception, die bei ruhiger Betrachtung auch ganz erklärlich ist:

  • Die Talks werden für talk.xhtml gelesen und in einem TalkPresenter-Objekt aufbewahrt. Nach dem Request sind natürlich die ggf. genutzte Transaktion und damit auch der zum Lesen genutzte EntityManager geschlossen, die gelesenen Talks also detached.
  • Beim Übergang zu talkEdit.xhtml wird einer der gelesen Talks direkt benutzt, um die Detaildaten anzuzeigen. Und da die Talks detached sind, funktioniert das Nachladen der Lazy-Daten nicht mehr – Peng!

Im Workshop haben wir dann die Variante genutzt, schon beim initialen Lesen der Talks auch die Speaker zu lesen, was aber nur dann wiklich sinnvoll ist, wenn die Speaker tatsächlich immer gebraucht werden, was für die Master-View talk.xhtml ja nicht zutrifft.

Besser wäre es gewesen, die Daten des im Detail anzuzeigenden Talks beim Übergang von talk.xhtml zu talkEdit.xhtml einfach neu einzulesen. So ist das jetzt in der aktuell auf GitHub befindlichen Version auch gelöst, zu sehen in der Methode TalkPresenter.editTalk.

Sollte Ihnen also eine zunächst unerklärliche Lazyload-Exception entgegen springen, überlegen Sie genau, wo die betroffenen Daten herkommen und ob sie im Moment des Nachladens nicht detached sind.

Eine alternative Lösung können Sie in dem Branch des Projektes betrachten, das im Zuge des Workshops auf der W-JAX 2015 in München entwickelt wurde (https://github.com/dirkweil/javaee-workshop/tree/wjax15): Hier wurde talk.xhtml nicht in den Flow integriert, sondern erst beim Übergang von talk.xhtml zu talkEdit.xhtml ein Flow namens „editTalk“ gestartet. Dadurch wurden die Daten vor dem Betreten der Detail-Seite aus der DB aktualisiert und waren beim Nachladen der Lazy-Daten nicht detached.

Man sieht: Scopes, Ladezeitpunkt und Lazyness von Entitydaten müssen aufeinander abgestimmt sein. Es braucht also ein wenig Planung vorweg, was im normalen Projektalltag auch problemlos gelingt. Bei einem teilweise recht agilen Workshop kann’s schonmal schief gehen, was ja auch Erkenntnisse bringt …