Scripting for Java: JavaScript, Groovy etc. in Java nutzen

In Java-Anwendungen können Skripte dynamischer Sprachen wie JavaScript, Groovy etc. aufgerufen werden. Die Basis wurde bereits vor langer Zeit mit dem JSR 223 gelegt: seit Java SE 6 ist die Einbindung der sog. Script Engines nach JSR 223 Teil der Java-Plattform.

Die Art der Skriptsprache ist nicht grundsätzlich eingeschränkt. Besonders interessant sind aber die Sprachen, die auf der Java VM laufen. Sie lassen sich mit der Java-Anwendung sehr eng verknüpfen: Es lassen sich Daten zwische Java und Skript austauschen, Skript-Funktionen aus Java heraus aufrufen und sogar Java-Objekte und Java-Methoden in einem Skript benutzen.

Script Engines

Zur Ausführung eines Skriptes wird eine passende Script Engine benötigt. Seit Java 6 ist eine Engine für JavaScript im Standardumfang von Java enthalten. Diese Rhino genanne Engine in den Versionen 6 und 7 wurde bei Java 8 durch Nashorn abgelöst.

Möchte man weitere Skriptsprachen einsetzen, muss die jeweils passende Implementierungsbibliothek in den Classpath aufgenommen werden. So ist bspw. für Groovy die folgende Maven Dependency nötig:

<dependency>
  <groupId>org.codehaus.groovy</groupId>
  <artifactId>groovy-jsr223</artifactId>
  <version>2.4.7</version>
  <scope>runtime</scope>
</dependency>

Den Einstiegspunkt zur Nutzung eines Skripts stellt die Klasse ScriptEngineManager dar. Mit ihrer Hilfe können zunächst mal die verfügbaren Script Engines aufgelistet werden:

ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
for (ScriptEngineFactory scriptEngineFactory : scriptEngineManager.getEngineFactories()) {
  System.out.printf("Script Engine: %s (%s)\n", scriptEngineFactory.getEngineName(), scriptEngineFactory.getEngineVersion());
  System.out.printf("  Language: %s (%s)\n", scriptEngineFactory.getLanguageName(), scriptEngineFactory.getLanguageVersion());

  for (String alias : scriptEngineFactory.getNames())
    System.out.printf("  Alias: %s\n", alias);
}

Im Beispielprojekt erzeugt dies die folgende Ausgabe:

Script Engine: Oracle Nashorn (1.8.0_72)
  Language: ECMAScript (ECMA - 262 Edition 5.1)
  Alias: nashorn
  Alias: Nashorn
  Alias: js
  Alias: JS
  Alias: JavaScript
  Alias: javascript
  Alias: ECMAScript
  Alias: ecmascript
Script Engine: Groovy Scripting Engine (2.0)
  Language: Groovy (2.4.7)
  Alias: groovy
  Alias: Groovy

Die Namen und Aliase der Script Engines können zur Instanzierung einer konkreten Engine genutzt werden, mit der anschließend Skript-Code ausgeführt werden kann:

ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("js");
scriptEngine.eval("print('Hello, JavaScript!');");

Datenaustausch zwischen Java und Skript

Nun ist das Starten von Skripten zwar interessant, verlöre aber schnell seinen Reiz, wenn man nicht zwischen dem aufrufenden Java-Programm und dem Skript Werte transferieren könnte. Dies ist zunächst einmal möglich mit Hilfe der sog. Bindings, die als Implementierung von Map in der Lage sind, Key-Value-Paare aufzunehmen. Es gibt je ein Bindings-Objekt auf globaler Ebene und in der genutzten Engine, zugreifbar mit der Methode getBindings in ScriptEngineManager bzw. ScriptEngine. Für den einfachen Zugriff auf die Key-Value-Paare gibt es in beiden Interfaces die von Map bekannten Methoden get und put.
Diese Bindings-Einträge lassen sich nun innerhalb des Skripts verwenden. Umgekehrt werden vom Skript erzeugte Werte im Bindings-Objekt der Engine abgelegt. Damit ist ein Austausch von Werten zwischen Java-Programm und Skript möglich:

scriptEngine.put("netto", 100);
scriptEngine.put("steuersatz", 0.19);

scriptEngine.eval("brutto = netto * (1+steuersatz)");

Object brutto = scriptEngine.get("brutto");

Das gezeigte Beispiel arbeitet mit dem Bindings-Objekt der ScriptEngine; die Methoden get und put sind Convenience-Methoden für den Aufruf der jeweils gleichnamigen Methode in getBindings(ScriptContext.ENGINE_SCOPE). Die in einem Bindings-Objekt enthaltenen Werte sind aus Sicht des Skriptes globale Daten vergleichbar mit den im Application Scope bzw. Session Scope einer Web-Anwendung abgelegten Werten.

Ausführung von Skript-Dateien

Einfacher Skript-Code lässt sich wie gezeigt direkt durch Übergabe des Codes als String an die Script Engine ausführen. Für umfangreichere Skripte ist kann statt dessen aber auch ein Reader an ScriptEngine.eval übergeben werden:

InputStream resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("scripts/hello.js");
scriptEngine.eval(new InputStreamReader(resourceAsStream));

Aufruf von Skript-Funktionen

Eine weitere Möglichkeit besteht darin, Skripte bzw. Teile daraus als Funktionen aufzurufen. Dazu wird das Skript zunächst einmalig ausgeführt und damit quasi kompiliert. Anschließend können im Skript definierte Funktionen aus dem Java-Programm heraus aufgerufen werden:

// scripts/factorial.js
function factorial(n) {
 return n == 1 ? 1 : n * factorial(n - 1)
}
InputStream resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("scripts/factorial.js");
scriptEngine.eval(new InputStreamReader(resourceAsStream));
Invocable invocable = (Invocable) this.scriptEngine;
Object fac5 = invocable.invokeFunction("factorial", 5);

In den Beispielen wurden nur primitive Daten bzw. Objekte der entsprechenden Wrapper-Klassen (int bzw. java.lang.Integer) zwischen Java und Skript ausgetauscht. Darauf ist man allerdings nicht eingeschränkt; es können vielmehr beliebige Objekte übergeben werden:

// scripts/showDate.js
function showDate(timestamp) {
  print('Jahr:  ' + timestamp.get(java.util.Calendar.YEAR))
  print('Monat: ' + (timestamp.get(java.util.Calendar.MONTH)+1))
  print('Tag:   ' + timestamp.get(java.util.Calendar.DAY_OF_MONTH))
}
InputStream resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("scripts/showDate.js");
scriptEngine.eval(new InputStreamReader(resourceAsStream));
Invocable invocable = (Invocable) this.scriptEngine;
invocable.invokeFunction("showDate", Calendar.getInstance());

Nutzung von Java-Objekten im Skript

Da das Skript auf der Java VM läuft, hat es auch zugriff auf die Standardbibliothek (oder andere Klassen im Classpath). Die Syntax zum Zugriff auf Java-Klassen ist natürlich sehr abhängig von der eingesetzten Skriptsprache. Groovy bietet z. B. eine Java-ähnliche Syntax an:

// scripts/fillList.groovy
list = new java.util.ArrayList();
list.add('Hugo')
list.add('Willi')
list.add('Otto')
InputStream resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("scripts/fillList.groovy");
scriptEngine.eval(new InputStreamReader(resourceAsStream));
List<?> list = (List<?>) scriptEngine.get("list");
list.forEach(System.out::println);

Weitere Informationen

Hibernate Envers – Entity Auditing

Beim Entity Auditing geht es darum Veränderungen an unseren JPA-Objekten festzuhalten um Nachverfolgen zu können wie sich ein Datensatz im Laufe der Zeit verändert hat. Bereits seit der Hibernate Version 3.5 steht „Envers“ im HibernateCore-Module bereit und bietet Out-Of-The-Box eine eben solche Lösung.

Die Funktionsweise von Envers ist schnell erklärt: intern arbeitet Envers mit Listenern die während einem merge / persist / remove  aktiv werden und den Zustand vor der Aktion in einer separaten Tabelle festhalten. Dabei verwendet Envers so genannte Revisions, eine Art Versionsnummer für Entitäten die pro Transaktion generiert wird und anhand der die unterschiedlichen Stände der Entitäten ermittelt werden können.

Unsere hier gezeigten Beispiele zielen auf den Wildfly 10 als Laufzeitumgebung ab der die benötigen Bibliotheken bereits (in Version 5.0.7) mit bringt. Um Envers im Projekt ein setzen reicht demnach eine provided-Abhängigkeit in unserer pom.xml:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-envers</artifactId>
    <version>5.0.7.Final</version>
    <scope>provided</scope>
</dependency>

Um nun ganze Entitäten oder wahlweise nur einzelne Attribute der Historisierung zu unterwerfen reicht es  mittels „@Audited“ Annotation dieses Hibernate mit zu teilen:

Auditing
@Entity
@Table(name = "ORDERTBL")
@Audited
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer orderID;

    @OneToMany(mappedBy = "order")
    @NotAudited
    private List<OrderDetail> orderDetails;

Das obige Beispiel zeigt die einfache Verwendung von Hibernate Envers. Mittels „@Audited“ markieren wir unsere Order-Entität für die Historisierung. Jegliche Transaktionen die Änderungen vornimmt führt nun zu einer neuen Revision. Dabei berücksichtigt Envers auch abhängige Objekte und würde diese, wenn nicht wie im obigen Beispiel mittels „@NotAudited“ annotiert, ebenfalls historisieren.

Neben der zu erwartenden Tabelle „ORDERTBL“ die unseren aktuellen Datensatz beinhaltet führt Envers nun eine weitere Tabelle mit dem default-Suffix „_AUD“ ein:

hibernate_envers

Hierbei handelt es sich um die Revisionstabelle für unsere Entität. Diese beinhaltet die Revisionnummer, einen Revisiontyp (ADD,MOD, DEL) und alle Daten der Entität, bzw. der Attribute die zur Historisierung markiert wurden. Eine entsprechende API zum Zugriff auf die Revisionsdaten stellt Envers ebenfalls bereit, dabei ist die Synatx sehr stark an die CriteriaAPI angelehnt.

Selektion der ersten Revision einer Bestellung mit bestimmter ID:

AuditReader auditReader = AuditReaderFactory.get(entityManager);
List<Number> revisions = auditReader.getRevisions(Order.class, order.getOrderID());
Order revOrder = auditReader.find(Order.class, order.getOrderID(), revisions.get(0));
Assert.assertTrue(revOrder.getShipName().equals(oldShipName));

Wer zum Teufel war das?

Eine häufige Anforderung ist ebenfalls das zu einer Revision zusätzliche Daten abgelegt werden sollen, z.B. das aktuelle Datum oder der Benutzer der die Änderung vollzogen hat. Hierzu bietet Envers ebenfalls eine Lösung. Zwei Klassen sind dazu zu implementieren

  • RevisonEntity
    • Entität welche die zusätzlichen Daten für jede Revision bereit hält
  • Revision Listener
    • Ermittlung der Daten
@Entity
@RevisionEntity(RevisionDataListener.class)
public class RevisionData extends DefaultRevisionEntity {

    @Temporal(TemporalType.TIMESTAMP)
    private Date changeDate;

    private String username;

    //Getter und Setter

}
public class RevisionDataListener implements RevisionListener {

    @Override
    public void newRevision(Object o) {
        RevisionData revData = (RevisionData) o;
        revData.setChangeDate(new Date());
        revData.setUsername(getUsername());
    }

    private String getUsername() {
    	return "some username"
    }

}

Jede Revision die erzeugt wird, durchläuft nun unserer Listener und wird mit den Informationen angereichert. Diese Informationen werden in einer separaten Tabelle mit entsprechender Referenz zur Revision in der Datenbank abgelegt und können mittels AuditReader ausgelesen werden:

hibernate_envers_revisiondata

// Revision-Objekt und zusätzliche Daten auf Basis der ID (8) und der Revision (55) ermitteln.
AuditQuery revQuery = auditReader.createQuery().forRevisionsOfEntity(Product.class, false, true);
revQuery.add(AuditEntity.revisionNumber().eq(55));
revQuery.add(AuditEntity.id().eq(8));

// liefert Objekt Array mit Entität, RevisionObjekt und RevisionType
Object[] revObject = (Object[]) revQuery.getSingleResult();

Product revProduct = (Product) revObject[0];
RevisionData revData = (RevisionData) revObject[1];
RevisionType revType = (RevisionType) revObject[2];

Fazit

Man sollte sich schon gut überlegen welche Daten wirklich dem Auditing unterworfen werden sollen, da dieser augenscheinlich so leichtgewichtige Ansatz natürlich bei schreibenden Operationen zu einem wesentlich höheren Aufwand auf Seiten der Datenbank führt. Davon abgesehen ist Envers eine sehr einfache Möglichkeit die Historisierung von Entitäten zu realisieren. Ein paar Annotationen reichen aus und Envers kümmert sich um den Rest.

Weite Informationen: Envers Docs

Github Projekt: https://github.com/GEDOPLAN/hibernate-envers

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.

JUnit,ShrinkWrap und der MavenImporter

In einen unserer letzten Beiträge haben wir einen Blick auf ein Basis-Setup für das Testen von Java EE Anwendungen geworfen. Dabei haben wir die zu testende Anwendung als vollständiges WAR deployt und zwar auf Basis eines zuvor laufenden Maven-Builds. Zumindest in Eclipse (mit seinen eigenen Build-Prozess) ist das nicht die optimale Wahl.

Die Möglichkeiten das zu testende Artefakt zu bauen sind Vielfältig, vom einzelnen hinzufügen von Klassen und Packages (Aufwändig, Fehleranfällig) bis hin zum vollständigen Deployment eines WAR-Archivs (Maven Build benötigt) ist alles machbar. Eine Variante die in den aktuelleren Versionen zur Verfügung steht geht noch einen Schritt weiter: auf Basis des der pom.xml die unser Projekt ja hinreichend beschreibt ist ShrinkWrap damit in der Lage unser Archiv selbständig zusammen zu bauen ohne das explizit ein Maven-Build im Vorfeld laufen muss:

pom.xml

<dependency>
    <groupId>org.jboss.shrinkwrap.resolver</groupId>
    <artifactId>shrinkwrap-resolver-impl-maven-archive</artifactId>
    <scope>test</scope>
</dependency>

Basis-Test-Klasse

@Deployment()
public static WebArchive createDeployment() {
    File pomFile = new File("pom.xml");
    WebArchive deployment = ShrinkWrap.create(MavenImporter.class, UUID.randomUUID().toString() + "_junit-demo-test.war")
            .loadPomFromFile(pomFile)
            .importBuildOutput().as(WebArchive.class);

    deployment
            .addPackage("de.gedoplan.webclients.test")
            .addPackage("de.gedoplan.webclients.testhelper")
            .addPackage("de.gedoplan.webclients.test.dbunit")
            .addAsResource(new File("src/test/resources/dbunit_full.xml"))
            .addAsResource("test-persistence.xml", "META-INF/persistence.xml");

    return deployment;
}

Mit dieser Konfiguration ist es den Eclipse-Benutzer nun auch wieder möglich direkt auf einer Test-Klasse den entsprechenden Test aus zu führen („Run as“ > „Junit Test“) ohne das vorher ein Maven-Build gelaufen ist (NetBeans z.B. würde diesen ohnehin vorher aufrufen).

Neben dem kompletten Laden des Projektes ermöglicht der Importer  es auch nur bestimmte Abhängigkeiten zu laden und diese dem Deployment hinzuzufügen um z.B. kleinere Test-Einheiten zu generieren. Nur eine kleine Einschränkung existiert derzeit noch: das komplette Laden des Projektes wie oben gezeigt wird derzeit für EAR-Projekte nicht unterstützt.

WildFly SSO für Webanwendungen mit PicketLink

Nachdem in einem vorherigen Blogeintrag (Wildfly SSO mit Keycloak (SAML,Oauth)) erklärt wurde wie Keycloak genutzt werden kann, soll in diesem Post darauf eingegangen werden wie SSO mit Hilfe von PicketLink realisiert werden kann.

Während Keycloak eine Out-Of-The-Box-Lösung für Security ist, welche auf PicketLink basiert, kann mit PicketLink die Security eigenständig in ein Projekt implementiert werden. PicketLink ist nicht nur ein Framework für SSO sondern bietet auch verschiedene andere Security-Lösungen für REST, LDAP-Anbindungen und vieles mehr.

Zur Beispielhaften Darstellung werden drei Applikationen herangezogen. Der Identity Provider, welcher die Authentifizierung durchführt und zwei Service Provider (SP) auf die nach einmaligen einloggen zugegriffen werden kann. Momentan läuft dieses Projekt nur in einem WildFly 9, die Grundkonfiguration von diesem ist in der GitHub README dieses Projektes beschrieben. Zusätzlich ist dort beschrieben wie PicketLink in den WildFly installiert wird.

Konfiguration der Security-Domains für den WildFly 9

Um SSO mit PicketLink zu realisieren, müssen zu Beginn zwei neue Security-Domains im WildFly eingerichtet werden.

securitydomain_idp.png

Die erste Security-Domain legt fest wie die Authentifizierung im Identity Provider durchgeführt werden soll. Der Einfachheit werden hier Property-Files verwendet. Alternativ könnten hier auch eine Datenbank, LDAP o.a. verwendet werden.

property_dateien.PNG

Die Property-Dateien für die Benutzer und Rollen müssen defaultmäßig in dem resources-Ordner des Identity Provider Projektes abgelegt sein.

securitydomain_sp.png

Die Security-Domain für die Service Provider legt das von PicketLink verwendete Login-Modul fest.Damit Der Application-Server das Authentifizieren über SAML-Assertions unterstützt und diese auflösen kann.

Erstellen eines Identity Providers

Für den Identity Provider wird ein normales Java-EE-Projekt angelegt. Dieses Projekt muss folgende Deskriptoren im WEB-INF-Order zur Konfiguration enthalten:

  • jboss-deployment-structure.xml
  • jboss-web.xml
  • picketlink.xml

picketlink_deployment_structure.png

Die Dependency des PicketLink-Moduls muss über die jboss-deployment-structure.xml in das Projekt eingebunden werden damit der Application-Server mit PicketLink arbeiten kann.

jboss-web.PNG

In der jboss-web.xml wird festgelegt, welche Security-Domain genutzt werden soll. In diesem Fall ist es die Security-Domain mit dem Namen „idp“, die vorher im WildFly konfiguriert wurde.

picketlink_IDP.PNG

Das oben zu sehende Bild zeigt die picketlink.xml. Mit dem Tag <IdentityURL> wird festgelegt über welche URL der Identity Provider angesprochen werden bzw. wo sich der Kontextpfad zur Anwendung auf dem Application Server befindet. In dem <Trust>-Tag können mehrere Domains angegeben werden, diese werden von dem Identity provider als vertrauenswürdig eingestuft. Zusätzlich können verschiedene Handler definiert werden. In diesem Fall sind es Handler, die PicketLink von Haus aus bereit stellt und die für die SAML Authentifizierung genutzt werden können. Darunter befindet sich z.B. der SAML2LogoutHandler, dieser gewährleistet die Möglichkeit von SLO (Single LogOut). SLO ist das Gegenteil von SSO, sodass sich durch einen Logout-Vorgang von allen Applikationen abgemeldet werden kann. Das <PicketLinkSTS>-Tag, das hier nicht mit abgebildet ist, kann genutzt werden um beispielsweise ein Token-Timeout oder Verschiedene Token-Provider festzulegen.

Zum Schluss müssen Anpassungen an der web.xml durchgeführt werden. Die Security-Constraints und Security-Roles werden wie üblich konfiguriert. Auch die Login-Konfiguration ist eine normale Form-Based Authentication. Deshalb wird an diesem Punkt nur auf die PicketLink spezifischen Änderungen eingegangen.

web_picketlink.PNG

In der web.xml wird der IDPHttpSessionListener definiert. Zudem wird noch der IDPFilter und dessen Filter-Mapping festgelegt.

 

Erstellen eines Service Providers

Das Erstellen eines Service Providers funktioniert so problemlos wie das Erstellen eines Service Providers. Auch der Service Provider benötigt folgende Deskriptoren:

  • jboss-deployment-structure.xml
  • jboss-web.xml
  • picketlink.xml

picketlink_extension.png

Zudem muss in dem Ordner WEB-INF/classes/META-INF/services die Datei „io.undertow.servlet.ServletExtension“mit dem oben abgebildeten Inhalt vorhanden sein. Dies sorgt dafür, dass PicketLink seine eigene ServletExtension nutzt.

Die jboss-deployment-structure.xml hat den selben Inhalt wie schon bei dem Identity Provider. Auch hier wird lediglich die Dependency zum PicketLink-Modul angegeben .

jboss-web-sp.PNG

Auch die jboss-web.xml unterscheidet sich kaum, es wird lediglich die Security-Domain der Service Provider angegeben.

picketlink_sp_.PNG

Die picketlink.xml nutzt bei den Service Providern das Tag <PicketLinkSP> hier stehen zwei weitere Attribute zur Verfügung. Mit ErrorPage kann festgelegt werden wohin weitergeleitet wird, wenn eine Authentifizierung scheitert. LogOutPage hingegen bestimmt wohin nach einem Logout navigiert wird. Werden diese beiden Konfigurationen nicht vorgenommen wird zu den Seiten /error.jsp bzw. /logout.jsp navigiert. Zwischen den IdentityURL-Tags wird die URL zum Identity Provider angegeben, der für die Authentifizierung genutzt werden soll. Zu dieser URL wird beim Zugriff auf einen SP weitergeleitet, wenn noch keine Authentifizierung durchgeführt wurde. Die darauf folgende ServiceURL gibt den Kontext-Pfad des Service Providers an. Der Rest der picketlink.xml ist identisch mit der des Identity Providers. Ein <PicketLinkSTS>-Tag zur weiteren Konfiguration wie bei einem Identity Provider steht nicht zur Verfügung.

webxml_formbased.PNG

In der web.xml des Service Providers muss die login-config ebenfalls vorhanden sein. Außerdem können in der web.xml die Security-Constraints des jeweiligen Service Providers konfiguriert werden. Ansonsten befinden sich hier keine PicketLink spezifischen Definitionen.

Es ist zu sehen, dass es außer ein wenig Aufwand kein Hexenwerk ist seinen eigenen Identity Provider zu erstellen und verschiedene Service Provider mit SSO zu versehen.

Leider ist die Frage wie es momentan mit PicketLink weiter geht nicht ganz klar. PicketLink soll in nächster Zeit mit Keycloak gemerged werden, dass PicketLink Projekt soll dennoch als Branch weiter bestehen und kann von der Community weiterentwickelt werden. Red Hat will aktiv nicht an diesem Branch weiterentwickelt, wollen der Community aber mit Rat und Tat zur Seite stehen. Evtl. könnte nach dem Merge auch die PicketLink API aus Keycloak nutzbar sein, doch dazu gibt es noch keine weiteren Informationen.

Lassen wir uns überraschen.

Das komplette Beispiel-Projekt mit einem Identity Provider und zwei Service Providern kann unter https://github.com/GEDOPLAN/sso-picketlink-saml-demo betrachtet werden.

Jackson + Glassfish 4.1.1

Vor kurzem hatten wir einen netten Beitrag zum Thema JSON-Parsen mit Jackson (Beitrag). Die dort verwendeten Beispiele wurden alle auf einem Wildfly zum laufen gebracht, schließlich wird Jackson als Standard JSON-Parser auf diesem Server verwendet. Aber wie sieht es denn mit dem Glassfish aus?

Der Glassfish 4 verwendet im Standard „Moxy“ als Parser für JSON. Jackson wird jedoch als Alternative mitgeliefert und so Bedarf es „eigentlich“ „nur“ einer Konfiguration um die Bibliothek zu aktivieren:

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>2.15</version>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>org.glassfish.jersey.core</groupId>
    <artifactId>jersey-server</artifactId>
    <version>2.10.4</version>
    <scope>provided</scope>
</dependency>

Und die Registrierung unserer Webservices:


import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("/rest")
public class JerseyApplicationResource extends ResourceConfig {

    public JerseyApplicationResource() {
        System.out.println("Register Jackson");
        super.register(JacksonFeature.class)
                .packages("de.gedoplan.showcase.resources");
    }

Theoretisch sind wir damit am Ende . Leider nur theoretisch. Auf dem aktuellen Glassfish 4.1.1 beglückt uns dieses Setup mit einer Fehlermeldung:

java.lang.ClassNotFoundException: com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector not found by com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider [128]
	at org.apache.felix.framework.BundleWiringImpl.findClassOrResourceByDelegation(BundleWiringImpl.java:1532)
	at org.apache.felix.framework.BundleWiringImpl.access$400(BundleWiringImpl.java:75)
	at org.apache.felix.framework.BundleWiringImpl$BundleClassLoader.loadClass(BundleWiringImpl.java:1955)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)

Dahiner steckt ein seit langem bekannter Bug:
https://java.net/jira/browse/GLASSFISH-21141

Dieser ist zum Glück schnell behoben. Durch das hinzufügen der fehlenden Bibliothek: jackson-module-jaxb-annotations-2.5.4 im glassfish/modules – Ordner und anschließendem Restart funktioniert Jackson auch Klaglos mit unserem Glassfish.

Demoprojekt erweitert
pom.xml
JerseyApplicationResource.java

Java EE im Test – Junit, DBUnit, Arquillian, Jailer

junit_title.png

Testen ist ein leidiges Thema: kaum jemand schreibt sie gerne, jeder weiß das sie notwendig sind und alle freuen sich wenn es welche gibt. Dabei ist es auch in einer Java EE Umgebung gar nicht schwer die Grundlage fürs Testen zu legen.

Arquillian

Arquillian ist ein Test-Framework das es uns erlaubt die Anwendung innerhalb des Containers zu testen. Anders als „einfache“ Junit Tests laufen also auch unsere Testfälle innerhalb unseres ApplicationServers. Das hat den entscheiden Vorteil das alle Infrastruktur-Dienste auch in unseren Tests zur Verfügung stehen: Datenbankzugriffe, Transaktionshandling und CDI Injection? Kein Problem! Dabei ist Arquillian nicht an Junit gebunden, sondern bietet auch Unterstützung für TestNG. Da in unserem Beispiel Junit zum Einsatz kommt sieht unser Maven pom.xml wie folgt aus:

<!--Junit als Basis für unsere Tests-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

<!-- Arquillian JUnit Integration -->
<dependency>
    <groupId>org.jboss.arquillian.junit</groupId>
    <artifactId>arquillian-junit-container</artifactId>
    <scope>test</scope>
</dependency>

<!-- Verbindung zum ApplicationServer -->
<dependency>
    <groupId>org.wildfly.arquillian</groupId>
    <artifactId>wildfly-arquillian-container-managed</artifactId>
    <version>1.1.0.Final</version>
    <scope>test</scope>
</dependency>

Neben Junit selbst und die zugehörige Integration mit Arquillian deklarieren wir noch einen Konnektor der dafür Sorge trägt wie unsere Tests später ihren Weg auf den ApplicationServer finden. Diese Konnektoren existieren für diverse Application Server in aller Regel in 3 unterschiedlichen Ausführung

  • embedded, Arquillian startet einen eigenen ApplicationServer mit festgelegter Konfiguration
  • remote, Arquillian deployt die Anwendung auf einen entfernt laufenden Server
  • managed, Arquillan deployt die Anwendung auf einen lokal bereits laufenden Server

Bevor unsere Tests nun ausgeführt werden, muss nun dafür gesorgt werden das Arquillian die Anwendung für unsere Tests bereit stellt. Der erste Schritt dazu ist unseren Test mit „@RunWith(Arquillian.class)“ zu annotieren damit Arquillian sich dieser Tests annimmt. Nun benötigen wir das eigentliche Deployment unserer Anwendung. Dazu setzt Arquillian auf „ShrinkWrap“ um dynamische Archive zu bauen auf dessen Basis die Tests statt finden. Damit haben wir die Möglichkeit nur dediziert Teile zu deployen die für unseren aktuellen Test benötigt werden. Dabei ist allerdings darauf zu achten das  auch wirklich alle abhängigen Klassen zusätzlich hinzufügt werden und auch die Deskriptoren müssen enthalten sein. Eine charmante Variante dagegen ist es das bereits gebaute Archiv unserer Anwendung zu verwenden und anschließend nur noch die nötigen Manipulationen durchzuführen (z.B. CDI Alternativen aktivieren oder die Datasource zu ändern). Die Vorbereitung eines solchen Archivs geschieht in einer Methode die mittels „@Deployment()“ annotiert ist.

@Deployment()
public static WebArchive createDeployment() {
    WebArchive deployment = ShrinkWrap
            .create(WebArchive.class, "junit-demo-test.war")
            .as(ZipImporter.class)
            .importFrom(new File("target/junit-demo.war"))
            .as(WebArchive.class)
            .addPackage("de.gedoplan.webclients.test")

    deployment.delete("META-INF/persistence.xml");
    deployment.addAsResource("test-persistence.xml", "META-INF/persistence.xml");

    return deployment;
}

Es bietet sich an eine solche Methode in eine Basisklassen aus zu lagern, sodass unser konkreter Test wie folgt aussieht:

@RunWith(Arquillian.class)
public class CustomerServiceTest extends TestBaseClass {

    @Inject
    CustomerService service;

    @Test
    public void calculateCustomerDiscount() {
        Assert.assertTrue(service.calculateCustomerDiscount("ALFAA").getDiscount() == 2.5);
    }
}

Hier ist gut zu sehen wie einfach dann ein konkreter Testfall ist. Da unser Test auf dem Server abläuft können wir CDI nutzen um direkt unsere Services zu injizieren und unsere Tests zu schreiben.

Alles auf Anfang, DBUnit

Eine Herausforderung bei den oben gesehenen Tests sind die Daten. Da wir mit einem echten Datenbestand arbeiten auf den wir auch schreibend zugreifen ist es anzuraten  einen festen Testdatenbestand bereit zu stellen. Diese Daten sollten nicht nur für konkrete Tests verändert werden können, sondern sollten vor allem Konstant bei jedem Test in gleicher Art und Weise zur Verfügung steht. Hier auf die „normale“ Testdatenbank zu setzen wird in aller Regel dazu führen das regelmäßig Tests aufgrund von geänderter Testdaten Fehlschlagen. DBUnit ist dabei ein etablierter Kandidat der auf eine XML-Datenbeschreibung setzt um Datenbanken in einen festgelegten Zustand zu versetzen. Eine solche Struktur beschreibt dann die konkreten Tabellen (shippers, categories…) und die Werte der entsprechenden Tabellenspalten (ShipperID, CategoryName…)

<dataset>
  <shippers ShipperID="1" CompanyName="Speedy Express" Phone="(503) 555-9831"/>
  <shippers ShipperID="2" CompanyName="United Package" Phone="(503) 555-3199"/>
  <shippers ShipperID="3" CompanyName="Federal Shipping" Phone="(503) 555-9931"/>

  <categories CategoryID="1" CategoryName="Beverages" Description="Soft drinks, coffees, teas, beers, and ales" Picture="beverages.gif"/>
  <categories CategoryID="2" CategoryName="Condiments" Description="Sweet and savory sauces, relishes, spreads, and seasonings" Picture="condiments.gif"/>
  <categories CategoryID="3" CategoryName="Confections" Description="Desserts, candies, and sweet breads" Picture="confections.gif"/>
</dataset>

Einen solchen Datenbestand können wir dann zu Beginn jeden Testes bereitstellen

@Before
public void initData() throws Exception {
    //Verbindung aufbauen
    Class driverClass = Class.forName("org.h2.Driver");
    Connection jdbcConnection = DriverManager.getConnection("jdbc:h2:~/junit;AUTO_SERVER=TRUE", "sa", "sa");

    //Datenladen
    InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("dbunit_full.xml");
    IDataSet dataset = new FlatXmlDataSet(inputStream);

    //Datenbank füllen
    DatabaseOperation.CLEAN_INSERT.execute(jdbcConnection, dataset);
}

(komplettes Beispiel)

Im obigen Beispiel verwenden wir aus gutem Grund eine separate H2-Datenbank. Mittels „CLEAN_INSERT“ veranlassen wir DBUnit dazu alle bestehenden Daten zu löschen. Das ist die beste Grundlage für valide Tests, sollte allerdings nicht mit der „echten“ Testdatenbank durchgeführt werden (! dazu passend müssen wir natürlich auch die test-persistence.xml anpassen die wir oben für unser Deployment erzeugen und mit ShrinkWrap installieren)

Wunde Finger? Testdaten mit Jailer

Die oben verwendeten Testdaten müssen natürlich nicht von Hand geschrieben werden. DBUnit bietet hierfür eine sehr einfache API an um bestehenden Datenbanktabellen in ein solches Format zu übertragen (DatabaseExportSample.java). Hier können wir festlegen ob eine komplette Datenbank exportiert oder selektiv nur benötige Tabelle ausgewertet werden sollen. Eine noch schönere Variante ist die Verwendung eines zusätzlichen Tools: „Jailer“. Jailer bietet uns die Möglichkeit sehr Feingranulat die Daten aus zu wählen, welche für wir exportieren wollen. Gerade bei sehr großen Datenbeständen will man als ersten Stand in aller Regel nur eine überschaubare Anzahl von Testdaten haben. Dafür bietet Jailer entsprechende Selektionskriterien und ermittelt selbstständig die benötigten Relationen (die bei Bedarf auch individuell deaktiviert werden können). Im unteren Beispiel selektieren wir z.B. ausgehenden von einer geringen Anzahl an Bestellungen alle abhängigen Daten, unterbinden aber bestimmte Relationen, wie z.B. customers > orders um die Datenmenge gering zu halten.

junit_jailer

Wie wir gesehen haben sind Tests im Java EE Umfeld kein Hexenwerk. DBUnit, Jailer, Arquillian und Junit bietet optimale Voraussetzungen um Tests zu schreiben. Allerdings bleibt es dabei: Schreiben müssen wir die Tests weiterhin selber.

Beispiel Projekt mit erweiterten Beispielen wie JAAS / RoleAllowed Testing und Maven Setup:

https://github.com/GEDOPLAN/junit-demo

Folgen

Erhalte jeden neuen Beitrag in deinen Posteingang.

Schließe dich 183 Followern an