Gradle Multi-Project Builds for Maven Users
This blog is the opportunity to check on new technologies:
I wanted to try another build solution for my Java backends. I used Maven for several years, both on OctoPerf and in my previous work experiences. Moving from Maven to Gradle is not necessarily easy, as the concepts involved are different.
This blog post is a guide for every developer with a Maven background that would like to give a try to Gradle (version 5.x), especially for authoring multi-module/multi-project builds.
It lists the differences between the two build solutions, step by step. You will learn what pitfalls to avoid, and how to setup code quality tools.
This is not a beginner guide on Gradle, it aims at describing the build setup of a complex project. However, be warned that my experience with Gradle is limited to this single project.
Maven VS Gradle¶
Apache Maven was first released in 2004 and holds the majority of the build tool market today.
Maven is a dependency management and a build automation tool, that relies on conventions (over configuration) to allow you to focus on what your build should do (only exceptions need to be written down).
By convention, Maven's configuration files are named pom.xml
.
They contain, amongst other things:
- Its dependencies on other external modules and components,
- The build order,
- The used directories,
- and the required plugins.
Gradle is a dependency management and a build automation tool, first released in 2007, that was built upon the concepts of Ant and Maven.
Gradle's configuration files are by convention called build.gradle
.
Unlike Ant or Maven, Gradle does not use XML files but instead its own DSL (Domain Specific Language) based on Groovy.
This leads to smaller configuration files, XML being more verbose than a specifically designed language.
IMHO, Gradle's configuration tends to be a bit more difficult than Maven. But that is probably only because I have much more experience with Maven than Gradle.
That being said, the thing that made me try Gradle is its build speed. Thanks to incremental tasks outputs, incremental compilation, and build caches, Gradle claims to be up to 100 times faster than Maven. And that's no joke!
Are you tired of waiting after your builds? So instead of buying a new laptop or building on expensive Cloud instances, why don't you give a try to Gradle?
From Maven Multi-module Project to Gradle Multi-project Build¶
For a start, searching on Google for the terms Gradle multi-module project
will lead you nowhere,
simply because this notion of splitting an application in sub-parts has a different name in the Gradle world:
It's called a multi-project build.
This naming gap between the two build solutions highlights how different they are from a conceptual point of view.
Maven Multi-module Project¶
In Maven you use XML to describe your build, more precisely pom.xml
files.
In a multi-module project, you define a root pom that lists all its sub-modules.
For example, here is an extract of the root pom.xml
file for the open-source Elastic CRUD project:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jeromeloisel</groupId>
<artifactId>elasticsearch-crud</artifactId>
<version>5.6.4-SNAPSHOT</version>
<packaging>pom</packaging>
[...]
<modules>
<module>db-repository-api</module>
<module>db-entity</module>
<module>db-conversion-api</module>
<module>db-conversion-jackson</module>
<module>db-repository-elasticsearch</module>
<module>db-spring-elasticsearch-starter</module>
<module>db-integration-test</module>
<module>db-scroll-api</module>
<module>db-scroll-elastic</module>
</modules>
</project>
You may want to have several layers of modules: a sub-module can in turn declare sub-modules of its own.
For example, the following root module (pom.xml
at the root of the project) declares two sub-modules sub-module
and impl-module
:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId>
<artifactId>root-project</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>sub-module</module>
<module>impl-module</module>
</modules>
</project>
In the folder sub-module
, you can create the following pom.xml
:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.test</groupId>
<artifactId>root-project</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>sub-module</artifactId>
<packaging>pom</packaging>
<modules>
<module>sub-module-api</module>
<module>sub-module-impl</module>
</modules>
</project>
It declares two sub-modules of its own: sub-module-api
and sub-module-iml
.
Finally, in the folder sub-module/sub-module-api
you would typically write the following pom.xml
file to declare a leaf module.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.test</groupId>
<artifactId>sub-module</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>sub-module-api</artifactId>
<dependencies>
[...]
</dependencies>
</project>
So, Maven pom.xml
files work as some kind of bi-directional tree structure:
- Each parent module lists all its children,
- Each child module declares its unique parent.
It took me a while to figure out that Gradle does not work like Maven at all!!!
Gradle Multi-project Build¶
Indeed, Gradle uses a flat structure to declare its sub-projects:
All information about the multi-project is written in the root settings.gradle
file!
Even with several layers of sub-projects, you only write one and only one settings.gradle
file at the root of the global project.
For example, in one of our Java backend we have several sub-projects (trimmed here for the sake of simplicity):
- backend
- applications
- command
build.gradle
- storage
build.gradle
build.gradle
- command
- commons
- command
- command-entity
- command-zt
build.gradle
- command-client
build.gradle
- storage
- storage-entity
- storage-file
build.gradle
- storage-client
build.gradle
- configuration
build.gradle
- command
- test-utils
build.gradle
build.gradle
settings.gradle
- applications
A first
applications
project is used to generate executable Jars with the taskbootJar
of Spring Boot Gradle plugin. Its two sub-projectsapplications/storage
andapplications/command
only contain dependencies and configurations required to startstorage
andcommand
applications.The second
commons
project contains libraries used by the applications. Some libraries are regrouped in their own sub-project likecommand
andstorage
. A standalone library,configuration
is used to read properties from environment variables of from the Stringapplication.yml
file.There is also a
test-utils
sub-project that is only used as a test dependency.
Here is an extract of the corresponding Gradle settings:
rootProject.name = 'backend'
include ':commons:command:command-entity'
include ':commons:command:command-zt'
include ':commons:command:command-client'
include ':commons:storage:storage-entity'
include ':commons:storage:storage-file'
include ':commons:storage:storage-client'
include ':commons:configuration'
include ':test-utils'
include ':applications:command'
include ':applications:storage'
project(':commons:command:command-entity').projectDir = file('commons/command/command-entity')
project(':commons:command:command-zt').projectDir = file('commons/command/command-zt')
project(':commons:command:command-client').projectDir = file('commons/command/command-client')
project(':commons:storage:storage-entity').projectDir = file('commons/storage/storage-entity')
project(':commons:storage:storage-file').projectDir = file('commons/storage/storage-file')
project(':commons:storage:storage-client').projectDir = file('commons/storage/storage-client')
project(':commons:configuration').projectDir = file('commons/configuration')
project(':test-utils').projectDir = file('test-utils')
project(':applications:command').projectDir = file('applications/command')
project(':applications:storage').projectDir = file('applications/storage')
Keep in mind that only one settings.gradle
must be used, it defines all the hierarchy of sub-projects, no matter their depth.
In these settings, only leaf projects have to be declared.
For example, there is no need to declare applications
and commons
sub-projects.
The syntax to declare a sub-project is include ':path:to:sub-project'
.
You may also add a line like project(':path:to:sub-project').projectDir = file('path/to/sub-project')
for each sub-project.
But it seems to work without it (the IDE Idea automatically generate it this way though ...).
Also, build.gradle
files are optional for sub-projects.
You only need them to declare dependencies for all projects or sub-projects.
allprojects and subprojects¶
Sub-projects without shared behaviors would be completely useless.
Even if the settings.gradle
file lets you defining sub-projects, it does not allow you to declare shared dependencies or tasks.
This can be done in the build definition (build.gradle
files) of each project, thanks to the allprojects
and subprojects
keywords:
allprojects
defines behaviors common to the current project and all its sub-projects,subprojects
defines behaviors common only to its sub-projects.
Note: they not only apply to direct sub-projects, but also to their respective children, if any.
For instance, the root backend
project declares both:
allprojects {
repositories {
[...]
}
jacoco {
[...]
}
}
subprojects {
apply plugin: 'java'
dependencies {
[...]
}
}
allprojects
is used to declare external repositories for every project as well as code quality related tasks such as Jacoco and SpotBugs.
subprojects
mark all sub-projects as Java projects thanks to the Java plugin, and declares dependencies that are shared by all sub-projects.
Build Plugins¶
Plugins are a good way to add features to your build with minimum efforts.
For instance, the Spring Boot plugin provides many convenient features (for both Maven and Gradle versions):
- It generates a runnable über-jar, that we use in Docker images,
- It searches for the
public static void main()
method to flag as a runnable class, - It provides a built-in dependency resolver that sets the version number to match Spring Boot dependencies.
With Maven the configuration is straightforward, please check out the Spring multi-module guide and the resulting code sample.
The configuration is a bit more complicated as one single Gradle build is used to:
- Build
commons
libraries that rely on Spring boot managed dependencies and Spring WebFlux dependencies, - Build
applications
and their executable bootJars.
So, the root gradle.build
of our backend has the following configuration:
plugins {
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
}
subprojects {
ext {
springBootVersion = '2.1.6.RELEASE'
springReactorVersion = '3.2.10.RELEASE'
}
apply plugin: 'io.spring.dependency-management'
dependencies {
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-webflux', version: springBootVersion
testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: springBootVersion
testCompile group: 'io.projectreactor', name: 'reactor-test', version: springReactorVersion
testImplementation group: 'org.springframework.boot', name: 'spring-boot-devtools', version: springBootVersion
}
dependencyManagement {
imports { mavenBom("org.springframework.boot:spring-boot-dependencies:${springBootVersion}") }
}
}
For all sub-projects (applications and commons alike):
- It declares and applies the Spring dependency management plugin to all projects,
- It also declares runtime and test dependencies to Spring Boot WebFlux.
This allows us to use SpringBoot WebFlux in every class of our project.
To generate executable Jars, we must configure the Spring Boot plugin on the applications
sub-project:
plugins {
id 'org.springframework.boot' version '2.1.6.RELEASE'
}
subprojects {
buildscript {
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'org.springframework.boot'
}
This adds the booJar
task on every child project of applications:
Gradle task launcher in Idea
Dependencies Management¶
Dependencies management in Gradle can be relatively similar to Maven's, even though it is not as straightforward to setup (probably for the sake of flexibility).
Declaring External Repositories¶
The first thing to be aware of when switching to Gradle is that it does not come with a central repository out of the box.
So, in order to declare dependencies to external libraries, you must first declare the Maven central repository with the syntax mavenCentral()
.
We declare the Maven Central repository for all projects in the root build.gradle
file:
allprojects {
repositories {
mavenCentral()
}
}
Note: We use the Jacoco plugin for all projects (even the root one). This plugin has Maven dependencies. Otherwise we could have declared the
mavenCentral()
in thesubprojects
section.
With Maven, some of your build plugin may have their own dependencies. In such case you can declare them with the following syntax:
<project>
[...]
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.2</version>
<dependencies>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant</artifactId>
<version>1.7.1</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
[...]
</project>
The same principle applies to Gradle: some of your build plugins may need access to external dependencies. Dependencies which are hosted, for example, on Maven central.
In such case you must declare the repository inside the buildscript
tag:
subprojects {
buildscript {
repositories {
mavenCentral()
}
}
}
To summarize:
- The repositories on the root level are used to fetch all dependencies you need to test, compile or build your project,
- The repositories in the
buildscript
block are used to fetch the dependencies used by your build, for example by external plugins.
One last thing with external repositories, in Maven you can use the file ~/.m2/setting.xml
to declare additional external repositories:
[...]
<repositories>
<repository>
<id>repo.jenkins-cimvn.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
</repository>
</repositories>
[...]
In Gradle, you can do the same in your root build.gradle
file with the following syntax (here to add the License4j repository):
subprojects {
maven {
url "http://www.license4j.com/maven/"
}
}
Simple Dependency Declaration¶
The syntax to declare a dependency in Maven is more verbose than in Gradle:
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-webflux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
</dependencies>
This could be a general statement: Maven build script are more verbose that Gradle ones, because of the XML.
That being said, a dependency declaration in Gradle contains exactly the same information:
dependencies {
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-webflux
compile group: 'org.springframework.boot', name: 'spring-boot-starter-webflux', version: '2.1.6.RELEASE'
}
The compile
scope for the dependency is discouraged: you may prefer to use implementation
with Gradle 5.
Centralizing Dependencies¶
When building a complex application with multiple Maven modules or Gradle projects, it's important to centralize common dependencies in a single place.
For instance, if you use Guava or Spring in all your Java projects, you should not declare them in each pom.xml
file of a multi-module Maven project.
Instead, you write them once in your root porm.xml
, and they will be available for all sub-modules:
<project>
[...]
<properties>
<guava.version>23.2-jre</guava.version>
</properties>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
</dependencies>
<modules>
<module>db-repository-api</module>
<module>db-entity</module>
</modules>
[...]
</project>
Another good habit is to also centralize dependency versions in a single place.
In the example above we only use one Guava library.
But we could also use Guava Testlib.
Having the version written is the <properties>
section and used with the ${guava.version}
syntax avoid a duplication of information:
When you want to upgrade to a newer Guava version, you only have to do it in a single place.
This prevents you from forgetting to update one of the two libraries!
The syntax is quite similar in Gradle.
For example here is an extract of the root build.gradle
:
subprojects {
ext {
springBootVersion = '2.1.6.RELEASE'
}
dependencies {
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-webflux
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-webflux', version: springBootVersion
}
}
The variables are defined as project extra properties in the ext
tag.
In Gradle, you need to place your dependencies
section inside the subprojects
in order to make it apply to all sub-projects of the current build (defined in your settings.gradle
file).
Dependency Management¶
The Maven dependency management section is a mechanism for centralizing dependency information.
Use the <dependencyManagement>
section of your parent pom.xml
to declare dependencies version and scope for all sub-modules.
For example, here we declare that the SLF4J version is 1.7.26 in our root prm.xml
file:
<project>
[...]
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.26</version>
</dependency>
</dependencies>
</dependencyManagement>
[...]
</project>
Then, in a child module, we can only declare the SLF4J api dependency without the version:
<project>
[...]
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
</dependencies>
[...]
</project>
Thanks to the dependencyManagement tag in the parent pom, the version is automatically set to the same 1.7.26 for all child modules.
As far as I know, such mechanism is not present natively in Gradle.
But you can use a Spring plugin to do it: Dependency Management Plugin.
dependencyManagement {
imports { mavenBom("org.springframework.boot:spring-boot-dependencies:${springBootVersion}") }
}
Cross Project Dependencies¶
In Maven, declaring a cross-modules dependency is done like any other dependency declaration.
Lets take for example the Elastic-CRUD project.
A sub-module db-repository-api
has a dependency towards db-entity
.
The pom.xml
for the DB-Entity module is quite simple:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.jeromeloisel</groupId>
<artifactId>elasticsearch-crud</artifactId>
<version>5.6.4-SNAPSHOT</version>
</parent>
<artifactId>db-entity</artifactId>
</project>
Then in the DB-Repository-API module we declare the dependency like any other:
<project>
[...]
<dependencies>
<dependency>
<groupId>com.jeromeloisel</groupId>
<artifactId>db-entity</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
[...]
</project>
But in Gradle, just as there is no default central repository for dependencies management, there is no local repository. So we need to declare dependencies towards other sub-projects, not towards the generated JARs.
Let's come back to our sample application. It's divided in two global sub-projects:
-
applications
that generates executable Jars, -
commons
that contains libraries.
The command
application uses the command-zt
library (available in commons/command/command-zt
).
So in order to declare the dependency, we must use the following syntax in the build.gradle
of the command application:
dependencies {
implementation project(':commons:command:command-zt')
}
Note that it differs from the syntax used to declare external dependencies: implementation group: 'com.external', name: 'library', version: 42
Also, you might want to read the next section about test dependencies that covers cross-project test dependencies.
Test Dependencies¶
This section describes how to:
- Import simple test dependencies used to compile and run your unit tests (but not required to compile the
src/main
classes), - Import cross sub-modules/sub-projects test dependencies, i.e. when you want to use
src/test
Classes of one module inside another one's tests.
Simple Test Dependencies¶
Importing a test dependency in Maven is easy.
You just need to use the test
scope:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
<scope>test</scope>
</dependency>
Similarly, Gradle has a simple syntax with the testImplementation
configuration:
dependencies {
testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: springBootVersion
}
In both cases, these dependencies are only available during compilation and execution of the unit tests, not for the normal classes.
test-jar
Test Dependencies¶
It's a bit more complex, both with Maven and Gradle, when you want to use test Classes of one sub-module/sub-project inside another one's tests.
The use case is that you want to have common test classes used to compile and run unit tests in many of your sub-projects. For example, we have a test-utils library that contains two classes:
TestUtils.java
to run EqualsVerifier and NullPointerTester on a given class,ResourceUtils.java
to load files from thetest/resources
folder.These classes are placed inside the
test
folder of the project, as they are only used in unit tests. Otherwise, we would have to unit test them to keep a good test coverage.
With Maven, you must configure the maven-jar-plugin accordingly.
For instance with the Elastic-CRUD root pom.xml
file:
<project>
[...]
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
<configuration>
<skipIfEmpty>true</skipIfEmpty>
</configuration>
</plugin>
</plugins>
</build>
[...]
</project>
This sample configuration will generate test-jar
for each sub-module that has test classes.
Then you can import these test-jar jars like this:
<project>
[...]
<dependencies>
<dependency>
<groupId>com.test</groupId>
<artifactId>my-test-sub-module</artifactId>
<version>version</version>
<scope>test</scope>
<type>test-jar</type>
</dependency>
</dependencies>
[...]
</project>
- The
<scope>test</scope>
tells Maven that this dependency is only used for unit tests, - The
<classifier>test-jars</classifier>
tells Maven to import the Jar of the dependency with thesrc/test
classes instead of the classicsrc/main
.
In Gradle, you must add the following build configuration to your root build.gradle
file:
subprojects {
configurations {
testArtifacts.extendsFrom testRuntime
}
task testJar(type: Jar) {
archiveClassifier.set('test')
from sourceSets.test.output
}
artifacts {
testArtifacts testJar
}
}
In the Gradle world this generates a testJar
artifact, just like the maven-jar-plugin test-jar
goal does.
Then, to use the test dependency, simply import it with the following syntax:
dependencies {
testCompile project(path: ':test-utils', configuration: 'testArtifacts')
}
- The
testCompile
tells Gradle that this dependency is only used for unit tests, - The
configuration: 'testArtifacts'
tells Gradle to import thesrc/test
artifact of the dependency instead of the classicsrc/main
.
Java Version¶
In a multi-module maven project, you need to define the Java version only once in your root pom.xml
file:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>11</release>
<optimize>true</optimize>
</configuration>
</plugin>
The configuration is done on the maven-compiler-plugin.
Note that if your are have troubles compiling a Java 11 project with Maven 3.3.9, you may add the configuration <useIncrementalCompilation>false</useIncrementalCompilation>
to this plugin.
In Gradle, the Java compilation is handled by the Java Plugin.
Here again, you can define the Java version only once for a multi-project build by setting the following configuration in your root build.gradle
.
subprojects {
sourceCompatibility = 11
}
It adds the sourceCompatibility
property to all sub-projects. This property is then used by the Gradle build Java Plugin:
Java version compatibility to use when compiling Java source. Default value: version of the current JVM in use JavaVersion. Can also set using a String or a Number, e.g. '1.5' or 1.5.
Code Quality¶
Since the beginning of OctoPerf we were always very considerate with code quality.
We extensively use SonarQube for code coverage and bugs analysis. I won't come back on how to generate Sonar reports using Maven, as it would require a dedicated blog post.
So let's skip directly to an overview of two code quality tools that can be ran with Gradle, without the requirement of an external SonarQube server.
How To Generate JaCoCo Reports With Gradle¶
JaCoCo is a free library that can generate test-coverage HTML reports.
The following integration in a Gradle multi-projects build allows you to generate a single report that covers all projects at once.
Every configuration is done in the root build.gradle
configuration file.
First you need to apply the Jacoco Gradle plugin to all projects and set the version used to 0.8.2
.
This plugin needs access to the Maven repository, so you must also add Maven in the external repositories section:
allprojects {
apply plugin: 'jacoco'
repositories {
mavenCentral()
}
jacoco {
toolVersion = '0.8.2'
}
}
Then, you must configure JaCoCo to generate XML reports as well as HTML reports.
Also, the check
task depends on the JaCoCo report one (jacocoTestReport
): it generates test reports automatically when the check
task is executed.
subprojects {
jacocoTestReport {
reports {
html.enabled = true
xml.enabled = true
csv.enabled = false
}
}
check.dependsOn jacocoTestReport
}
Finally, create a jacocoRootReport
that aggregates all sub-project reports into one single big report:
task jacocoRootReport(type: JacocoReport) {
dependsOn = subprojects.test
getAdditionalSourceDirs().setFrom(files(subprojects.sourceSets.main.allSource.srcDirs))
getSourceDirectories().setFrom(files(subprojects.sourceSets.main.allSource.srcDirs))
getClassDirectories().setFrom(files(subprojects.sourceSets.main.output))
getExecutionData().setFrom(files(subprojects.jacocoTestReport.executionData))
reports {
html.enabled = true
xml.enabled = true
csv.enabled = false
}
onlyIf = {
true
}
doFirst {
getExecutionData().setFrom(files(executionData.findAll {
it.exists()
}))
}
}
To generate the test coverage report, simply run the command ./gradlew jacocoRootReport
.
Once the task is done, the report is available in the sub-folder build/reports/jacoco/jacocoRootReport/html
of your root project directory.
How To Spot Bugs With Gradle¶
SpotBugs, the spiritual successor of FindBugs, is a free program which uses static analysis to look for bugs in Java code.
Once again, its usage with Gradle is eased thanks to a plugin: SpotBugs Gradle Plugin.
In a multi-projects Gradle build, you can declare the plugin in the root build.gradle
file to be applied on every sub-project:
plugins {
id 'com.github.spotbugs' version '2.0.0'
}
subprojects {
apply plugin: 'com.github.spotbugs'
dependencies {
implementation group: 'com.google.code.findbugs', name: 'annotations', version: '3.0.1'
}
spotbugsMain {
reports {
xml.enabled = false
html.enabled = true
}
}
spotbugsTest {
reports {
xml.enabled = false
html.enabled = true
}
}
check.dependsOn spotbugsMain
}
The check
task depend on the spotbugsMain
one: SpotBugs reports are automatically generated when the check
task is executed.
So running ./gradlew check
will execute SpotBugs on each sub-project and generate a report.
Also, the build fails in case of any spotted bug.
Unfortunately I did not manage to generate a global report for the whole project. So the information of potential bugs is split across all sub-project SpotBugs reports.
Conclusion¶
I hope this blog post is helpful for anyone that would like to give a try to Gradle for a complex multi-project build.