When It's Business Critical

Write your first Gradle plugin

Need an expert?
You are more than welcomed to leave your contact info and we will be in touch shortly

A promise to hit the sweet spot

The Gradle build system promises to hit the sweet spot between Ant and Maven. It allows for convention over configuration build scripts (lesser code). The build scripts are highly readable and compact compared to its XML based cousins (looking at you Ant and Maven) thanks to its own groovy based DSL (domain specific language) .

Consistent and (fail-) fast builds are a key component to any successful Continuous Integration / Continuous Deployment (delivery) – process (from now on CI/CD). The build in itself is one of the most important parts of a modern CI solution. Gradle integrates well with Jenkins and Artifactory (binary storage). In a previous assignment I was tasked with replacing the old build system (Ant based), having worked with Gradle in the mobile world I began a journey to convert the old build to a new based on Gradle. The project to build was a Java EE application with several sub modules, here are the key points on why specifically Gradle was chosen:

  • Incremental build support (Gradle only builds files that has changed), checksums everything (both input and output files)
  • Parallel builds, Gradle utilizes all your cores if you have several Gradle sub-projects to build
  • Gradle has a daemon (a JVM running ready to build instantly), default enabled in Gradle 3
  • Fails fast (trust me it’s a good thing!). Builds are separated into three phases init, config and execute phases.
  • Groovy scripting support
  • Kotlin scripting support (as of Gradle 3.0), enables autocomplete for your build scripts in e.g. IntelliJ
  • Compact DSL: easy dependency declaration, a dependency needs one row of declaration
  • Support running Ant tasks from within Gradle (great for complicated legacy tasks from e.g. Ant, enables gradually rewrite of legacy tasks)
  • Debugging / profiling support, you can get a html report of where your build time is spent in the build process and also since Gradle runs in a JVM you can attach a debugger to the Gradle process itself
  • In a future release (targeted for 4.0, in 2017) there are plans for a distributed build cache. This means that same commit (branch) needs only to be built once and for any subsequent builds the artifacts can be fetched from the cache.
Magnus Borgström

Magnus Borgström

Senior Consultant

Magnus has a profile on stackoverflow: https://itinforma.se/member/magnus-borgstrom/ He is a strong believer in OSS and use several projects on a daily basis, in his spare time, he builds computers and explore IT related technologies such as container technology (docker, e.t.c.) / VM (VMware) and web technologies.

Gradle builds in action showing the powerful up to date checks that’s builtin in the tool

Part of the old Ant build was a JAXB generating task, converts XML schema definitions files to Java files that later consumes data (XML) to generate reports. The declaration in Ant for this was around 400 lines (most parts was specifying which XSD files to use while generating the Java ones). Four different modules were using the same JAXB generating code, which led me to the idea of reusing the code for generating the java files through JAXB. This is where Gradle plugins come in handy, since it promotes reusability and with Gradle’s powerful configuration injection makes the whole process a breeze. A complete rewrite of this part of the Ant build script was not planned.

A complete Gradle plugin

Below you’ll find the complete Gradle plugin for generating java files from xsd files. All the interesting stuff happens in the convert method. Most important is the call to project.ant here Gradle delegates a call to the Gradle built in Ant runtime with the XJC task target that takes in all the different parameters we set with the definitions inside BaseTask class, we set the parameters from the dynamically generated Gradle tasks, see later code snippet.

import org.gradle.api.DefaultTask
import org.gradle.api.Project

import org.gradle.api.tasks.TaskAction

// Base (holder) class
class BaseTask extends DefaultTask {
String packageName;
String packagePrefix = “com.name.xml”;
String packageSuffix = “generated”;
String partialPackageName;
String schemaDir = “schemas”;
String schemaFile = “*.xsd”;
String targetDir = project.projectDir.toString();
}

class JaxbTask extends BaseTask {
@TaskAction
void convert() {
// gets the jaxb configuration path (path to lib containing Ant task) from the build script
def classpath = project.configurations.jaxb.asPath;

println “Running xjc for packageName: $packageName, schemaDir: $schemaDir, schemaFile: $schemaFile”
// perform ant actions
project.ant {
taskdef(name: ‘xjc’, classname: ‘com.sun.tools.xjc.XJCTask’, classpath: classpath);

// target property is renamed to destdir in jaxb lib versions > 2
// package name decides where on the filesystem the generated files will land, and schema property points to the XSD file
xjc(target: targetDir, package: packageName) {
schema(dir: schemaDir, includes: schemaFile);
}
}
}
}

To use the above plugin in your Gradle build you need the following lines in your script:

buildscript {
dependencies {
// these depends on your version etc of the above groovy code
classpath 'groovy.org.gradle:JaxbTask:1.0.2'
}
}

class XSDHolder {
String dirName;
String packageName;
String schemaFile;

XSDHolder(String dirName, String packageName, String schemaFile) {
this.dirName = dirName
this.packageName = packageName
this.schemaFile = schemaFile
}
}

An array with holder data, example of one element:

new XSDHolder("order", "com.name.reports.order.generated.summary", "Report.xsd")

task jaxbGenerate() {
for (XSDHolder xsdHolder : holders) {
// TaskName is derived from Report.xsd -> [Report, .xsd] -> ReportJaxbTask
String taskName = xsdHolder.getSchemaFile().split("\\.")[0] + "JaxbTask"
// Dynamically creates several tasks (available as separate targets when using Gradle from cli)
task "${taskName}"(type: JaxbTask) {
// Here we configure all the properties that our task should have (executed during init phase, the task itself with the properties set is executed during execution phase)
schemaFile = xsdHolder.schemaFile
description "Generates java classes for the $schemaFile schema"
schemaDir = "schemas/" + xsdHolder.dirName
packageName = xsdHolder.packageName
targetDir = generatedSources
// tracks the input files (for up to date checks)
inputs.dir fileTree(dir: schemaDir, includes: ["**/${schemaFile}"])
// tracks the output files (for up to date checks)
outputDir = new File(generatedSources + "/" + packageNameToPath(packageName))
}
}
}
// The name of the dependency used by the groovy code to execute the jaxb task
configuration {
jaxb
}
// The dependency for running the groovy jaxb code
dependencies {
jaxb name: 'jaxb-xjc'
}