Gradle is klaar voor de ‘enterprise’

Wanneer je denkt aan ‘enterprise’, dan denk je aan grote complexiteit, veel verschillende soorten omgevingen (heterogeen), legacy code, één of meerdere grote projecten met veel teams, veel gedeelde componenten en andere zaken. Wat je vaak ziet in een dergelijke omgeving is dat het automatisch build proces niet meer onderhoudbaar is, de performance niet goed is en geen goede ondersteuning biedt voor speciale zaken die afwijken van wat de build tool standaard inhoudt. Gradle biedt een toolkit om in een ‘enterprise’ omgeving goed aan de slag te kunnen en deze zaken wel goed te regelen.

Voordat we gaan kijken wat Gradle allemaal te bieden heeft om je in een “enterprise” omgeving goed te helpen, is het goed om eerst te kijken naar wat Gradle is. Gradle is een build en automatiseer toolkit die draait op de Java Virtual Machine (JVM). Gradle is het resultaat van evolutie in build tools en probeert het beste te pakken uit bestaande build tools als Maven. Met Gradle kun je het bouwen, testen, publiceren en de deployment van software automatiseren. Maar Gradle kan ook gebruikt worden voor andere type projecten, zoals statische websites.

Eigenschappen Gradle
Een aantal belangrijke eigenschappen van Gradle zijn beschrijvende builds, waarbij je aangeeft wat je wilt uitvoeren, maar je niet hoeft aan te geven hoe. Ook is er een duidelijke build-by-convention met defaults die we kennen uit bijvoorbeeld Maven projecten. Maar er is wel de mogelijkheid om dit aan te passen aan standaarden in je eigen organisatie. Er is een zogenaamde Deep API, en dat betekent dat je op heel veel niveau’s aanpassingen kan maken in een build. Daarnaast zijn er multi-project builds, waarbij het mogelijk is om afhankelijkheden tussen projecten te definiëren. Gradle is in staat om zelf te bepalen of bij een wijziging binnen één project de van dat project afhankelijke projecten ook opnieuw gebouwd moeten worden. Dit hoef je dus niet zelf te doen, maar kun je overlaten aan Gradle.

De ondersteuning van plugins is ook belangrijk in Gradle. Een plugin kan een set van taken bevatten, maar bijvoorbeeld ook configuratie of nieuwe elementen toevoegen aan een build. Ook heeft Gradle ondersteuning voor dependency management en het gebruik van Maven/Ivy repositories. Als laatste is er de Gradle wrapper, waarmee het mogelijk is om een eigen Gradle distributie te definiëren met daarin je eigen standaarden, plugins en conventies, en deze op één manier te distribueren aan alle ontwikkelaars in de team(s).

Hoe werkt Gradle?
Gradle biedt drie niveau’s van functionaliteit aan. Als eerste is er de logica voor het uitvoeren van taken. Daarnaast is er een Domain Specific Language (DSL) voor verschillende domeinen, zoals Java, Groovy, Scala, JavaScript, C++, Docbook en meer. Daar bovenop kunnen weer implementaties komen die gebaseerd op conventies functionaliteit kunnen aanbieden. Dankzij de DSL is het mogelijk om binnen de enterprise eigen standaarden en conventies toe te passen, gebaseerd op bestaande Gradle functionaliteit of met eigen functionaliteit. De basis van Gradle is geschreven in Java en de DSL is Groovy. Dit betekent dat we de scripts schrijven in Groovy en niet in bijvoorbeeld XML. Het grote voordeel hiervan is dat een script ook code is en dat we die code kunnen refactoren, bijvoorbeeld om een betere onderhoudbaarheid te krijgen van onze build scripts.

Configuratie standaarden
Als je binnen je organisatie bepaalde standaarden wilt doorvoeren voor de builds of de bestaande standaarden en configuraties wilt gebruiken, dan kun je dat in Gradle aangeven. Je hebt toegang tot de API van Gradle, en er zijn verschillende plekken waar je kunt inhaken op het Gradle build proces. Zo is het bijvoorbeeld mogelijk om te signaleren welke dependencies gedefinieerd zijn in een project en daar een check op uit te voeren. Stel dat in je project hebt gedefinieerd dat er geen dependency mag zijn op de Jakarta Commons Logging library. In het volgende Gradle build script is te zien hoe we na het resolven van de dependencies voor het compileren kunnen zien welke dependencies er zijn en daarop ingrijpen.


configurations {
  compile.incoming.afterResolve = {
    final jclDependencies = compile.resolvedConfiguration.resolvedArtifacts.findAll {
      it.moduleVersion.id.group == ‘commons-logging’
   
    if (jclDependencies) {
      throw new Exception(“Found JCL dependencies ${jclDependencies*.moduleVersion*.id}”)
  }
}

Daarnaast bied Gradle de mogelijkheid om vele zaken te configureren, zelfs voor elementen die nog niet bestaan. Dit betekent dat we een standaard configuratie kunnen maken en dat die ook toegepast wordt op objecten die later door bijvoorbeeld andere projecten of builds worden aangemaakt. In het volgende script zien we hoe we alle taken van het type Test de property maxParallelForks van een waarde kunnen voorzien. Met deze property is het trouwens mogelijk om tests sneller uit te laten voeren, door ze parallel uit te voeren. Een andere feature van Gradle die ervoor zorgt dat builds ook snel zijn.


tasks.withType(Test) { // Ook nieuwe taken van type Test krijgen deze property
  maxParallelForks = project.numberOfForks
}
 
ext {
  numberOfForks = Math.max(2, (int) (Runtime.runtime.availableProcessors() / 2))
}

Plugins
Gradle gebruikt plugins om gerelateerde taken of extra configuratie mogelijkheden modulair op te nemen in een build. Plugins kunnen ook van elkaar afhankelijk zijn. Zo is er bijvoorbeeld een java-base plugin die extra configuration mogelijkheden biedt voor Java projecten, en een java plugin die daarop gebaseerd is, en dan weer specifieke taken aanbiedt die te maken hebben met bijvoorbeeld het compileren en testen van Java projecten.

Je kunt ook zelf een plugin schrijven en daarmee bepaalde standaarden toevoegen aan projecten. Deze plugin kan dan nog gedistribueerd worden via een repository zoals Nexus of Artifactory binnen de organisatie, zodat de code gedeeld wordt voor verschillende projecten.

Stel, je wilt configureren dat projecten altijd dependencies downloaden vanaf een central repository op het netwerk van de onderneming. Of je wilt bepaalde dependencies voor elk project toevoegen. Je kunt dit realiseren door het maken van een plugin. De plugin bevat de code voor onze conventies en wordt dan door andere projecten gebruikt. De volgende code is een voorbeeld van een plugin waarbij de repository voor dependencies wordt geconfigureerd, dit zorgt ervoor dat elk Java project een dependency krijgt op de SLF4J logging library.


package gradle.jdriven
 
import org.gradle.api
 
class JDrivenStandards extends Plugin<Project> {
  void apply(Project project) {
    final repositoryUrl = ‘http://nexus.jdriven.net/nexus/’
    project.repositories {
      all { ArtifactRepository repo ->
        if (!repo instanceof MavenArtifactRepository
   || repo.url.toString() != repositoryUrl) {
project.logger.info ‘Repository removed.’
remove repo
      }
    maven {
        url repositoryUrl
      }
    }
   
    project.plugins.withType(JavaPlugin) {
      project.dependencies {
        compile ‘org.slf4j:slf4j-api:1.7.5’
      }
    }
 
  }    
}

Je kunt de plugin deployen naar de eigen dependency repository. Andere projecten kunnen dan de plugin gebruiken. Stel, het volgende Java project wil de plugin gebruiken, dan kunnen we het classpath van de build uitbreiden via een buildscript configuratie en daarna de plugin gebruiken. Je kunt dit ook automatisch toepassen op projecten binnen de organisatie, daarvoor moet je de Gradle wrapper gebruiken, welke later nog besproken wordt.


buildscript {
  repositories {
    maven { url ‘http://nexus.jdriven.net/nexus/’ }
  }
  dependencies {
    classpath ‘jdriven:jdriven-standards:1.0’
  }
}
 
apply plugin: ‘java’
apply plugin: ‘jdriven-standards’

DSL extensies
Je hebt nu een aantal voorbeelden gezien van Gradle build scripts. De methoden en properties die je hebt gezien zijn een standaard onderdeel van de Gradle DSL. Maar je kunt de DSL ook uitbreiden en aanpassen, zodat je eigen termen kan gebruiken die gelden voor je organisatie. Dit biedt ook een manier om de build script beter leesbaar of onderhoudbaar te maken. Stel je wilt een vaste set van project dependencies aanbieden aan de ontwikkelaars, zodat ze via een eigen configuratie kunnen kiezen welke set ze willen gebruiken. In het volgende script maken we een nieuwe extensie JDrivenDependencies, met daarin methoden die sets van dependencies definiëren voor een test library Spock en logging library met implementatie SLF4J/Logback.


class JDrivenDependencies {
  private final Project project
 
  JDrivenDependencies(final Project project) {
    this.project = project
  }
 
  void useSpock() {
    project.dependencies {
      testCompile ‘'org.spockframework:spock-core:0.7-groovy-2.0'’
    }
  }
 
  void useSLF4J() {
    project.dependencies {
      compile ‘org.slf4j:slf4j-api:1.7.5’
      runtime ‘'ch.qos.logback:logback-classic:1.0.13'’
    }
  }
}
 
extensions.create ‘jdrivenDependencies’, JDrivenDependencies, project
 
jdrivenDependencies {
  useSpock()
  useSLF4J()
}

Incremental build support
Gradle heeft de feature incremental build support om de performance van builds te vergroten. De feature betekent dat een taak zelf kan bepalen of het nodig is om uitgevoerd te worden. Bijvoorbeeld een taak die code compileert, hoeft alleen uitgevoerd te worden als één van de source bestanden is gewijzigd of de output bestanden zijn veranderd. Als blijkt dat dit niet het geval is, dan is een taak up-to-date en hoeft niet uitgevoerd te worden. Dit kan een behoorlijke winst opleveren, vooral bij grotere projecten waarbij veel taken uitgevoerd moeten worden.

Deze feature geldt niet alleen voor de taken die al met Gradle worden meegeleverd, maar kun je ook toepassen bij taken die je zelf schrijft. Dit betekent dus dat je je eigen build logica ook kunt laten mee profiteren van de incremental build support in Gradle. Elke taak in Gradle heeft properties voor input (een bestand, directory of property) en output (file, directory). Als je deze properties gebruikt bij de definitie van een taak dan zal Gradle deze gebruiken om te bepalen of een taak up-to-date is of niet. In het volgende voorbeeld hebben we een eigen taak die een bestand maakt met de naam VERSION, met daarin de waarde van de project property version. Als de waarde van de version property verandert, moet het bestand opnieuw worden gemaakt.


version = '1.0'
 
task genVersionFile {
ext {
outputFile = new File("$projectDir/VERSION")
}
inputs.property 'version', project.version
outputs.files outputFile
 
doFirst {
outputFile.text = project.version
}
}

Als je de taak voor de eerste keer uitvoert, wordt het bestand aangemaakt. Nu kun je de taak nog een keer aanroepen. Is de version property niet veranderd, dan wordt de taak niet uitgevoerd en zie je op de command-line terug dat de taak up-to-date-is.


$ gradle genVersionFile
:genVersionFile
 
BUILD SUCCESSFUL
 
Total time: 4.069 secs
 
$ gradle genVersionFile
:genVersionFile  UP-TO-DATE
 
BUILD SUCCESSFUL
 
Total time: 2.345 secs

Multi-project builds
In een enterprise omgeving zal je vaak te maken hebben met multi-project builds. Bijvoorbeeld als code ondergebracht is in verschillende projecten, maar er wel een afhankelijkheid tussen de projecten is. De projecten kunnen zelfs gebruik maken van verschillende programmeertalen, zoals Java en Scala. Gradle biedt de mogelijkheid om een afhankelijkheid tussen projecten te definiëren via dependencies, en om de meest efficiënt mogelijke manier van bouwen te bepalen. Als ontwikkelaar hoef je niet zelf te bepalen welke projecten een afhankelijkheid hebben met jouw project of van welke projecten je eigen project afhankelijk is. Indien er veranderingen zijn in de code van projecten waar je afhankelijk van bent, dan kan Gradle die projecten meteen voor je bouwen met de taak buildNeeded. Ook hier geldt weer de incremental build feature van Gradle, dus als er geen veranderingen zijn dan worden die projecten ook niet opnieuw gebouwd.

Daarnaast is er nog de mogelijkheid om de projecten die afhankelijk zijn van je eigen project te laten bouwen, zodat je kunt kijken of de code wijzigingen die je doet, geen zaken in andere projecten verstoord met de taak buildDependents.

In Gradle kan je ook de configuratie van multi-project builds op verschillende niveau’s definiëren. Zo is het mogelijk algemene configuratie die door meerdere projecten wordt gebruikt op het hoogste niveau te definiëren en dan voor subprojecten deze configuratie te verfijnen. In het volgende build script wordt voor alle projecten de Java plugin gebruikt, en voor het project met de naam impl wordt ook nog de Groovy plugin toegepast.


allprojects { apply plugin: ‘java’ }
project(‘:impl’) { apply plugin: ‘groovy’ }

In de laatste versie van Gradle is ook de mogelijkheid toegevoegd om parallel projecten te bouwen. Deze feature is nog niet final, maar geeft aan dat Gradle performance van builds hoog in het vaandel heeft staan.

Gradle wrapper
De Gradle wrapper is een batch script voor Windows (en een shell script voor andere besturingssystemen), die automatisch een Gradle distributie kan downloaden en builds kan uitvoeren. De wrapper kan worden toegevoegd aan het versie beheer systeem en maakt het mogelijk dat ontwikkelaars niet eens zelf Gradle hoeven te installeren, maar door het gebruik van de wrapper ze wel Gradle builds kunnen uitvoeren.

Je hebt de mogelijkheid om een eigen Gradle distributie te maken voor je organisatie en deze door de wrapper te laten gebruiken. Deze distributie kan al een aantal specifieke eigen plugin’s bevatten, maar ook scripts die automatisch worden toegepast voor alle projecten waaraan ontwikkelaars werken. Op deze manier kun je er dus voor zorgen dat iedereen in de organisatie niet alleen dezelfde versie van Gradle gebruikt, maar ook nog eens met de plugin’s en scripts die van toepassing zijn binnen de organisatie.

Bij het maken van een eigen Gradle distribute kan je init scripts toevoegen. Deze init scripts staan in een directory init.d/ in de distributie en gelden voor de gehele Gradle installatie op een computer, dus niet alleen voor een project of multi-project build. In het volgende init script bijvoorbeeld kan je de standaard Maven repository opgeven die gebruikt wordt en een eigen plugin toevoegen aan alle projecten.


buildscript {
  repositories {
    maven { url ‘http://nexus.jdriven.net/nexus/’ }
  }
  dependencies {
    classpath ‘jdriven:jdriven-standards:1.0’
  }
}
 
allprojects { apply plugin: e‘jdriven-standards’ }

Conclusie
Gradle heeft veel features en mogelijkheden om gebruikt te worden in een enterprise omgeving. Je kunt met Gradle standaarden definiëren die moeten gelden voor de hele organisatie, en je kunt zelfs je eigen Gradle distributie maken die door iedereen gebruikt kan worden. De incremental build feature en het parallel uitvoeren van projecten betekent dat een build zo snel mogelijk is. Daarnaast biedt Gradle de mogelijkheid om project afhankelijkheden te configureren voor multi-project builds. En uiteindelijk is het ook mogelijk om de Gradle DSL uit te breiden met termen en zaken die geldig zijn voor je eigen enterprise, zodat de build script aansluiten bij de terminologie en ook beter leesbaar en onderhoudbaar zijn.

De laatste versie van Gradle (1.6) is te downloaden gradle.org/downloads .