Maven Toolchains: Builds (u. a.) mit konfigurierbarem JDK

Wer mit der Betreuung von Software vertraut ist, die nicht mehr ganz taufrisch ist, kenn das Problem: Der Build muss mit einer älteren Java-Version geschehen, z. B. weil genau die beim Kunden im Einsatz ist oder weil bestimmte (auch nicht mehr ganz taufrische) Tools für aktuelle Java-Versionen nicht mehr zur Verfügung stehen.

Seit der Maven-Version 3.3.1 gibt es dafür eine Unterstützung in Form sog. Toolchains. Einfach gesprochen verbirgt sich dahinter eine Zuordnung von logischen Namen und Versionen zu Installationspfaden im Dateisystem. Dazu muss eine neue Konfigurationsdatei (per Default im Ordner .m2 des Users) angelegt werden:

<toolchains>
  <toolchain>
    <type>jdk</type>
    <provides>
      <version>1.6</version>
    </provides>
    <configuration>
      <jdkHome>C:\Program Files\Java\jdk1.6.0_45</jdkHome>
    </configuration>
  </toolchain>
  <toolchain>
    <type>jdk</type>
    <provides>
      <version>1.8</version>
    </provides>
    <configuration>
      <jdkHome>C:\Program Files\Java\jdk1.8.0_101</jdkHome>
    </configuration>
  </toolchain>
</toolchains>

Leider können in dieser Datei derzeit keine Maven-Properties in der Form ${name} verwendet werden. Wer also seine Java-Home-Pfade in Environment-Variablen hat, muss sie dennoch redundant in toolchains.xml eintragen.

Im pom.xml des Maven-Projekts kann nun auf diese Konfiguration Bezug genommen werden. Dazu muss das maven-toolchains-plugin mit der gewünschten Version konfiguriert werden:

<project ...>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-toolchains-plugin</artifactId>
        <version>1.1</version>
        <executions>
          <execution>
            <goals>
              <goal>toolchain</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <toolchains>
            <jdk>
              <version>1.6</version>
            </jdk>
          </toolchains>
        </configuration>
      </plugin>

Das Plugin prüft am Beginn des Builds, ob die gewünschte Version in toolchains.xml vorhanden ist und hinterläßt die zugehörigen Werte in der internen Maven-Session. Dort holen andere Plugins, u. a. das Compiler-Plugin, sich die für sie benötigten Werte ab.

Toolchains können nicht nur für JDKs aufgebaut werden. Details dazu finden sich in der Beschreibung des Plugins.

Ein Demo-Projekt ist hier verfügbar: https://github.com/GEDOPLAN/maven-toolchains-demo.

Git-Flow und das JGit-Flow Plugin für Maven

In diesem Artikel wird Git-Flow erläutert, wobei dabei allgemein das Konzept dahinter aufgegriffen wird. Der eigentliche Umgang mit den Git-Flow-Befehlen wird mit Hilfe des JGit-Flow Plugin für Maven gezeigt. Zusätzlich werden dennoch auch die Git- und Git-Flow-Befehle, die sich dahinter verstecken gezeigt.

Das JGit-Flow Maven Plugin

Das JGit-Flow Plugin für Maven bildet die Git-Flow Befehle als Maven Goals ab, sodass es mit diesem Plugin möglich ist die später beschriebenen Branches anzulegen ohne Git-Flow installiert zu haben. So kann dieses Plugin beispielsweise in eine Parent-POM eingebettet werden, sodass jedes abgeleitete Projekt automatisch die JGit-Flow-Befehle nutzen kann. So kann darauf verzichtet werden auf jedem Entwickler-Rechner Git-Flow zu installieren.

Einbinden des JGit-Flow Plugins

Zum Einbinden wird folgendes Plugin in die POM des Maven-Projektes hinzugefügt:

<build>
   <plugins>
      <plugin>
         <groupId>external.atlassian.jgitflow</groupId>
         <artifactId>jgitflow-maven-plugin</artifactId>
         <version>1.0-m5.1</version>
         <configuration>
            <username>username</username>
            <password>password</password>
            <pushFeatures>true</pushFeatures>
            <pushRelease>true</pushRelease>
            <pushHotfixes>true</pushHotfixes>
            <flowInitContext>
               <masterBranchName>master</masterBranchName>
               <developBranchName>develop</developBranchName>
               <featureBranchPrefix>feature-</featureBranchPrefix>
               <releaseBranchPrefix>release-</releaseBranchPrefix>
               <hotfixBranchPrefix>hotfix-</hotfixBranchPrefix>
               <versionTagPrefix>${project.artifactId}-</versionTagPrefix>
            </flowInitContext>
         </configuration>
      </plugin>
   </plugins>
</build>

In der Konfiguration können z.B. der Username und das Passwort zum Git Repository angegeben werden, sodass bei Erstellung eines neuen feature-, release-, oder hotfix-Branches die Branches direkt ins Repository gepusht werden ohne das die Credentials jedes Mal manuell angegeben werden müssen. Im flowInitContext werden Namen und Prefixes von bestimmten Branches definiert.

Git-Flow Grundlagen

Git-Flow beschreibt Abläufe zum Umgang mit Git und dem dort einfach zu nutzendem Branching. In Git-Flow sind fünf verschiedene Branch-Typen definiert:

  • master
  • develop
  • feature
  • release
  • hotfix

Im Folgenden werden die einzelnen Branches beschrieben und erklärt, welche Befehle zum Erstellen und wieder zusammenführen der einzelnen Branches verwendet werden. Zudem ist zu sehen welche einzelnen Git-Befehle sich aus den Git-Flow-/JGit-Flow-Befehlen ableiten lassen.

Die Hauptbranches  master und develop

Die Branches master und develop sind die einzigen Branches im Git-Flow, die beständig sind und nie gelöscht werden. Hierbei enthält der master meist die aktuell releaste Version oder eine für den Release bereite Version. Der develop-Branch enthält die aktuellen Änderungen aus der Entwicklung für das nächste Release z.B. ein neues Feature.

Die unterstützenden Branches

Der feature-Branch

Wie der Name schon sagt, werden feature-Branches genutzt, um neue Features für folgende Releases zu entwickeln. Typischerweise bestehen diese Branches nur in den Repositories der Entwickler und werden nicht in das Origin Repo gepusht. Dies bedeutet natürlich nicht, dass Entwickler nur alleine an einem Feature arbeiten sollen. Zum Zweck des Austausches zwischen Entwicklern die zusammen in kleineren Gruppen arbeiten, werden weitere Git remotes zwischen den zusammenarbeitenden Personen eingerichtet. Falls doch in einem Repository alle feature-Branches enthalten sind, muss darauf geachtet werden, dass immer auf dem passenden Branch gearbeitet wird und nicht versehentlich Arbeiten von anderen verändert werden. GitLab zum Beispiel ermöglicht die Berechtigungssteuerung für einzelne Branches, was bei großen Projekten evtl. Sinn macht, da das versehentliche arbeiten auf einem falschen Branch nicht erst passieren kann.

Der feature-Branch entsteht aus dem develop-Branch und wird nach der Fertigstellung des Features wieder in diesen gemerged. Als Namenskonvention sollte dieser Branch einen Namen wie feature- oder feature/ bekommen. Auf keinen Fall darf der Namen master, develop, release-* (release/) oder hotfix- (hotfix/*) heißen, da diese Namen für andere Branches vorgesehen sind.

Mit Hilfe der folgenden Befehle von Git-Flow oder JGit-Flow kann ein neuer Feature-Branch erstellt werden:

Git-Flow:

$ git flow feature start featurename

JGit-Flow Plugin:

mvn -B jgitflow:feature-start -DfeatureName=featurename -DenableFeatureVersions=true

Dahinter verbirgt sich in diesem Fall:

$ git checkout -b featurename develop

Dadurch wird lediglich ein Branch mit dem Namen „myfeature“ aus dem Branch „develop erzeugt. Anhand von feature-start sind noch keine Vorteile von Git-Flow zu erkennen, da der eigentliche Befehl nur aus einer Zeile besteht. Das JGit-Flow Plugin hingegen sorgt durch die Option -DenableFeatureVersions=true zusätzlich dafür, dass die Version in der POM für den feature-Branch angepasst wird. Heißt das Feature „newfeature“ wird die Version z.B. in „1.0.0-newfeature-SNAPSHOT“ geändert. Leider wird diese Versionsnummer bei dem Beenden des Features mit in die POM des develop-Branches übernommen. Wird von diesem develop-Branch erneut ein feature-Branch abgeleitet, so wird der Featurename wieder an die Version konkateniert, sodass die Version nach vielen entwickelten Features sehr lang und unübersichtlich wird.
-DfeatureName=featurename legt den Namen des Features fest. Normalerweise muss der Name bei der Durchführung des Goals bestätigt werden, dies kann verhindert werden, wenn wie oben zu sehen mit mvn -B der Batchmode aktiviert wird. So kann der Name ohne weitere Interaktion festgelegt werden.

Wenn ein Feature-Branch mit

Git-Flow:

$ git flow feature finish featurename

oder

JGit-Flow Plugin:

mvn jgitflow:feature-finish -DfeatureName=featurename

beendet wird, stecken mehrere Befehle dahinter:

$ git checkout develop
$ git merge --no-ff featurename
$ git branch -d featurename
$ git push origin develop

Zum Beenden eines Features muss zuerst in den develop-branch gewechselt werden. Danach wird das entwickelte Feature in den Branch gemerged und der feature-branch gelöscht. Zuletzt wird der develop-Branch ins Git-Repository gepusht. An diesem Beispiel ist zu sehen wie Git-Flow/JGit-Flow den Aufwand zum Beenden eines Features minimiert. Statt vier Befehlen muss lediglich einer ausgeführt werden.

Der release-Branch

Der release-Branch wird aus dem develop-Branch erzeugt, sobald dieser einen releasebereiten Status erreicht hat. Sobald ein release-Branch erzeugt wurde, fließen alle neuen Features oder Änderungen an dem develop-Branch in das darauffolgende Release ein. In dem release-Branch sind lediglich Bugfixes zulässig. Außerdem ist dies der Branch der am Ausgiebigsten getestet werden sollte, da dieser in den master gemerged wird und sich dort nur fertige, zum Release geeignete Versionen befinden sollten. Beim Start des release-Branches wird die Releasenummer festgelegt, die nach intern definierten Regeln gesetzt wird. Benannt wird diese Art von Branches mit dem Prefix release- oder release/.

Mit Hilfe der folgenden Befehle von Git-Flow oder JGit-Flow kann ein neuer Feature-Branch erstellt werden:

Git-Flow:

git flow release start 1.0.0

JGit-Flow Plugin:

mvn -B jgitflow:release-start -DreleaseVersion=1.0.0

Hinter diesen Befehlen stecken die Befehle:

$ git checkout -b release-1.0.0 develop
$ ./version-anpassen.sh 1.0.0
$ git commit -a -m "Versionsnummer auf 1.0.0 geändert"

Zuerst wird der release-Branch aus dem aktuellen develop-Branch erstellt. Die nächste Zeile besteht aus einem fiktiven Skript, das die Versionsnummer für die Releaseversion setzt. Nachdem die Änderung commited wurde, kann mit dem release-Branch gearbeitet werden.

Das Beenden des release-Branches sieht dann folgendermaßen aus:

Git-Flow:

git flow release finish 1.0.0

JGit-Flow Plugin:

mvn jgitflow:release-finish

Dahinter verbergen sich die Befehle:

$ git checkout master
$ git merge --no-ff release-1.2
$ git tag -a 1.2

Als erstes wird der release-Branch in den master gemerged. Danach bekommt die Version noch ein Tag.

$ git checkout develop
$ git merge --no-ff release-1.2

Zusätzlich muss der release-Branch in den develop-Branch überführt werden, um ggf. behobene Fehler auch in diesem zu beseitigen.

$ git branch -d release-1.2

Zum Schluss wird der nicht mehr benötigte release-Branch gelöscht.

Der hotfix-Branch

Der hotfix-Branch ist der einzige Branch der seinen Ursprung im master-Branch hat. Benannt wird diese Art von Branch mit dem Prefix hotfix- oder hotfix/. Dieser Branchtyp ähnelt dem release-Branch. Er dient zur Vorbereitung eines Produktions-Release, aber sind im Gegensatz zu release-Branches unplanmäßig. Genutzt wird dieser Typ, wenn in einer Live-Version kritische Fehler schnellstmöglich behoben werden müssen, der develop-Branch aber noch zu instabil ist, um produktiv zu gehen.

Git-Flow:

git flow hotfix start 1.0.1

JGit-Flow Plugin:

mvn -B jgitflow:hotfix-start -DreleaseVersion=1.0.1

Dahinter verbergen sich die Befehle:

$ git checkout -b hotfix-1.0.1 master
$./version-anpassen.sh 1.0.1
$ git commit -a -m "Versionsnummer auf 1.0.1 geändert"

Es ist zu erkennen, dass die Befehle, die genutzt werden identisch mit denen zum Erstellen eines release-Branches sind.

Beenden des release-Branches:

Git-Flow:

git flow hotfix finish 1.0.1

JGit-Flow Plugin:

mvn jgitflow:hotfix-finish

Dahinter verbergen sich die Befehle:

$ git checkout master
$ git merge --no-ff release-1.0.1
$ git tag -a 1.0.1
$ git checkout develop
$ git merge --no-ff release-1.0.1

Auch das Beenden eines hotfix-Branches gleicht dem eines release-Branches. Zuerst wird der Hotfix in den master gemerged und mit einem Tag versehen. Danach muss der hotfix-Branch wieder zusätzlich in den develop-Branch gemerged werden.

Fazit

Git-Flow/JGit-Flow vereinfachen das Arbeiten durch definierte Abläufe und einfachen Befehlen, sodass weniger Fehler beim Erstellen von neuen Branches auftreten können. Auch wenn nicht direkt Git-Flow mit deinen Befehlen genutzt wird sondern nur die Arbeitsweise mit der Brancherstellung übernommen wird, wird ein guter Leitfaden zum Arbeiten mit Git geliefert.

In Maven-Projekten ist der Einsatz des JGit-Flow Plugin praktisch, da nicht auf jedem Entwickler-Rechner Git-Flow installiert werden muss. Zudem wurde Git-Flow für Linux konzipiert und ist beispielsweise unter Windows ohne Cygwin nicht nutzbar. Wird stattdessen das JGit-Flow Plugin genutzt, so hat jeder Entwickler nach dem Klonen des Projektes die passende Git-Flow Konfiguration. Zudem werden die Versionen der einzelnen Branches automatisch angepasst, wenn dies so konfiguriert ist.

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:

&lt;dependency&gt;
  &lt;groupId&gt;org.codehaus.groovy&lt;/groupId&gt;
  &lt;artifactId&gt;groovy-jsr223&lt;/artifactId&gt;
  &lt;version&gt;2.4.7&lt;/version&gt;
  &lt;scope&gt;runtime&lt;/scope&gt;
&lt;/dependency&gt;

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(&quot;Script Engine: %s (%s)\n&quot;, scriptEngineFactory.getEngineName(), scriptEngineFactory.getEngineVersion());
  System.out.printf(&quot;  Language: %s (%s)\n&quot;, scriptEngineFactory.getLanguageName(), scriptEngineFactory.getLanguageVersion());

  for (String alias : scriptEngineFactory.getNames())
    System.out.printf(&quot;  Alias: %s\n&quot;, 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(&quot;js&quot;);
scriptEngine.eval(&quot;print('Hello, JavaScript!');&quot;);

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(&quot;netto&quot;, 100);
scriptEngine.put(&quot;steuersatz&quot;, 0.19);

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

Object brutto = scriptEngine.get(&quot;brutto&quot;);

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(&quot;scripts/hello.js&quot;);
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(&quot;scripts/factorial.js&quot;);
scriptEngine.eval(new InputStreamReader(resourceAsStream));
Invocable invocable = (Invocable) this.scriptEngine;
Object fac5 = invocable.invokeFunction(&quot;factorial&quot;, 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(&quot;scripts/showDate.js&quot;);
scriptEngine.eval(new InputStreamReader(resourceAsStream));
Invocable invocable = (Invocable) this.scriptEngine;
invocable.invokeFunction(&quot;showDate&quot;, 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(&quot;scripts/fillList.groovy&quot;);
scriptEngine.eval(new InputStreamReader(resourceAsStream));
List&lt;?&gt; list = (List&lt;?&gt;) scriptEngine.get(&quot;list&quot;);
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.