Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

How to Design a Modular Gradle Build Script

DZone's Guide to

How to Design a Modular Gradle Build Script

Zone Leader Matthew Casperson walks through the process of building a single build script for two applications with Gradle.

· DevOps Zone
Free Resource

Download “The DevOps Journey - From Waterfall to Continuous Delivery” to learn about the importance of integrating automated testing into the DevOps workflow, brought to you in partnership with Sauce Labs.

One of the oft-cited benefits of Gradle is that the Groovy language (upon which Gradle is built) provides developers with a very fine grained level of control over their build processes. Which sounds great in theory, but how does this manifest in practice?

To demonstrate the power of Gradle consider a scenario where you have two kinds of applications to build: the first is a stock standard JSP web application compiled into a WAR file, while the second is a Swing GUI desktop application. While the underlying libraries and platforms used by both apps are different, you do have some common elements to the build process; for example, both will access an internal Maven repo, and both will be analyzed by an internal SonarQube instance.

Your task is to create a single build script that allows you to share the common build logic, while still supporting the individual needs of the two applications.

Create the Interfaces

Let’s start by breaking down the requirements of the build script into reusable interfaces.

We need the ability to apply other Gradle plugins to the project. Our web application will need the war plugin, while our desktop app will need the Java plugin.

interface ApplyPlugins {
        void applyPlugins(Project project);
}

Let’s assume that our web application was originally built as part of a standard Eclipse project. Elcipse web projects placed the web app files in a directory called WebContent, whereas the standard Gradle project directory structure would have placed these files under src/main/webapp. To account for this custom directory structure, we need an interface that will allow us to override the Gradle project defaults.

interface CustomizeLocations {
        void customizeLocations(Project project);
}

Since we use an internal Maven repository, we need a way to configure our Gradle build with a custom Maven URL.

interface ConfigureMaven {
        void configureMaven(Project project);
}

Finally, we are uploading our projects to an internal instance of SonarQube for analysis.

interface ConfigureSonarQube {
        void configureSonarQube(Project project);
}

Create the Traits

Now that we have the interfaces defined, it’s time to add the actual logic that will build our applications. Groovy has support for traits, which (among other things) provides “composition of behaviors”. This is exactly the functionality we need in our scenario where we will be mixing and matching various behaviours within our build scripts.

For our web application, we need to apply the web plugin. To do this, we create a trait that implements the ApplyPlugins interface.

trait WebApplyPlugins implements ApplyPlugins {
    void applyPlugins(Project project) {
        assert project != null;
        project.plugins.apply('war');
        project.plugins.apply('maven');
        project.plugins.apply('sonar-runner');
    }
}

Our desktop Swing application needs the Java plugin.

trait DesktopApplyPlugins implements ApplyPlugins {
    void applyPlugins(Project project) {
        assert project != null;
        project.plugins.apply('java');
        project.plugins.apply('maven');
        project.plugins.apply('sonar-runner');
    }
}

We need a trait that overrides the Gradle project default web application directory to account for the fact that we are working on a project that is following the standard Eclipse web application project directory structure.

trait WebCustomizeLocations implements CustomizeLocations {
    void customizeLocations(Project project) {
        assert project != null;
        project.webAppDirName = 'WebContent';
    }
}

Our desktop Swing application uses the default directory structure, so it does not require a trait implements the CustomizeLocations interface.

Both builds will need to reference an internal Maven repository, which we configure in this trait.

trait SharedConfigureMaven implements ConfigureMaven {
    void configureMaven(Project project) {
        assert project != null;
        project.repositories.maven {
            url 'http://internalmavenrepo';
        }
    }
}

Finally, we need to create a trait to submit our applications to an internal instance of SonarQube.

trait SharedConfigureSonarQube implements ConfigureSonarQube {
    void configureSonarQube(Project project) {
        assert project != null;
        project.sonarRunner {
            sonarProperties {
                property "sonar.host.url", "http://my.server.com"
                property "sonar.jdbc.url", "jdbc:mysql://my.server.com/sonar"
                property "sonar.jdbc.driverClassName", "com.mysql.jdbc.Driver"
                property "sonar.jdbc.username", "Fred Flintstone"
                property "sonar.jdbc.password", "very clever"
            }
        }
    }
}

Construct the Plugins

The traits are then implemented by two custom Gradle plugin classes: one to build the web app, and the second to build the desktop app.

Here is the class that is used to build the web applications.

class WebPlugin implements
        Plugin<Project>,
        WebApplyPlugins,
        WebCustomizeLocations,
        SharedConfigureMaven,
        SharedConfigureSonarQube   {

    void apply(Project project) {
        applyPlugins(project);
        customizeLocations(project);
        configureMaven(project);
        configureSonarQube(project);
    }
}

Here is the class used to build the desktop application. Note that we are reusing the SharedConfigureMaven and SharedConfigureSonarQube traits, have not required any trait implementing the CustomizeLocations interface, and implemented the custom DesktopApplyPlugins trait.

With little more than a customized list of implemented traits, we have created a second plugin that shares common traits with the WebPlugin class, implements the unique traits required by our desktop applications, and ignores any traits that are not necessary.

class DesktopPlugin implements
        Plugin<Project>,
        DesktopApplyPlugins,
        SharedConfigureMaven,
        SharedConfigureSonarQube {

    void apply(Project project) {
        applyPlugins(project);
        configureMaven(project);
        configureSonarQube(project);
    }
}

Conclusion

As you can see, by breaking down the build script into interfaces that define discrete units of functionality, implementing those interfaces as traits, and then implementing the traits from the custom plugin class, we have a very flexible and fine grained way to compose Gradle plugins with a mix of unique and shared functionality.

The benefits of this approach really become apparent when you have multiple projects that all require some common functionality in their build scripts, and yet where several projects also have their own unique requirements. Gradle plugins composed of Groovy traits allow you to accommodate these similarities and differences in a way that should be quite familiar to any enterprise developer.

You can view a complete implementation of the custom plugin at https://github.com/mcasperson/sample-gradle-plugin, and sample applications that use it at https://github.com/mcasperson/sample-desktop-application and https://github.com/mcasperson/sample-web-application.

Discover how to optimize your DevOps workflows with our cloud-based automated testing infrastructure, brought to you in partnership with Sauce Labs

Topics:
gradle

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}