
Write your first Gradle plugin
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, especially compared to XML-based tools like Ant and Maven. Its Groovy-based domain-specific language (DSL) simplifies scripting and enhances readability. We will guide you on how to write your first Gradle plugin
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 replaced an old Ant-based build system. I leveraged my Gradle experience from the mobile development world to convert the old build system to a Gradle-based solution.
The project
The project involved building a Java EE application with several submodules. The key reasons for specifically choosing Gradle are:
- 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)
- Gradle offers robust debugging and profiling support. An HTML report can be generated to detail where build time is spent during the process. Additionally, since Gradle runs in a JVM, you can attach a debugger directly to the Gradle process.
- In a future release (targeted for 4.0, in 2017) there are plans for a distributed build cache. This approach builds the same commit or branch only once. Subsequent builds retrieve artifacts from the cache, saving time and resources.
Gradle builds in action showing the powerful up to date checks that’s builtin in the tool
Reusing code
The old Ant build included a JAXB generation task designed to convert XML schema definition (XSD) files into Java files. These Java files later consumed XML data to generate reports. The Ant build script declared this task in about 400 lines, mostly specifying the XSD files for Java file generation. Four different modules relied on the same JAXB generation code, which led me to consider reusing it for efficiency.
This is where Gradle plugins proved invaluable. They promote reusability and, combined with Gradle’s powerful configuration injection, made the entire process significantly more streamlined and efficient. 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. The most crucial part is the call to project.ant. Here, Gradle delegates the task to its built-in Ant runtime, using the XJC task target. This target processes the various parameters defined within the BaseTask class. These parameters are dynamically configured by the Gradle tasks, as demonstrated in the upcoming 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'
}
