Gradle build file organisation and reuse

|

Some ideas on how to organise and reuse Gradle build files in a multi-project build.

Introduction

Our project was to create a set of related microservices in Java using Spring Boot, built as a set of Gradle projects all under a single root project.

We wanted to reuse Gradle build scripts as much as possible and reduce the amount of copy-and-paste between service projects.

General layout

(root)
  |
  +- build.gradle
  +- settings.gradle
  +- gradle/
  |    +- code-standards.gradle
  |    +- idea.gradle
  |    +- package.gradle
  |    +- spring-actuator-info.gradle
  |    +- spring-boot.gradle
  |    +- spring-rest-docs.gradle
  |
  +-- microservice-a/
  |     +- build.gradle
  |     +- gradle.properties
  |     +- src/
  |
  +-- microservice-b/
        +- build.gradle
        +- gradle.properties
        +- src/

Gradle scripts

Project root

The project root contains settings.gradle with:

rootProject.name = 'api-microservices'

include 'microservice-a', 'microservice-b'

There can only be one settings.gradle file and it must be in the root directory.

The root build.gradle contains information in common with all projects:

// Common build versions for Spring Boot, Cloud and Prometheus, and Boot plugin.
buildscript {
    ext {
        springBootVersion = '1.5.4.RELEASE'
        springCloudVersion = 'Dalston.SR1'
        springMetricsVersion = '0.5.1.RELEASE'
        prometheusVersion = '0.0.23'
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

// External plugins cannot be declared in an external build script but will be available to
// all child projects.
plugins {
    id 'org.asciidoctor.convert' version '1.5.3'
    // Plugin to generate git.properties on the classpath that will be included
    // in the Actuator info response.
    id 'com.gorylenko.gradle-git-properties' version '1.4.17'
}

group 'com.example'

// These things are applied to all projects.
allprojects {

    apply plugin: 'java'
    sourceCompatibility = 1.8

    repositories {
        maven {
            // Search local repo first.
            url 'http://repo.example.com/public-remote-virtual'
            mavenCentral()
        }
    }
}

If you don’t want all your services to use the same version of Spring Boot (etc.) you will need a different configuration.

Service projects

The build.gradle for each service project is now simplified to start with:

apply from: '../gradle/spring-boot.gradle'
apply from: '../gradle/idea.gradle'
apply from: '../gradle/code-standards.gradle'
apply from: '../gradle/spring-rest-docs.gradle'
apply from: '../gradle/spring-actuator-info.gradle'
apply from: '../gradle/package.gradle'

Other, project-specific declarations follow. For example, to use Redis:

dependencies {
    compile('org.springframework.boot:spring-boot-starter-data-redis')
}

Each project contains a gradle.properties file containing the current version of the service. It is separate from build.gradle so it can be read and written by the Gradle Release Plugin.

Common, reusable scripts

Here is a brief description of the reusable scripts that we created.

File Description
spring-boot.gradle Apply Spring Boot plugin and specify Spring and other dependencies for all projects.
idea.gradle Apply and configure the IntelliJ IDEA plugin.
code-standards.gradle Apply and configure Checkstyle and Jacoco plugins.
spring-rest-docs.gradle Apply and configure the AsciiDoctor plugin; also include the generated doc in the assembled JAR.
spring-actuator-info.gradle Apply and configure the Gradle Git Properties plugin; Specify that Spring Boot generates build-info.properties that will returned by the Actuator info endpoint.
package.gradle Apply and configure the Gradle Linux Packaging plugin to package the service as an RPM.

Postscript

We also used the Gradle Release Plugin but I was not able (in the time available) to get it to work from an external script using apply from. This meant that each service build script contained the same big block of code (not shown here).

Perhaps I will solve that one in the future.