Schaukeln für Fortgeschrittene

Gradle im Praxistest

Marek Iwaszkiewicz, Moritz Zimmer

Seit kurzer Zeit steht das neue Build-System Gradle in der ersten finalen Version zur Verfügung. Zu den Konzepten von Gradle und den Vorteilen gegenüber anderen bewährten Build-Systemen wurde viel geschrieben – eine Betrachtung, ob Gradle den Ansprüchen professioneller Softwareprojekte genügt, blieb jedoch oft aus. Es wird Zeit, diese nun nachzuholen.

Die Grundlage aller Projekte für die Erstellung hochqualitativer Software bildet ein Build- System, mit dem aus dem Programmcode sowie den benötigten Ressourcen das auszuführende Programm erstellt wird. Bis vor Kurzem standen Entwicklern und Architekten aus dem Java-Bereich in erster Linie die Build-Systeme Ant [1] und Maven [2] zur Verfügung. Hier hat sich Maven in den letzten Jahren zunehmend durchgesetzt, sodass bei einem Großteil der neu aufgesetzten Projekte die Wahl auf Maven fiel. Zuletzt betrat mit Gradle ein neues Build-System die Bühne, das den Anspruch verfolgt, die Vorteile seiner beiden Vorgänger in sich zu konsolidieren [3].

Artikelserie
Teil 1: Projektbeschreibung, Entwicklungsumgebung, Anwendungsentwicklung
Teil 2: Qualitätssicherung, CI, umgebungsspezifische Konfiguration

Gradle liegt mittlerweile in einer ersten finalen Version vor und wird allein hierdurch für den Einsatz in Projekten interessanter. Zu Gradle selbst, den Vorteilen und den zugrunde liegenden Konzepten wurde in der Vergangenheit schon des Öfteren berichtet. Ob das Build-System den Ansprüchen professioneller Softwareprojekte genügt, wurde jedoch meistens nicht diskutiert. Diese Ansprüche sind allgemeiner Natur und unterscheiden sich nicht von denjenigen, die an andere Build-Systeme gestellt werden. Im Folgenden soll nicht auf die Frage eingegangen werden, welches von den vorhandenen Systemen das beste ist, sondern ob Gradle die allgemein geltenden Ansprüche erfüllt und die für den Einsatz in professionellen Projekten erforderliche Reife hat.

Ansprüche an ein Build-System

Was aber sind die allgemein geltenden Ansprüche? Im Kontext von Build-Systemen gelten nicht für alle Projekte immer dieselben Anforderungen. Dennoch gibt es eine Menge Kernaspekte, die ungeachtet der eingesetzten Technologien für alle Projekte und die jeweils eingesetzten Build-Systeme relevant sind. Aus Entwicklersicht stellt sich zum Beispiel die Frage nach der IDE-Unterstützung. Muss der Entwickler beispielsweise den Build immer über die Kommandozeile steuern oder bietet die eingesetzte Entwicklungsumgebung entsprechende Hilfestellungen? Von zentraler Bedeutung für alle Projekte ist zudem die Frage, wo die von der Anwendung benötigten Bibliotheken abgelegt und wie die Abhängigkeiten zwischen diesen aufgelöst werden (Dependency-Management). Darüber hinaus ist es wichtig, dass im Rahmen eines Builds Integrations- und Unit Tests automatisiert ausgeführt werden können – idealerweise inklusive der Ermittlung der Testabdeckung. Im Bereich der Integrationstests stellt sich zudem nicht nur die Frage, ob das Build-System die hierfür notwendigen Mittel bereitstellt, sondern auch, ob und wie eine von Unit Tests losgelöste Ausführung möglich ist. Auf diese und weitere Aspekte möchten wir im Rahmen dieser zweiteiligen Artikelserie eingehen, um schließlich die zu Beginn gestellte Frage zu beantworten, ob Gradle die für den Projekteinsatz notwendige Reife hat.

Wir stellen dabei ein fachlich einfaches Java-EE-Projekt vor. Das Projekt tangiert im Build-Kontext die wichtigsten Aspekte, auf die wir sequenziell eingehen und deren Umsetzung mit Gradle wir aufzeigen. Im ersten Artikel stellen wir zunächst das Beispielprojekt vor. Anschließend widmen wir uns der Entwicklungsumgebung und der Anwendungsentwicklung. Der zweite Artikel behandelt Aspekte zur Qualitätssicherung, Continuous Integration sowie der Erstellung umgebungsspezifischer Konfigurationen. Der Quellcode des vorgestellten Projekts steht auf GitHub zum Download bereit [4].

Projektbeschreibung

Für das Beispielprojekt haben wir uns für eine fachlich einfache, auf dem Java-EE-6-Stack basierende Webanwendung zur Verwaltung von Adressen entschieden. Als Applikationsserver verwenden wir den JBoss AS 7.1.1. Das Projekt wird in einer Multi-Projekt-Struktur umgesetzt, deren Subprojekte die Implementierung der üblichen Applikationsschichten für Anwendungslogik, Datenhaltung oder auch Präsentation kapseln. Eine Übersicht und Beschreibung der Projektstruktur kann über folgenden Aufruf angezeigt werden:

$ gradlew projects

Die Angabe aller am Build beteiligten Projekte erfolgt im Wurzelprojekt in der Datei settings.gradle. Auf gleicher Ebene konfigurieren wir in der Datei build.gradle Plug-ins, Dependencies und Tasks, die für alle Subprojekte gelten sollen. Die Build-Files in den Subprojekten selbst enthalten wiederum projektspezifische Einstellungen.

In Listing 1 sehen wir einen Ausschnitt aus der Datei build.gradle des Wurzelprojekts, in der die Angabe der Gradle-Plug-ins erfolgt, die auf alle Subprojekte angewandt werden sollen. Weiterhin enthält Listing 1 Angaben zu Bibliotheken, die in allen Projekten einzubinden sind. So wird zum Beispiel die Bibliothek jboss-javaee-6.0 benötigt, um in den Projekten von EJB3.1-Komponenten Gebrauch machen zu können. Zu beachten ist an dieser Stelle, dass der Geltungsbereich dieser Dependency auf compile gesetzt ist. Dadurch würde diese Bibliothek sowie deren transitive Abhängigkeiten mit in die Webanwendung gelangen, was beim Deployen der Anwendung zu Fehlern führen würde. Um diese Bibliothek sowie deren transitiv abhängige Bibliotheken aus der Webanwendung auszuschließen, muss in der Datei build.gradle des Webprojekts der Geltungsbereich auf providedCompile geändert werden (Listing 2). Nach dieser Kurzeinführung in das Beispielprojekt widmen wir uns als Nächstes der Entwicklungsumgebung.

subprojects {
  apply plugin: 'java'
  apply plugin: 'checkstyle'
  
  // Versionsbezeichnung unter Verwendung einer eigenen Klasse
  version = new org.gradle.Version(version_major: major,
    minor: minor, buildnumber: buildNumber, releaseType:releaseType) 

  dependencies {
    compile (
      "org.jboss.spec:jboss-javaee-6.0:$javaEEVersion"
    )
    
    testCompile (
      "org.easymock:easymock:$easyMockVersion",
      "de.akquinet.jbosscc:jbosscc-needle:$needleVersion"
    )
  }
}
    dependencies {
  // Bibliotheken werden nicht ins WAR übernommen
  providedCompile (
    "org.jboss.spec:jboss-javaee-6.0:$javaEEVersion"
  )

  // Umgebungsspezifische Dependency
  if (Boolean.TRUE ==  project.rootProject.ext.get('config').bootstrap) {
    runtime project(':addressManager-bootstrap')
  }
    }

[ header = Entwicklungsumgebung]

Entwicklungsumgebung

Die Implementierungsphase beginnt für Entwickler in der Regel mit dem Aufsetzen der lokalen Entwicklungsumgebung: Auschecken der Quellen aus dem Versionsverwaltungssystem (VCS), Integration dieser in die IDE, Installation des Build-Systems, Aufsetzen des Applikationsservers, um nur einige zu nennen. Die Installation der benötigten Gradle-Version kann dem Entwickler dabei durch die Verwendung des Gradle-Wrappers abgenommen werden. Beim Gradle-Wrapper handelt es sich im Wesentlichen um ein Batch- bzw. Shell-Skript zum Ausführen von Gradle. Dabei wird zunächst die konfigurierte Gradle-Version von einem dezidierten URL heruntergeladen, insofern diese auf dem System noch nicht vorhanden ist [5]. Somit ist zum einen keine manuelle Installation seitens der Entwickler notwendig. Zum anderen kann sichergestellt werden, dass alle Beteiligten die gleiche Version verwenden. Mit dem Wrapper besteht zudem die Möglichkeit, unternehmens- oder auch technologiespezifisch angepasste Gradle-Versionen zu erzeugen und im internen Netz bereitzustellen. Eine Möglichkeit einer solchen Anpassung besteht in der Bündelung des Wrappers mit so genannten Initialization Scripts.

Initialization Scripts werden automatisch vor dem eigentlichen Build ausgeführt und können beispielsweise genutzt werden, um allgemeingültige Repositories oder auch Dependencies zu deklarieren (Listing 3).

allprojects {
  buildscript {
    repositories {
      add(new org.apache.ivy.plugins.resolver.URLResolver()) {
        name = 'GitHub'
        addArtifactPattern 'http://cloud.github.com/downloads/[organisation]/[module]/[module]-[revi...'
      }
    }
    dependencies { classpath '...'}
  }
} 

Damit die Entwicklung des Produktionscodes starten kann, sollte es für den Entwickler möglich sein, die Quellen aus dem VCS, typischerweise in Form einer Multi-Projekt-Struktur, nahtlos in seine IDE zu importieren. Gradle bietet hier zwei Varianten: Einerseits stehen interne Plug-ins zur Generierung notwendiger Metadaten für Eclipse sowie IDEA zur Verfügung. In unserem Beispiel führt beispielweise ein Aufruf von

 $ gradlew eclipse

 im Wurzelprojekt dazu, dass für alle Projekte die Eclipse-typischen Dateien wie die für den Build-Path (.classpath) oder die Projektbeschreibung (.project) generiert werden. Hierbei werden die in den Projekten benötigten Bibliotheken aus den Dependencies ermittelt und in den Build-Path der Eclipse-Projekte übertragen. Die Projekte können anschließend manuell importiert werden. Auf der anderen Seite können herstellerspezifische Erweiterungen der IDEs selbst verwendet werden [6]. Die aus unserer Sicht aktuell beste Integration bietet hier das Gradle Eclipse Plug-in, das vom Team der Spring Tool Suite (STS) entwickelt wird. Am einfachsten lässt es sich über das Dashboard in STS direkt installieren, eine Verwendung in einer anderen Eclipse-Installation ist auch möglich [7]. Der Import unserer Multi-Projekt-Struktur kann über den bereitgestellten Wizard erfolgen (Abb. 1).

Abb. 1: STS Import Wizard

Beim Import können dabei Gradle Tasks angegeben werden, die sowohl vor als auch nach dem Import ausgeführt werden sollen. Wir nutzen diese Möglichkeit, um Eclipse-spezifische Builder sowie Natures für Eclipse-Plugins wie JRebel und Checkstyle in der Datei .project zu setzen (Listing 4). Der Entwickler muss diese somit nicht manuell den importierten Projekten hinzufügen. Des Weiteren müssen aktuell eigene Gradle-Configurations, in unserem Beispiel für Integrationstests, noch explizit angegeben werden, damit die entsprechenden Bibliotheken im Eclipse Build-Path zur Verfügung stehen [8].

eclipse {
  classpath {
    plusConfigurations += configurations.integrationTestRuntime
  }
  project {
    file {
      buildCommand 'net.sf.eclipsecs.core.CheckstyleBuilder'
      buildCommand 'org.zeroturnaround.eclipse.rebelXmlBuilder'
      
      beforeMerged { project ->
    project.natures.addAll(['net.sf.eclipsecs.core.CheckstyleNature', 'org.zeroturnaround.eclipse.jrebelNature'])
      }
    }
  }
}

Einmal importiert, stehen in Eclipse Funktionen zum Aktualisieren von Dependencies oder auch zum Ausführen von Gradle Tasks zur Verfügung. Das Editieren von Gradle-Skripten wird über einen DSL-Support mit Funktionen wie Syntax-Highlighting, Autoformatierung sowie einer eingeschränkten Autovervollständigung unterstützt.

Abschließend betrachten wir noch das Aufsetzen des Applikationsservers in der Entwicklungsumgebung. Dabei sollten alle Entwickler mit einer definierten Version, in unserem Beispiel JBoss 7.1.1, arbeiten. Um die lokale Installation möglichst vollständig zu automatisieren, verwenden wir das Cargo-Plug-in [9] (Listing 5). Mittels des Plug-in stehen dem Entwickler eine Reihe von Deployment Tasks zur Verfügung, wobei der konfigurierte Zielserver automatisch heruntergeladen und entpackt werden kann. Unsere Beispielapplikation lässt sich mit folgendem Aufruf lokal starten:

$ gradlew war cargoRunLocal –info

cargo {
  containerId = 'jboss71x'
  deployable { context = 'addressManager'}  
  local {
    homeDir = file("$parent.projectDir/jboss/extract/" + jbossVersion)
    homeDir.mkdirs()
    
    installer {
      installUrl  = jbossInstallUrl
      downloadDir = file("$parent.projectDir/jboss/download")
      extractDir  = file("$parent.projectDir/jboss/extract")
    }
  }
}

plugins.withType(JavaPlugin) {
  configurations {
    all { Configuration configuration ->
      configuration.incoming.afterResolve {
        def eclipseLinkDependencies = configuration.resolvedConfiguration.resolvedArtifacts.findAll {
          it.moduleVersion.id.group == "org.eclipse.persistence"
        }
  
        if (eclipseLinkDependencies) {
          throw new Exception("Found Eclipse-Link dependencies on configuration $configuration.name! - ${eclipseLinkDependencies*.moduleVersion*.id}")
        }
      }
    }
  }

  sourceCompatibility = "1.6"
  tasks.withType(Compile) {
    doFirst {
      assert sourceCompatibility == "1.6" : "The sourceCompatibility of $name was changed!"
    }
  }
}

[ header = Ausblick]

Ausblick

Im zweiten Artikel steigen wir tiefer in die Beispielanwendung ein und widmen uns Themen rund um die Qualitätssicherung. Hierbei stellen wir zuerst die Umsetzung der Testautomatisierung vor. Neben der Umsetzung der wichtigen Integrationstests behandeln wir auch Aspekte wie Testabdeckung und Codequalität. Darüber hinaus werfen wir einen Blick auf Continuous Integration und stellen die Erstellung umgebungsspezifischer Konfigurationen vor.

Geschrieben von
Marek Iwaszkiewicz
Marek Iwaszkiewicz
Marek Iwaszkiewicz ist für die directline AG als Senior Software Engineer tätig. Er beschäftigt sich seit Jahren mit Java EE Architekturen und Technologien. Seine Schwerpunkte liegen unter anderem im Bereich des Buildmanagements. Kontakt: @Marek_Iwaszkiewicz
Moritz Zimmer
Moritz Zimmer
Moritz Zimmer arbeitet als Teamleiter und Senior Software Engineer bei der Axel Springer ideas Engineering GmbH. Seine Schwerpunkte liegen unter anderem im Bereich des Build- und Content Managements sowie der Testautomatisierung. Kontakt: @Moritz_Zimmer
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.