Anwendungskonfiguration mit Apache Tamaya

Nahezu jede Anwendung muss irgendwie konfiguriert werden – seien es Namen, die statt im Code fest eingebaut zu werden, aus einem File gelesen werden sollen, seien es Berechnungsparameter oder URLs. Java (EE) bietet dazu bislang keinen Standard an, so dass jeder sich „sein eigenes Süppchen kochen“ muss.

Nun zeigt sich ein Silberstreif am Horizont in Form eines API zur Konfiguration von Anwendungen, das auf der diesjährigen Java One als Bestandteil von Java EE 8 angekündigt wurde und das Ideen aus dem entsprechenden Teil von Apache DeltaSpike und von Apache Tamaya übernehmen soll. Letzteres soll im Folgenden kurz beschrieben werden.

Apache Tamaya (tamaya.incubator.apache.org) befindet sich derzeit noch in einem sehr frühen Stadium, bedient sich aber bewährter Ideen aus Apache DeltaSpike Configuration und ist somit durchaus in Grenzen bereits einsetzbar.

Apache Tamaya ist modular aufgebaut. Der Core-Anteil wird immer benötigt. Darüber hinaus gibt es diverse Extensions wie bspw. zur Bereitstellung von Konfigurationswerten per CDI Injection. Für Maven-Projekte sind dies die folgenden Dependencies:

<dependency>
  <groupId>org.apache.tamaya</groupId>
  <artifactId>tamaya-core</artifactId>
  <version>${version.tamaya}</version>
</dependency>
<dependency>
  <groupId>org.apache.tamaya</groupId>
  <artifactId>tamaya-cdi</artifactId>
  <version>${version.tamaya}</version>
</dependency>

Derzeit (Dez. 2016) ist die aktuelle Version 0.2-incubating.

Konfigurationswerte können mit einem einfachen API abgeholt werden:

String javaVendor 
  = ConfigurationProvider.getConfiguration()
                         .getOrDefault("java.vendor", "unknown");

ConfigurationProvider.getConfiguration() liefert dabei das Einstiegsobjekt vom Typ Configuration, das neben der oben genutzten Methode getOrDefault auch solche anbietet, die den Wert in einen anderen Typ als String gewandelt liefern.

In CDI Beans kann zudem per Injektion auf die Werte zugegriffen werden:

@Inject
@Config(defaultValue = "unknown")
String companyName;

@Inject
@Config
int answerToLifeUniverseAndEverything;

@Inject
@Config("java.version")
String javaVersion;

Apache Tamaya holt die Werte per Default aus System Properties, Environment Entries und der Datei META-INF/javaconfiguration.properties im Classpath. Es können weitere Property Sources registriert werden und auch die Suchreihenfolge manipuliert werden.

Weitergehende Informationen finden sich auf der Homepage des Projektes (tamaya.incubator.apache.org). Aufgrund des aktuellen Projektstands ist aber noch viel im Fluss.

Ein Demo-Projekt zum Anschauen und Ausprobieren gibt es hier: github.com/GEDOPLAN/config-demo.

Microservices und das Uberjar

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

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

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

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

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

Angular 2 – Lazy Load Modules

Unsere bisherigen Beispiele sind alle davon ausgegangen das es ein zentrales Modul gibt dessen Referenzen von Webpack analysiert und in in großes JavaScript-Bundle zusammen gefasst wird. Für kleine Anwendungen mag dieses Vorgehen gut funktionieren, wird die Anwendung jedoch komplexer und lassen sich einzelne Bereiche gut voneinander abtrennen bietet es sich an die eigenen Anwendung in mehrere Module zu unterteilen und diese nur bei Bedarf zu laden.

Webpack – prepare

Wir werden, anders als bisher, nicht mehr alle Komponenten die wir per Routing ansteuern wollen in unseren Routen importieren. Damit Webpack die Deklaration von Angular 2 spezifischen asynchronen Routen erkennt benötigen wir einen zusätzlichen Loader:

npm install angular2-router-loader --save-dev

Zusätzlich registrieren wir diesen Loader für unsereTypeScript-Dateien in unserer Konfiguration:

webpack.common.js

...
{
    test: /\.ts$/,
    loaders: ['ts-loader','angular2-router-loader']
}
...

Modul die Zweite

In unserem sehr einfachen Beispiel wollen wir nun die „zweite“ Seite unserer Anwendung in ein eigenes Modul verschieben, um diese später nur bei Bedarf laden zu können. Dazu erzeugen wir eine neue Modul-Defitionsdatei mit einer Route-Definition für unser Unter-Modul:

hello.module.ts

import { NgModule}      from '@angular/core';
import { routing } from './hello.routing';
import {HelloComponent} from '../'

@NgModule({
    imports: [routing],
    declarations: [HelloComponent],
    providers: [ ],
    exports: [HelloComponent]
})
export class HelloModule { }

hello.routing.ts

import { Routes, RouterModule } from '@angular/router';
import {HelloComponent} from '../'

const appRoutes: Routes = [
    { path: '', component: HelloComponent}
];

export const routing = RouterModule.forChild(appRoutes);

In großen Teilen unterscheidet sich diese Modul-Definition nicht von unserem Root-Modul. Wir geben hier lediglich kein „bootstrap“-Attribut an. Darüber hinaus referenzieren wir natürlich nur die externen Module („imports“), die eigenen Komponenten („declarations“) und Provider („providers“) die in diesem Modul gültig sind. Auch die Routing-Datei ändert sich lediglich an einer Stelle. Anstatt das Routing als Root-Navigation zu exportieren (.forRoot) geben wir lediglich an das es sich bei diesen exportierten Routen um Navigationen für ein Kind-Modul handelt (.forChild).

Laden? Später bitte

Die entscheiden Anpassung in unserem Root-Modul geschieht nun in Routen-Definition:

app.routing.ts

// -- ALT --
...
const appRoutes: Routes = [
    { path: '', component: HomeComponent},
    { path: 'hello', component: HelloComponent},
    { path: 'animation', component: AnimationComponent},
];
...

// -- NEU --

import { Routes, RouterModule } from '@angular/router';
import {HomeComponent} from './components'

const appRoutes: Routes = [
    { path: '', component: HomeComponent},
     { path: 'hello', loadChildren: './components/+hello/hello.module#HelloModule' },
     { path: 'animation', loadChildren: './components/+animation/animation.module#AnimationModule' }
];

export const appRoutingProviders: any[] = [

];

export const routing = RouterModule.forRoot(appRoutes);

Anstatt direkt die Kind-Komponenten zu referenzieren verwenden wir eine spezielle URL-Syntax die dank des oben installierten und registrierten Webpack-Loaders auch beim Bundling der Anwendung funktioniert. Asynchrone Routen werden wie hier zu sehen mittels „loadChildren“ Attribut deklariert, gefolgt von einem String im Aufbau:

„[Pfad-zur-Modul-Datei]#[ModulName]“

(die hier verwendeten Bezeichnungen der Ordner mit vorangestelltem „+“ Zeichen ist nicht verpflichtend, ist aber eine allgemeine akzeptierte Konvention um asynchrone Komponenten zu markieren)

Alles zu seiner Zeit

Die oben gezeigte Konfiguration führt nun dazu das Webpack mehrere Bundles ausprägt, ein Bundle pro Module, sodass diese später geladen werden können. Dies lässt sich dann auch in der laufenden Anwendung nachvollziehen. Bei der Navigation zwischen den Seiten (/ Modulen) lädt Angular diese Kind-Komponenten erst wenn sie angesteuert werden:

angular2_lazy_routing_network

Lazy-Loaded Routes ist eine optimale und einfach zu verwendete Möglichkeit die Anwendung in schneller ladbare Teile zu teilen und den Anwender bei großen Anwendungen nicht mit Programmteilen zu belasten die er vielleicht gar nicht verwendet. Das gezeigteBeispiel gibt es wie immer auch bei Github:

https://github.com/dominikmathmann/angular-webpack-starter/tree/lazy-loaded-routes

Angular 2 Animationen

Angular 2 hat seinen Beta-Status verlassen und liegt in einer finalen Version vor. Fester Bestandsteil der Version ist die deklarative Entwicklung von CSS Animationen.

Animationen  die durch Angular gesteuert werden sollen werden innerhalb der konkreten Komponenten festgelegt. Dazu bietet das „@Component“ Element ein entsprechendes Attribut. Im Kern bestehen die hier abgelegten Animationen aus „States“ (festgelegte Zustände) und „Transitions“ (der Übergang zwischen diesen Zuständen) die mit entsprechenden Style- und Animations-Angaben versehen werde können. Die States sind selbst gewählte String-Werte die später in der Anwendung die Animationen triggern.

Hallo Komponente

Schauen wir uns erst einmal ein ganz einfaches Beispiel an (die verwendeten Komponenten wie ‚trigger‘, ’style‘ etc. stammen alle aus dem Modul ‚@angular/core‘):

@Component({
    animations: [
            trigger("fadeIn", [
                transition("void => *", [
                    style({ opacity: 0 }),
                    animate('2s ease'),

                ])
    ]),
    ],
    template: "
<div @fadeIn>...content...</div>
"
})
export class AnimationComponent {...}

Unsere Deklaration beginnt mit einem eindeutigen Bezeichner („trigger), den wir auch später im Template wieder finden werden. In diesem einfachen Beispiel definierten wir keinen eigenen State sondern verwenden zwei Vordefinierte und deklarieren nur die Übergänge zwischen den Beiden. „*“ ist eine Wildcard und bezeichnet jeden Zustand, „void“ bezeichnet den Zustand wenn ein Element noch nicht in der GUI angezeigt wird. Mit „transition“ geben wir nun den Übergang zwischen zwei Zuständen an. In unserem Beispiel handelt es sich um eine klassische Fade In-Animation ( void => * , von „nicht sichtbar“ in einen „beliebig anderen Status“). Innerhalb der „transition“ geben wir nun die Stylings an, die zu Beginn dieser Animation gesetzt werden sollen und die gewünschten Animationsangaben. Das Styling am Ende der Animation wird durch das bestehende CSS definiert. Die Verknüpfung im Template geschieht nun über den angegeben Trigger, dieser wird mit einem vorangestellten „@“ an das HTML-Element geschrieben welches mit der Animation versehen werden soll

ng2_animation_fadein.gif

Mein State

Mit eigenen States können wir den Status einer Animation an unsere Anwendungslogik binden. Ein Beispiel hierzu ist das Starten einer Animation auf Basis einer Benutzerinteraktion. Hierzu definierten wir ein oder mehrere Zustände welche die Styling-Informationen beinhalten welche die HTML-Komponente in diesem Zustand (am Ende der Animation) haben soll. Hier ein Beispiel: (dieses ließe sich auch durch die Verwendung von *ngIf und analog zum ersten Beispiel umsetzen)

@Component({
    animations: [
        trigger('moveInTop', [
            state("true", style({transform: "translateY(0%)", opacity: 1})),
            state("false", style({transform: "translateY(-500%)", opacity: 0})),
            transition("* => 1", [
                style({transform: "translateY(-50%)", opacity: 0}),
                animate("1s")
            ]),
            transition("1 => 0", [
                animate("1s ease")
            ])
        ])
    ],
    template: `<button type="button" class="btn-primary btn-lg" on-click="toggleShow()">Click me</button>\n\
<div [@moveInTop]="show" (@moveInTop.done)="afterAnimation($event)>...content...</div>
`
})
export class AnimationComponent {
    show=false;

    toggleShow(){
        this.show=!this.show;
    }

    afterAnimation(event:AnimationTransitionEvent){
        //...
    }
}

ng2_animation_click

Wir legen zwei Zustände fest „true“ und „false“ und versehen diese mit den Styling-Angaben welche die HTML-Komponente haben soll wenn dieser Zustand aktiv ist (also am Ende der Animation). Der Zustand wird durch ein Attribut in unserem Controller festgelegt („[@moveInTop=’show‘]“) und wird durch einen entsprechenden Button verändert. Ist der Status „false“ ist das Element nicht zu sehen und wird nach oben außerhalb der Anzeige verschoben. Ändert sich nun der State auf „true“ (* => 1) wird zu Beginn der Animation das Styling der Transition angewendet, bevor mit der angegeben Animation-Einstellungen das Styling des States „true“ angewendet wird. Demzufolge wird das DIV zuerst auf die Position -50% verschoben, dann unter Verwendung der Animation auf den Wert 0% verschoben. Ändert sich der State hingegen auf „false“ (1 => 0) wird das Element wieder außerhalb des Bildschirms verschoben und auf nicht sichtbar gestellt. Zusätzlich sehen wir im obigen Beispiel einer der beiden Callbacks für Animationen („start“ und „done“) welche entsprechend des Animationsstatus ausgeführt werden und zum Beispiel dazu genutzt werden können um bei Abschluss der Animation eine weitere Aktion aus zu führen.
Neben den hier gezeigten Möglichkeiten bieten die Animationen von Angular 2 noch einige weitere Features wie automatische Berechnung von Größen und Keyframe- und paralelle Animationen die in der Dokumentation zu finden sind: https://angular.io/docs/ts/latest/guide/animations.html

Demo:
Github – Komponente

Github – Animationen

( es bietet sich an Animationen welche Komponenten übergreifend verwendet werden sollen zentral zu deklarieren, das Beispiel tut dies über eine globale TypeScript Datei „animations.ts“

Angular 2 testen mit Protractor (Update)

Protractor ist das Framework um Angular JS Anwendungen in einem End-To-End Test unter die Lupe zu nehmen. Zwar gibt es für Angular2 noch keine angepasste Version, sodass wir auf einige Features verzichten müssen, trotzdem ist auch Angular2 Bereit zum Test.

angular2_protractor2

Die Anforderung ist klar: wir wollen unsere Anwendung im Browser laufen lassen und Benutzer-Interaktionen simulieren um dann das Ergebnis zu prüfen. Unter der Haube von Protractor setzt auch dieses Framework dafür auf die bewährte WebDriver-API von Selenium. Selenium dient hier als Bindeglied zwischen unserem Test-Code und unserer Anwendung die im Browser abläuft. Die eigentlichen Tests werden schließlich mithilfe des Test-Frameworks „Jasmine“ zum Leben erweckt.

Projektsetup

Aber machen wir einen Schritt nach dem anderen und beginnen bei der Vorbereitung unseres Projektes. Zu allererst benötigen wir natürlich Protractor das mithilfe von npm schnell installiert ist:

 npm install –save-dev protractor

anschließend benötigen wir noch den bereits angesprochenen Selenium-Server der bei unseren Tests die Kommunikation zwischen dem Test-Code und den gewünschten Browsern übernimmt:

./node_modules/protractor/bin/webdriver-manager update

Für unser kleines Beispielprojekt verwenden wir Webpack als Bundler für unsere Webanwendung. Die Verwendung von Protractor ist aber nicht daran gebunden. Im Kern geht es darum eine laufende Anwendung bereit zu stellen, die protractor testen kann, ob die Anwendung mit Webpack,Browserfy oder Gulp/Grunt erzeugt wird spielt für unseren Test erst einmal keine rolle.

In unserem eigentlichen Task „test“ referenzieren wir ein Config-File das Protractor zur Initialisierung dient. In diesem legen wir fest unter welcher URL unsere Anwendung zur Verfügung steht, in welchem Ordner unsere Tests zu finden sind und welchen TestRunner (Jasmin) wir verwenden wollen

exports.config = {
  baseUrl: 'http://localhost:4300/',

  specs: [
    './target/**/*.test.js'
  ],
  exclude: [],

  framework: 'jasmine2',

  allScriptsTimeout: 110000,

  jasmineNodeOpts: {
    showTiming: true,
    showColors: true,
    isVerbose: false,
    includeStackTrace: false,
    defaultTimeoutInterval: 400000
  },
  directConnect: true,

  capabilities: {
    'browserName': 'chrome'
  },

  onPrepare: function () {
    var SpecReporter = require('jasmine-spec-reporter');
    jasmine.getEnv().addReporter(new SpecReporter({displayStacktrace: true}));
    
    
    browser.ignoreSynchronization = false;
  },


  /**
   * Angular 2 configuration
   *
   * useAllAngular2AppRoots: tells Protractor to wait for any angular2 apps on the page instead of just the one matching
   * `rootEl`
   *
   */
  useAllAngular2AppRoots: true
};

test/protractor.conf.js

Um unsere Tests aus zu führen sorgen wir für ein clean unserer kompilierten Testsourcen, führen TypeScript aus (um aus unseren TypeScript tests JavaScript-Dateien zu erzeugen) und führen anschließen Protractor aus:

rimraf ./test/target && tsc ./test/**/*.ts -t ES5 –outDir ./test/target && protractor test/protractor.conf.js

Zum Test bitte

In unseren eigentlichen Tests interagieren wir nun über den Selenium Server mit der laufenden Anwendung. Wir rufen also die zu testende URL auf, führen Aktionen aus und prüfen die Anzeige. Für das eigentliche Auffinden der Elemente stehen uns diverse so genannte Locators zu Verfügung die Elemente über Tag-Names, CSS, Texte und IDs ermittelt werden können. Wer schon Erfahrung mit dem Testen von Angular 1 hat wird allerdings einige Selektoren vermissen, so gab es z.B. die Möglich Selektionen aufgrund von NG-Model-Bindings zu ermitteln. Diese Angular-Spezifischen Selektoren werden leider (noch?) nicht Unterstützt und stehen somit erst einmal nicht zur Verfügung.

Zum auffinden der Elemente bleiben also die nicht Anguluar-Spezifischen Selektoren:

describe“ um eine Test-Suite zu deklarieren um unsere Test zu gruppieren
beforeEach“ um in diesem Beispiel vor jeder Funktion eine frische Navigation auf unsere Seite durch zu führen
it“ um einen einzelnen Test zu deklarieren
expect“ um ein erwartetes Verhalten fest zu legen
Protractor liefert die entsprechenden Möglichkeiten Elemente auf zu finden
element“ dient zum Finden von Elementen
by.id“ Selector für die Suche nach ID
getText()“ Aktion die den Text des gefunden Elementes liefert

Ein wichtiger Punkt ist das alle Aktionen die mit/auf dem DOM des Browsers ausgeführt werden asynchron sind und damit einer so genannten Promise zurück liefern. Die „expect“ Methode akzeptiert solche Promises und wartet mit der Auswertung bis das Ergebnis der asynchronen Anfrage geliefert wurde. Wollen wir jedoch selber mit dem Ergebnis einer solchen Aktion weiter arbeiten ist es notwendig eine entsprechende Callback-Methode zu implementieren, die erst dann ausgeführt wird, wenn die Aktion abgeschlossen wurde.

Wer seine Anwendung mittels TypeScript schreibt wird vermutlich auch die entsprechenden Test in dieser Form verfassen wollen. Die dafür benötigten Type Definitions liefert uns das npm – Modul „typings“ (npm install –g typings) mittels:

typings install --ambient --save selenium-webdriver
typings install --ambient --save angular-protractor
typings install --ambient --save jasmine

Ein entsprechender Test könnte dann so aussehen:

/// <reference path="../../typings/index.d.ts" />

class PageModel {
    static get messageButton() {
        return element.all(by.className('btn-primary')).first();
    }
    
    static get errorButton() {
        return element.all(by.className('btn-primary')).get(1);
    }
    
    static get nfoPanel(){
        return element(by.className('nfoPanel'));
    }
    
    static get error(){
        return element(by.id("toasty"));
    }
}

describe("Home-Page", () => {
    beforeEach(() => {
        browser.get('http://localhost:4300/#hello');
    })

    it("MessageButton", () => {
        RootPageModel.waitUntilSpinnerFinished();
        expect(PageModel.nfoPanel.isPresent()).toBeFalsy();
        expect(PageModel.messageButton.isPresent()).toBeTruthy();
        expect(PageModel.messageButton.getText()).toEqual("Hello from Angular-InMemoryApi");
        
        PageModel.messageButton.click();
        expect(PageModel.nfoPanel.isPresent()).toBeTruthy();
    })

    it("ErrorButton", () => {
        RootPageModel.waitUntilSpinnerFinished();
        expect(PageModel.errorButton.isPresent()).toBeTruthy();
        PageModel.errorButton.click()
        expect(PageModel.error.isPresent()).toBeTruthy();
    })
})


Wie wir sehen konnten ist das Testen mittels Protractor nicht schwer, dank Jasmin, Selenium und Webpack ist ein solcher Prozess schnell aufgebaut und die ersten Tests implementiert. Neben den hier gezeigten Möglichkeiten bietet Protractor und Jasmin noch einige weitere Features wie das Generieren von Reports oder sogar das Erstellen von Screenshots nach jedem Test. Mehr dazu finden Sie auf den entsprechenden Seiten der Hersteller.

Beispiel Projekt:

https://github.com/dominikmathmann/angular-webpack-starter

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.